Short description
🏷️ LayerLeaderPlugin is an AutoCAD and Civil 3D .NET WinForms plugin for placing DB-standard MLeader labels from layer names, dynamic block visibility states, and managed JSON label databases.
Command:
🧰 Main commands: LLP, SHOWPANEL, PLACELEADER, ADDLAYER, REMOVELAYER, LISTLAYERS, ADDBLOCK, REMOVEBLOCK, LISTBLOCKS, LLUPDATEFIELDS, SETUPDIMSTYLES, LOADSETUPDIMSTYLES. Run NETLOAD, load LayerLeaderPlugin.dll, then use LLP or SHOWPANEL to open the modeless Layer Leader panel.
Description:
🚀 LayerLeaderPlugin is a production-focused AutoCAD Civil 3D plugin that automates MLeader label placement based on layer rules, block visibility rules, and drawing standards.
📌 The plugin loads a modeless WinForms panel that can stay open while the user works in Model Space or Paper Space. It reads layer_database.json and block_database.json from the DLL folder, displays editable label rules, and places MLeader annotations using the configured DB MLeader style.
🧩 The Leader workflow supports selecting drawing entities, picking arrowhead and text points, and placing a linked label that can be refreshed later. The Layer tab manages layer-to-label mappings, while the Blocks tab maps dynamic block names and visibility-state patterns to clean display labels.
🧠 The project also includes integrated AutoLISP helpers for dimension-style override cleanup and background-mask workflows, giving CAD managers one combined tool for annotation placement, style control, and Civil 3D production cleanup.
🔐 Version 1.0.027 includes a build-path fix for long OneDrive and support-file folders by using a short intermediate build folder, helping Visual Studio avoid classic Windows path-length failures during MSBuild generation.
Helper function: (if any)
Functionalities:
✅ Modeless AutoCAD toolbar panel with the LLP and SHOWPANEL commands.
✅ PLACELEADER command for entity-based MLeader annotation using DB-standard display labels.
✅ Layer database manager based on layer_database.json with add, remove, list, refresh, apply, undo, and delete workflows.
✅ Dynamic block label manager based on block_database.json with wildcard matching for block names and visibility states.
✅ Place on Block workflow for dynamic blocks, including visibility-state label resolution such as matching X_2.80_2.30_* patterns.
✅ LLUPDATEFIELDS command to refresh source-object-backed MLeader label fields in the active drawing.
✅ Automatic DB MLeader style creation and activation on plugin load.
✅ Automatic BoQ.01_Text layer setup using ACI color 11 and Continuous linetype.
✅ Setup Styles integration through SETUPDIMSTYLES and LOADSETUPDIMSTYLES commands.
✅ Integrated SetupOverriteDimStyles.lsp and Background_Mask.lsp copied beside the DLL at build time.
✅ Native Setup Styles panel for Text, Dimensions, and MLeader style control.
✅ Background mask controls for text, dimensions, and multileaders.
✅ Arrowhead symbol support using AutoCAD standard MLeader arrowheads.
✅ UCS-aware position handling for placed leaders and block labels.
✅ Horizontal label rotation convention preserved after the v1.0.023 block-label correction.
✅ Log tab with timestamped activity, store log, clear log, copy log, and auto-persist options.
✅ Visual Studio 2022, .NET Framework 4.8, x64, Windows Forms, Newtonsoft.Json, and AutoCAD Civil 3D ObjectARX references.
Result:
🎯 The result is a controlled annotation workflow for AutoCAD and Civil 3D: users can place consistent MLeader labels, manage layer and block label rules, update linked labels, clean style overrides, and keep project annotation standards synchronized from one modeless plugin panel.
📊 The tool is useful for CAD managers, Civil 3D production teams, infrastructure designers, and drafting users who need repeatable leader labels, structured layer naming, dynamic block annotation rules, and cleaner drawing standards.
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="LayerLeaderPlugin"> <src:RibbonToolTip.ExpandedContent> <StackPanel> <TextBlock Background="AntiqueWhite" TextAlignment="Left"> <Bold>Function Syntax: LLP / SHOWPANEL / PLACELEADER / ADDLAYER / REMOVELAYER / LISTLAYERS / ADDBLOCK / REMOVEBLOCK / LISTBLOCKS / LLUPDATEFIELDS / SETUPDIMSTYLES / LOADSETUPDIMSTYLES</Bold> <LineBreak/> <Bold>Version: 1.0 Date: 29.06.2026 16:23:18</Bold> <LineBreak/> <Bold>Version: 1.1 Date: 29.06.2026 4:23:24PM</Bold> <LineBreak/> <Bold>Version: 1.1 Date: 02.07.2026 3:10:31PM</Bold> <LineBreak/> <Hyperlink>N/A</Hyperlink> <Bold><Run Foreground="DodgerBlue">Function Syntax: LayerLeaderPlugin</Run></Bold><LineBreak/> <Bold><Run Foreground="DodgerBlue">Version: 1.0.027</Run></Bold><LineBreak/> <Bold><Run Foreground="LimeGreen">Description</Run></Bold><LineBreak/> <Run Foreground="White">🚀 LayerLeaderPlugin is an AutoCAD and Civil 3D .NET WinForms plugin for controlled DB-style MLeader label placement, layer-based label mapping, dynamic block visibility labeling, and CAD annotation standard management.</Run><LineBreak/> <Run Foreground="White">📌 The tool opens a modeless panel with dedicated Leader, Layer, Blocks, Setup Styles, and Log workflows so production users can place, edit, update, and audit labels without closing their active drawing workflow.</Run><LineBreak/> <Run Foreground="White">🧩 It reads and writes </Run><Bold><Run Foreground="Orange">layer_database.json</Run></Bold><Run Foreground="White"> and </Run><Bold><Run Foreground="Orange">block_database.json</Run></Bold><Run Foreground="White"> beside the loaded DLL, making label rules portable and easy to maintain.</Run><LineBreak/> <Run Foreground="White">🔐 Version </Run><Bold><Run Foreground="Orange">1.0.027</Run></Bold><Run Foreground="White"> fixes Visual Studio build problems in long OneDrive and support-file paths by using a short intermediate build folder.</Run><LineBreak/> <Bold><Run Foreground="DodgerBlue">Main Commands</Run></Bold><LineBreak/> <Run Foreground="White">🧰 </Run><Bold><Run Foreground="Orange">LLP</Run></Bold><Run Foreground="White"> and </Run><Bold><Run Foreground="Orange">SHOWPANEL</Run></Bold><Run Foreground="White"> open the modeless Layer Leader panel.</Run><LineBreak/> <Run Foreground="White">📍 </Run><Bold><Run Foreground="Orange">PLACELEADER</Run></Bold><Run Foreground="White"> starts the entity pick, arrowhead point, and text point workflow for placing DB-linked MLeader labels.</Run><LineBreak/> <Run Foreground="White">📚 </Run><Bold><Run Foreground="Orange">ADDLAYER</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">REMOVELAYER</Run></Bold><Run Foreground="White">, and </Run><Bold><Run Foreground="Orange">LISTLAYERS</Run></Bold><Run Foreground="White"> manage layer-to-label database entries.</Run><LineBreak/> <Run Foreground="White">🧱 </Run><Bold><Run Foreground="Orange">ADDBLOCK</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">REMOVEBLOCK</Run></Bold><Run Foreground="White">, and </Run><Bold><Run Foreground="Orange">LISTBLOCKS</Run></Bold><Run Foreground="White"> manage dynamic block visibility-state label rules.</Run><LineBreak/> <Run Foreground="White">🔄 </Run><Bold><Run Foreground="Orange">LLUPDATEFIELDS</Run></Bold><Run Foreground="White"> refreshes linked MLeader label fields in the active drawing.</Run><LineBreak/> <Run Foreground="White">🛠️ </Run><Bold><Run Foreground="Orange">SETUPDIMSTYLES</Run></Bold><Run Foreground="White"> and </Run><Bold><Run Foreground="Orange">LOADSETUPDIMSTYLES</Run></Bold><Run Foreground="White"> run or reload the integrated AutoLISP style setup helper.</Run><LineBreak/> <Bold><Run Foreground="LimeGreen">Functionalities</Run></Bold><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">DB MLeader style automation</Run></Bold><Run Foreground="White"> - ensures the DB multileader style exists and sets it current when the plugin loads.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Layer label database</Run></Bold><Run Foreground="White"> - maps Civil 3D layer names to clean display labels, color values, linetypes, and descriptions.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Dynamic block label database</Run></Bold><Run Foreground="White"> - resolves labels from block names and visibility-state patterns with wildcard support.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Place leader workflow</Run></Bold><Run Foreground="White"> - picks the source entity, arrowhead point, and text point, then creates a production-ready MLeader label.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Place on block workflow</Run></Bold><Run Foreground="White"> - reads dynamic block visibility states and places the correct annotation label.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Linked label refresh</Run></Bold><Run Foreground="White"> - updates DB-linked MLeader fields after database or drawing changes.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Setup Styles workspace</Run></Bold><Run Foreground="White"> - supports Text, Dimensions, and MLeader style workflows, including dimension override cleanup.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Background mask tools</Run></Bold><Run Foreground="White"> - integrates masking controls for readable labels and clean plotted drawings.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Arrowhead symbol support</Run></Bold><Run Foreground="White"> - exposes standard AutoCAD MLeader arrowhead styles for controlled annotation output.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">UCS-aware placement</Run></Bold><Run Foreground="White"> - transforms picked positions correctly from the current UCS to WCS while preserving horizontal label convention.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Log and diagnostics</Run></Bold><Run Foreground="White"> - records plugin actions with timestamps, supports saving, copying, clearing, and auto-persisting logs.</Run><LineBreak/> <Run Foreground="White">✅ </Run><Bold><Run Foreground="LimeGreen">Long-path build fix</Run></Bold><Run Foreground="White"> - v1.0.027 uses a short </Run><Bold><Run Foreground="Orange">o\</Run></Bold><Run Foreground="White"> intermediate folder to avoid path-length build failures.</Run><LineBreak/> <Bold><Run Foreground="DodgerBlue">Technical Notes</Run></Bold><LineBreak/> <Run Foreground="White">🧠 Built for </Run><Bold><Run Foreground="Orange">.NET Framework 4.8</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">x64</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">Windows Forms</Run></Bold><Run Foreground="White">, AutoCAD ObjectARX references, Civil 3D references, and </Run><Bold><Run Foreground="Orange">Newtonsoft.Json 13.0.3</Run></Bold><Run Foreground="White">.</Run><LineBreak/> <Run Foreground="White">📦 Runtime files must stay beside the DLL: </Run><Bold><Run Foreground="Orange">LayerLeaderPlugin.dll</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">Newtonsoft.Json.dll</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">layer_database.json</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">block_database.json</Run></Bold><Run Foreground="White">, </Run><Bold><Run Foreground="Orange">BuildDate.txt</Run></Bold><Run Foreground="White">, and the bundled </Run><Bold><Run Foreground="Orange">Lisp</Run></Bold><Run Foreground="White"> folder.</Run><LineBreak/> <Run Foreground="White">⚠️ AutoCAD and Civil 3D reference DLLs must be loaded from the local Autodesk installation and should not be copied into the plugin output folder.</Run><LineBreak/> <Bold><Run Foreground="DodgerBlue">Practical Result</Run></Bold><LineBreak/> <Run Foreground="White">🎯 The final result is a faster, more consistent AutoCAD and Civil 3D annotation workflow for DB-style multileaders, layer labels, dynamic block labels, field refresh, style setup, background masks, and CAD production standards.</Run><LineBreak/> <Bold><Run Foreground="Yellow">Tags</Run></Bold><LineBreak/> <Run Foreground="Yellow">🏷️ AutoCAD, 🏷️ Civil 3D, 🏷️ AutoCAD .NET, 🏷️ CSharp, 🏷️ WinForms, 🏷️ MLeader, 🏷️ Layer Management, 🏷️ Dynamic Blocks, 🏷️ CAD Standards, 🏷️ ObjectARX, 🏷️ AutoLISP Integration</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: o\net48\LayerLeaderPlugin.AssemblyInfo.cs =====
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("LayerLeaderPlugin")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("LayerLeaderPlugin")]
[assembly: System.Reflection.AssemblyTitleAttribute("LayerLeaderPlugin")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.
// ===== FILE: o\net48\.NETFramework,Version=v4.8.AssemblyAttributes.cs =====
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
// ===== FILE: Core\LabelFieldUpdater.cs =====
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Forms;
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// DB-driven label "field" support for placed MLeaders.
///
/// AutoCAD's native Field engine can display a selected object's raw
/// Layer property, but it cannot run this plugin's JSON lookup rules
/// (Layer tab / Blocks tab) to convert that layer or visibility state
/// into the drawing-specific label. This class therefore stores a
/// persistent source-object handle on each placed MLeader and refreshes
/// the visible MText from the JSON rules whenever the source entity or
/// the rule database changes.
///
/// Result: the MLeader text is no longer dead/static text; it is a
/// source-object-backed label field controlled by the Layer/Blocks DB.
/// </summary>
public static class LabelFieldUpdater
{
private const string SRC = "LabelFieldUpdater";
private const string RegAppName = "LayerLeaderPlugin";
private const string Marker = "LL_DYNAMIC_LABEL_V1";
public const string SourceLayer = "Layer";
public const string SourceBlock = "Block";
private static readonly object Sync = new object();
private static readonly HashSet<Database> AttachedDatabases = new HashSet<Database>();
private static readonly HashSet<Database> DirtyDatabases = new HashSet<Database>();
private static Timer _timer;
private static bool _initialized;
private static bool _refreshing;
public static void Initialize()
{
if (_initialized) return;
_initialized = true;
try
{
foreach (Document doc in AcadApp.DocumentManager)
AttachDocument(doc);
AcadApp.DocumentManager.DocumentCreated += (s, e) => AttachDocument(e.Document);
AcadApp.DocumentManager.DocumentActivated += (s, e) => AttachDocument(e.Document);
_timer = new Timer { Interval = 750 };
_timer.Tick += (s, e) => FlushDirtyDatabases();
_timer.Start();
LogService.Info(SRC, "Dynamic label-field updater initialized.");
}
catch (Exception ex)
{
LogService.Error(SRC, $"Initialize failed: {ex.Message}");
}
}
private static void AttachDocument(Document doc)
{
if (doc == null || doc.Database == null) return;
AttachDatabase(doc.Database);
}
private static void AttachDatabase(Database db)
{
if (db == null) return;
lock (Sync)
{
if (AttachedDatabases.Contains(db)) return;
AttachedDatabases.Add(db);
}
db.ObjectModified += Database_ObjectModified;
db.ObjectErased += Database_ObjectErased;
LogService.Debug(SRC, "Attached database object-change events.");
}
private static void Database_ObjectModified(object sender, ObjectEventArgs e)
{
if (_refreshing || e?.DBObject == null) return;
// Any non-MLeader entity can be a source object. MLeaders are
// ignored here to avoid a feedback loop when we rewrite the
// label text during refresh. Layer table changes are also
// relevant because a layer rename changes what a linked source
// entity resolves to through the Layer tab rules.
if ((e.DBObject is Entity && !(e.DBObject is MLeader)) ||
e.DBObject is LayerTableRecord)
MarkDirty(e.DBObject.Database);
}
private static void Database_ObjectErased(object sender, ObjectErasedEventArgs e)
{
if (_refreshing || e?.DBObject == null) return;
if (e.Erased && e.DBObject is Entity && !(e.DBObject is MLeader))
MarkDirty(e.DBObject.Database);
}
private static void MarkDirty(Database db)
{
if (db == null) return;
lock (Sync) DirtyDatabases.Add(db);
}
private static void FlushDirtyDatabases()
{
List<Database> dirty;
lock (Sync) dirty = DirtyDatabases.ToList();
if (dirty.Count == 0) return;
foreach (Document doc in AcadApp.DocumentManager)
{
if (doc?.Database == null || !dirty.Contains(doc.Database)) continue;
try
{
int changed = RefreshDocument(doc, quiet: true);
lock (Sync) DirtyDatabases.Remove(doc.Database);
if (changed > 0)
LogService.Info(SRC, $"Auto-refreshed {changed} linked label(s).");
}
catch (Autodesk.AutoCAD.Runtime.Exception ex)
{
// Most commonly eLockViolation while a command is still
// actively changing geometry. Leave the DB dirty; the next
// timer tick will try again.
LogService.Debug(SRC, $"Deferred refresh: {ex.Message}");
}
catch (Exception ex)
{
lock (Sync) DirtyDatabases.Remove(doc.Database);
LogService.Error(SRC, $"Auto-refresh failed: {ex.Message}");
}
}
}
/// <summary>
/// Stores the source-object link on a newly placed MLeader.
/// </summary>
public static void AttachLink(MLeader ml, Transaction tr, ObjectId sourceObjectId, string sourceKind)
{
if (ml == null || tr == null || sourceObjectId.IsNull || string.IsNullOrWhiteSpace(sourceKind))
return;
try
{
var src = tr.GetObject(sourceObjectId, OpenMode.ForRead, false) as DBObject;
if (src == null) return;
EnsureRegApp(ml.Database, tr);
if (!ml.IsWriteEnabled) ml.UpgradeOpen();
ml.XData = new ResultBuffer(
new TypedValue((int)DxfCode.ExtendedDataRegAppName, RegAppName),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, Marker),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, sourceKind),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, src.Handle.ToString())
);
LogService.Info(SRC,
$"Attached dynamic label field: kind={sourceKind}, sourceHandle={src.Handle}");
}
catch (Exception ex)
{
LogService.Error(SRC, $"AttachLink failed: {ex.Message}");
}
}
/// <summary>
/// Refreshes linked labels in the active drawing. Safe to call after
/// a rule-table edit, because it re-evaluates every linked MLeader.
/// </summary>
public static int RefreshActiveDocument(bool quiet = true)
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
return doc == null ? 0 : RefreshDocument(doc, quiet);
}
public static int RefreshDocument(Document doc, bool quiet = true)
{
if (doc == null || doc.Database == null) return 0;
AttachDocument(doc);
int changed;
_refreshing = true;
try
{
using (doc.LockDocument())
using (var tr = doc.Database.TransactionManager.StartTransaction())
{
changed = RefreshLinkedMLeaders(doc.Database, tr);
tr.Commit();
}
}
finally
{
_refreshing = false;
}
if (!quiet)
doc.Editor.WriteMessage($"\n[INFO] Refreshed {changed} linked label field(s).");
return changed;
}
private static int RefreshLinkedMLeaders(Database db, Transaction tr)
{
int changed = 0;
var bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
if (bt == null) return 0;
foreach (ObjectId btrId in bt)
{
var btr = tr.GetObject(btrId, OpenMode.ForRead) as BlockTableRecord;
if (btr == null || !btr.IsLayout) continue;
foreach (ObjectId id in btr)
{
var ml = tr.GetObject(id, OpenMode.ForRead, false) as MLeader;
if (ml == null) continue;
if (!TryReadLink(ml, out string sourceKind, out string sourceHandle))
continue;
string label = ResolveLabel(db, tr, sourceKind, sourceHandle);
if (string.IsNullOrWhiteSpace(label)) continue;
MText mt = ml.MText;
if (mt == null) continue;
if (string.Equals(mt.Contents ?? "", label, StringComparison.Ordinal))
continue;
if (!ml.IsWriteEnabled) ml.UpgradeOpen();
mt = ml.MText;
mt.Contents = label;
BackgroundMaskHelper.ApplyToMText(mt);
ml.MText = mt;
changed++;
}
}
return changed;
}
private static bool TryReadLink(DBObject obj, out string sourceKind, out string sourceHandle)
{
sourceKind = null;
sourceHandle = null;
using (var rb = obj.GetXDataForApplication(RegAppName))
{
if (rb == null) return false;
var strings = rb.AsArray()
.Where(v => v.TypeCode == (int)DxfCode.ExtendedDataAsciiString)
.Select(v => v.Value?.ToString())
.Where(v => !string.IsNullOrWhiteSpace(v))
.ToArray();
if (strings.Length < 3 || strings[0] != Marker) return false;
sourceKind = strings[1];
sourceHandle = strings[2];
return true;
}
}
private static string ResolveLabel(Database db, Transaction tr, string sourceKind, string sourceHandle)
{
ObjectId sourceId = ResolveHandle(db, sourceHandle);
if (sourceId.IsNull) return null;
var ent = tr.GetObject(sourceId, OpenMode.ForRead, false) as Entity;
if (ent == null) return null;
if (string.Equals(sourceKind, SourceBlock, StringComparison.OrdinalIgnoreCase))
{
var br = ent as BlockReference;
if (br == null) return null;
string blockName = BlockInspector.GetEffectiveName(br, tr);
string visState = BlockInspector.GetVisibilityState(br) ?? "(none)";
var entry = BlockDatabase.GetEntry(blockName, visState);
return entry?.DisplayLabel ?? visState;
}
// Default/fallback: resolve through the Layer DB.
string layerName = ent.Layer ?? "";
var layerEntry = LayerDatabase.GetEntry(layerName);
return layerEntry?.DisplayLabel ?? layerName;
}
private static ObjectId ResolveHandle(Database db, string handleString)
{
if (db == null || string.IsNullOrWhiteSpace(handleString)) return ObjectId.Null;
try
{
long value = Convert.ToInt64(handleString, 16);
return db.GetObjectId(false, new Handle(value), 0);
}
catch
{
return ObjectId.Null;
}
}
private static void EnsureRegApp(Database db, Transaction tr)
{
var rat = tr.GetObject(db.RegAppTableId, OpenMode.ForRead) as RegAppTable;
if (rat == null || rat.Has(RegAppName)) return;
rat.UpgradeOpen();
var rec = new RegAppTableRecord { Name = RegAppName };
rat.Add(rec);
tr.AddNewlyCreatedDBObject(rec, true);
}
}
}
// ===== FILE: Core\MLeaderStyleHelper.cs =====
using Autodesk.AutoCAD.Colors;
using Autodesk.AutoCAD.DatabaseServices;
using LayerLeaderPlugin.Models;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Creates / ensures the "DB" MLeader style and provides two-way
/// conversion between the live MLeaderStyle in the drawing and the
/// editable LeaderStyleConfig model used by the Leader tab UI.
///
/// IMPORTANT — enum coverage:
/// Only enum members already proven to exist in this Civil 3D
/// ObjectARX binding are used (StraightLeader/SplineLeader,
/// MTextContent/BlockContent, HorizontalAngle, and
/// AttachmentBottomOfTopLine). Values such as LeaderType.NoLeader,
/// ContentType.NoContent, TextAngleType.AsInsertedAngle, and the
/// Middle*/BottomOfBottomLine attachment variants are NOT available
/// in this binding and must never be referenced.
/// </summary>
public static class MLeaderStyleHelper
{
public const string StyleName = "DB";
public const string LayerName = "KTB_Text";
private const string SRC = "MLeaderStyleHelper";
public const short TextLayerColorIndex = 11;
public const string TextLayerLinetype = "Continuous";
// Back-compat constants used by the MLeader preview caption
public const double ArrowSize = 0.18;
public const double DoglegLength = 0.36;
public const double LandingGap = 0.09;
public const double TextHeight = 1.0;
public const string TextStyle = "K_ISOCP";
// ── Ensure style exists + make current ──────────────────────────
public static ObjectId EnsureStyle(Database db, Transaction tr)
{
LogService.Debug(SRC, "EnsureStyle(Database db, Transaction tr)");
EnsureLayer(db, tr);
ObjectId styleId = EnsureMLeaderStyle(db, tr, DefaultConfig());
SetCurrentMLeaderStyle(db, styleId);
return styleId;
}
/// <summary>Default config matching the reference screenshots.</summary>
public static LeaderStyleConfig DefaultConfig() => new LeaderStyleConfig();
// ── KTB_Text layer ────────────────────────────────────────────
private static void EnsureLayer(Database db, Transaction tr)
{
var lt = tr.GetObject(db.LayerTableId, OpenMode.ForRead) as LayerTable;
LayerTableRecord ltr;
bool created = false;
if (lt.Has(LayerName))
{
ltr = tr.GetObject(lt[LayerName], OpenMode.ForWrite) as LayerTableRecord;
}
else
{
lt.UpgradeOpen();
ltr = new LayerTableRecord { Name = LayerName };
lt.Add(ltr);
tr.AddNewlyCreatedDBObject(ltr, true);
created = true;
}
ltr.Color = Color.FromColorIndex(ColorMethod.ByAci, TextLayerColorIndex);
ObjectId continuousId = GetLinetypeId(db, tr, TextLayerLinetype);
if (!continuousId.IsNull) ltr.LinetypeObjectId = continuousId;
if (created) ltr.LineWeight = LineWeight.LineWeight025;
LogService.Info(SRC,
$"{(created ? "Created" : "Updated")} layer \"{LayerName}\" " +
$"Color={TextLayerColorIndex}, Linetype={TextLayerLinetype}.");
}
private static ObjectId GetLinetypeId(Database db, Transaction tr, string name)
{
var ltt = tr.GetObject(db.LinetypeTableId, OpenMode.ForRead) as LinetypeTable;
if (ltt.Has(name)) return ltt[name];
LogService.Warn(SRC, $"Linetype \"{name}\" not found — leaving current layer linetype.");
return ObjectId.Null;
}
// ── Read current "DB" style into an editable config ─────────────
// Used by the Leader tab to populate the 3 sub-tabs on open.
public static LeaderStyleConfig ReadConfig(Database db, Transaction tr)
{
var dict = tr.GetObject(db.MLeaderStyleDictionaryId, OpenMode.ForRead) as DBDictionary;
if (!dict.Contains(StyleName))
return DefaultConfig();
var style = tr.GetObject(dict.GetAt(StyleName), OpenMode.ForRead) as MLeaderStyle;
var cfg = new LeaderStyleConfig
{
// Only Straight/Spline are distinguished; anything else
// (no proven "None" member) falls back to Straight.
LeaderType = style.LeaderLineType == LeaderType.SplineLeader ? "Spline" : "Straight",
LineColor = ColorToString(style.LeaderLineColor),
Linetype = GetLinetypeName(db, tr, style.LeaderLineTypeId),
LineWeight = style.LeaderLineWeight == LineWeight.ByBlock ? "ByBlock"
: style.LeaderLineWeight == LineWeight.ByLayer ? "ByLayer"
: style.LeaderLineWeight.ToString(),
ArrowSymbol = ResolveArrowSymbolDisplayName(db, tr, style.ArrowSymbolId),
ArrowSize = style.ArrowSize,
LeaderBreakSize = 0.125, // not exposed by ObjectARX MLeaderStyle — UI-only default
MaxLeaderPointsEnabled = style.MaxLeaderSegmentsPoints > 0,
MaxLeaderPoints = style.MaxLeaderSegmentsPoints > 0 ? style.MaxLeaderSegmentsPoints : 2,
AutoIncludeLanding = style.EnableLanding,
SetLandingDistance = style.EnableDogleg,
LandingDistance = style.DoglegLength,
Annotative = style.Annotative == AnnotativeStates.True,
Scale = style.Scale,
// Only Mtext/Block are distinguished; no proven "None" member.
MultileaderType = style.ContentType == ContentType.BlockContent ? "Block" : "Mtext",
TextStyle = GetTextStyleName(db, tr, style.TextStyleId),
// Only HorizontalAngle is a proven member; everything else
// reads back as "Keep horizontal".
TextAngle = "Keep horizontal",
TextColor = ColorToString(style.TextColor),
TextHeight = style.TextHeight,
LandingGap = style.LandingGap,
ExtendLeaderToText = style.ExtendLeaderToText,
HorizontalAttachment = true,
// Only AttachmentBottomOfTopLine is a proven member, so both
// sides always read/write that single value.
LeftAttachment = "Underline top line",
RightAttachment = "Underline top line",
};
return cfg;
}
// ── Write an edited config back to the "DB" style ────────────────
// Used by the Leader tab's Apply/OK to persist changes into the drawing.
public static ObjectId WriteConfig(Database db, Transaction tr, LeaderStyleConfig cfg)
{
EnsureLayer(db, tr);
ObjectId styleId = EnsureMLeaderStyle(db, tr, cfg, forceUpdate: true);
SetCurrentMLeaderStyle(db, styleId);
return styleId;
}
// ── MLeader style "DB" ────────────────────────────────────────
private static ObjectId EnsureMLeaderStyle(Database db, Transaction tr,
LeaderStyleConfig cfg, bool forceUpdate = false)
{
var dict = tr.GetObject(db.MLeaderStyleDictionaryId, OpenMode.ForRead) as DBDictionary;
MLeaderStyle style;
ObjectId id = ObjectId.Null;
bool created = false;
if (dict.Contains(StyleName))
{
id = dict.GetAt(StyleName);
if (!forceUpdate)
{
LogService.Debug(SRC, $"Style \"{StyleName}\" already exists.");
return id;
}
style = tr.GetObject(id, OpenMode.ForWrite) as MLeaderStyle;
}
else
{
dict.UpgradeOpen();
style = new MLeaderStyle();
created = true;
}
ApplyConfigToStyle(style, cfg, db, tr);
if (created)
{
id = dict.SetAt(StyleName, style);
tr.AddNewlyCreatedDBObject(style, true);
LogService.Info(SRC, $"Created MLeader style \"{StyleName}\".");
}
else
{
LogService.Info(SRC, $"Updated MLeader style \"{StyleName}\".");
}
return id;
}
private static void ApplyConfigToStyle(MLeaderStyle style, LeaderStyleConfig cfg,
Database db, Transaction tr)
{
// ── Leader Format ────────────────────────────────────────
style.LeaderLineType = cfg.LeaderType == "Spline"
? LeaderType.SplineLeader
: LeaderType.StraightLeader;
style.LeaderLineColor = StringToColor(cfg.LineColor);
style.LeaderLineWeight = StringToLineWeight(cfg.LineWeight);
// FIX: ArrowSymbol now actually resolves to a real AutoCAD
// standard arrowhead block (Closed filled, Dot, Open, etc.)
// via ArrowheadHelper instead of always being forced to
// ObjectId.Null regardless of what was selected.
style.ArrowSymbolId = ArrowheadHelper.ResolveByDisplayName(db, tr, cfg.ArrowSymbol);
style.ArrowSize = cfg.ArrowSize;
// ── Leader Structure ─────────────────────────────────────
style.MaxLeaderSegmentsPoints = cfg.MaxLeaderPointsEnabled ? cfg.MaxLeaderPoints : 0;
style.EnableDogleg = cfg.SetLandingDistance;
style.DoglegLength = cfg.LandingDistance;
style.EnableLanding = cfg.AutoIncludeLanding;
style.Scale = cfg.Scale;
style.Annotative = cfg.Annotative ? AnnotativeStates.True : AnnotativeStates.False;
// ── Content ──────────────────────────────────────────────
style.ContentType = cfg.MultileaderType == "Block"
? ContentType.BlockContent
: ContentType.MTextContent;
style.TextHeight = cfg.TextHeight;
style.TextColor = StringToColor(cfg.TextColor);
// Only HorizontalAngle is a proven member.
style.TextAngleType = TextAngleType.HorizontalAngle;
// Only AttachmentBottomOfTopLine is a proven member — applied
// to both directions, matching the reference screenshot.
style.TextAttachmentType = TextAttachmentType.AttachmentBottomOfTopLine;
style.SetTextAttachmentType(TextAttachmentType.AttachmentBottomOfTopLine,
LeaderDirectionType.LeftLeader);
style.SetTextAttachmentType(TextAttachmentType.AttachmentBottomOfTopLine,
LeaderDirectionType.RightLeader);
style.LandingGap = cfg.LandingGap;
style.ExtendLeaderToText = cfg.ExtendLeaderToText;
style.TextAlignmentType = TextAlignmentType.RightAlignment;
style.TextStyleId = GetTextStyleId(db, tr, cfg.TextStyle);
}
private static void SetCurrentMLeaderStyle(Database db, ObjectId styleId)
{
if (styleId.IsNull) return;
if (!db.MLeaderstyle.Equals(styleId))
{
db.MLeaderstyle = styleId;
LogService.Info(SRC, $"Current MLeader style set to \"{StyleName}\".");
}
}
private static ObjectId GetTextStyleId(Database db, Transaction tr, string name)
{
var tst = tr.GetObject(db.TextStyleTableId, OpenMode.ForRead) as TextStyleTable;
if (tst.Has(name)) return tst[name];
LogService.Warn(SRC, $"Text style \"{name}\" not found — using Standard.");
return db.Textstyle;
}
private static string GetTextStyleName(Database db, Transaction tr, ObjectId id)
{
if (id.IsNull) return TextStyle;
var tsr = tr.GetObject(id, OpenMode.ForRead) as TextStyleTableRecord;
return tsr?.Name ?? TextStyle;
}
/// <summary>
/// Reverse-resolves an ArrowSymbolId back to its display name
/// (e.g. "Closed filled") by matching the block's name against
/// ArrowheadHelper.StandardArrowheads. Falls back to "None" if
/// the id is null, or "Custom" if it's a real block that isn't
/// one of the recognised standard arrowheads (e.g. a user's own
/// custom arrow block created outside this plugin).
/// </summary>
private static string ResolveArrowSymbolDisplayName(Database db, Transaction tr, ObjectId arrowId)
{
if (arrowId.IsNull) return "None";
var btr = tr.GetObject(arrowId, OpenMode.ForRead) as BlockTableRecord;
string blockName = btr?.Name;
if (string.IsNullOrEmpty(blockName)) return "Custom";
foreach (var (display, name) in ArrowheadHelper.StandardArrowheads)
{
if (string.Equals(name, blockName, System.StringComparison.OrdinalIgnoreCase))
return display;
}
return "Custom";
}
private static string GetLinetypeName(Database db, Transaction tr, ObjectId id)
{
if (id.IsNull) return "ByBlock";
var ltr = tr.GetObject(id, OpenMode.ForRead) as LinetypeTableRecord;
return ltr?.Name ?? "ByBlock";
}
// ── Small string<->enum converters for the editable config ──────
private static string ColorToString(Color c)
{
if (c.ColorMethod == ColorMethod.ByBlock) return "ByBlock";
if (c.ColorMethod == ColorMethod.ByLayer) return "ByLayer";
return c.ColorIndex.ToString();
}
private static Color StringToColor(string s)
{
if (string.Equals(s, "ByBlock", System.StringComparison.OrdinalIgnoreCase))
return Color.FromColorIndex(ColorMethod.ByBlock, 0);
if (string.Equals(s, "ByLayer", System.StringComparison.OrdinalIgnoreCase))
return Color.FromColorIndex(ColorMethod.ByLayer, 0);
if (short.TryParse(s, out short idx))
return Color.FromColorIndex(ColorMethod.ByAci, idx);
return Color.FromColorIndex(ColorMethod.ByBlock, 0);
}
private static LineWeight StringToLineWeight(string s)
{
if (string.Equals(s, "ByLayer", System.StringComparison.OrdinalIgnoreCase))
return LineWeight.ByLayer;
return LineWeight.ByBlock;
}
/// <summary>
/// Applies "DB" style overrides to an MLeader instance.
/// Call AFTER AppendEntity + AddNewlyCreatedDBObject.
/// </summary>
public static void ApplyStyle(MLeader ml, ObjectId styleId, Database db, Transaction tr)
{
var cfg = ReadConfig(db, tr);
ml.MLeaderStyle = styleId;
ml.ArrowSize = cfg.ArrowSize;
ml.ArrowSymbolId = ArrowheadHelper.ResolveByDisplayName(db, tr, cfg.ArrowSymbol);
ml.DoglegLength = cfg.LandingDistance;
ml.EnableDogleg = cfg.SetLandingDistance;
ml.EnableLanding = cfg.AutoIncludeLanding;
ml.LandingGap = cfg.LandingGap;
ml.LeaderLineType = cfg.LeaderType == "Spline"
? LeaderType.SplineLeader
: LeaderType.StraightLeader;
ml.LeaderLineColor = StringToColor(cfg.LineColor);
ml.LeaderLineWeight = StringToLineWeight(cfg.LineWeight);
ml.ContentType = cfg.MultileaderType == "Block"
? ContentType.BlockContent
: ContentType.MTextContent;
ml.TextHeight = cfg.TextHeight;
ml.TextColor = StringToColor(cfg.TextColor);
ml.TextAngleType = TextAngleType.HorizontalAngle;
ml.TextAttachmentType = TextAttachmentType.AttachmentBottomOfTopLine;
ml.Scale = cfg.Scale;
ml.EnableAnnotationScale = cfg.Annotative;
ml.ExtendLeaderToText = cfg.ExtendLeaderToText;
ml.TextAlignmentType = TextAlignmentType.RightAlignment;
ml.TextStyleId = GetTextStyleId(db, tr, cfg.TextStyle);
var lt = tr.GetObject(db.LayerTableId, OpenMode.ForRead) as LayerTable;
if (lt.Has(LayerName)) ml.Layer = LayerName;
}
}
}
// ===== FILE: Core\C3DTheme.cs =====
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// WinForms translation of the uploaded C3D_UI_Style_Template.py palette.
/// Keeps the Layer Leader panel visually aligned with the C3D dark UI language.
/// </summary>
internal static class C3DTheme
{
public static readonly Color Bg = Hex("#2b2d3e");
public static readonly Color Panel = Hex("#23253a");
public static readonly Color Border = Hex("#3e4060");
public static readonly Color HeaderBg = Hex("#1e2030");
public static readonly Color Bg2 = Hex("#23253a");
public static readonly Color Bg3 = Hex("#1e2030");
public static readonly Color Fg = Hex("#dde1f0");
public static readonly Color Muted = Hex("#7a7f9a");
public static readonly Color Accent = Hex("#2979d4");
public static readonly Color TextW = Hex("#ffffff");
public static readonly Color SelBg = Hex("#3a5fa0");
public static readonly Color SelFg = Hex("#ffffff");
public static readonly Color CodeBg = Hex("#1e2030");
public static readonly Color CodeFg = Hex("#dde1f0");
public static readonly Color Entry = Hex("#1e2030");
public static readonly Color Blue = Hex("#2979d4");
public static readonly Color Green = Hex("#2a9e52");
public static readonly Color Red = Hex("#c0392b");
public static readonly Color Orange = Hex("#e06c2a");
public static readonly Color Purple = Hex("#7c4dbd");
public static readonly Color Grey = Hex("#3e4060");
public static readonly Color Reload = Hex("#4a5070");
public static readonly Color Success = Hex("#27ae60");
public static readonly Color Warn = Hex("#e06c2a");
public static readonly Color Info = Hex("#2979d4");
public static readonly Font Normal = new Font("Segoe UI", 9f);
public static readonly Font Small = new Font("Segoe UI", 8f);
public static readonly Font Title = new Font("Segoe UI", 10f, FontStyle.Bold);
public static readonly Font Button = new Font("Segoe UI", 9f);
public static readonly Font ButtonBold = new Font("Segoe UI", 9f, FontStyle.Bold);
public static readonly Font Code = new Font("Consolas", 10f);
public static readonly Font CodeSmall = new Font("Consolas", 8.5f);
public static readonly Font CodeBig = new Font("Consolas", 11f);
public static Color Hex(string value)
{
return ColorTranslator.FromHtml(value);
}
public static Color Lighten(Color color, int amount = 22)
{
return Color.FromArgb(
Math.Min(255, color.R + amount),
Math.Min(255, color.G + amount),
Math.Min(255, color.B + amount));
}
public static Button MakeButton(string text, string roleName = "neutral", int width = 90)
{
var button = new Button
{
Text = text,
Width = width,
Height = 27,
Margin = new Padding(3, 3, 3, 3),
FlatStyle = FlatStyle.Flat,
Font = Button,
Cursor = Cursors.Hand,
UseVisualStyleBackColor = false
};
StyleButton(button, roleName);
return button;
}
public static void StyleButton(Button button, string roleName = "neutral")
{
roleName = (roleName ?? "neutral").ToLowerInvariant();
Color bg;
Color fg = TextW;
Color active;
Font font = Button;
if (roleName == "primary" || roleName == "ok" || roleName == "success")
{
bg = Green;
active = Lighten(Green);
font = ButtonBold;
}
else if (roleName == "refresh" || roleName == "info" || roleName == "blue" || roleName == "apply")
{
bg = Blue;
active = Lighten(Blue);
}
else if (roleName == "danger" || roleName == "delete" || roleName == "revert")
{
bg = Panel;
fg = Red;
active = Border;
}
else if (roleName == "warn" || roleName == "warning" || roleName == "undo")
{
bg = Warn;
active = Lighten(Warn);
}
else
{
bg = Panel;
fg = Fg;
active = Border;
}
button.BackColor = bg;
button.ForeColor = fg;
button.Font = font;
button.FlatAppearance.BorderColor = Border;
button.FlatAppearance.MouseOverBackColor = active;
button.FlatAppearance.MouseDownBackColor = Lighten(active, 10);
}
public static Label MakeLabel(string text, ContentAlignment align, bool muted = true)
{
return new Label
{
Text = text,
Dock = DockStyle.Fill,
ForeColor = muted ? Muted : Fg,
BackColor = Panel,
Font = muted ? Small : Normal,
TextAlign = align
};
}
public static TextBox MakeTextBox()
{
return new TextBox
{
Dock = DockStyle.Fill,
BackColor = Entry,
ForeColor = Fg,
BorderStyle = BorderStyle.FixedSingle,
Font = CodeSmall,
Margin = new Padding(3, 3, 0, 3)
};
}
public static void StyleGrid(DataGridView grid)
{
grid.EnableHeadersVisualStyles = false;
grid.BackgroundColor = CodeBg;
grid.GridColor = Border;
grid.BorderStyle = BorderStyle.None;
grid.ColumnHeadersDefaultCellStyle.BackColor = HeaderBg;
grid.ColumnHeadersDefaultCellStyle.ForeColor = Fg;
grid.ColumnHeadersDefaultCellStyle.SelectionBackColor = HeaderBg;
grid.ColumnHeadersDefaultCellStyle.SelectionForeColor = Fg;
grid.ColumnHeadersDefaultCellStyle.Font = CodeSmall;
grid.DefaultCellStyle.BackColor = CodeBg;
grid.DefaultCellStyle.ForeColor = Fg;
grid.DefaultCellStyle.SelectionBackColor = SelBg;
grid.DefaultCellStyle.SelectionForeColor = SelFg;
grid.DefaultCellStyle.Font = CodeSmall;
grid.AlternatingRowsDefaultCellStyle.BackColor = Bg3;
grid.RowTemplate.Height = 24;
}
public static void StyleTabPage(TabPage page)
{
page.BackColor = Panel;
page.ForeColor = Fg;
}
public static void StyleTextBox(TextBox textBox)
{
textBox.BackColor = Entry;
textBox.ForeColor = Fg;
textBox.BorderStyle = BorderStyle.FixedSingle;
textBox.Font = CodeSmall;
}
}
}
// ===== FILE: Core\LispIntegration.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Integrates bundled AutoLISP routines with the .NET plugin.
/// Each LSP is copied beside the DLL at build time and loaded into
/// the active AutoCAD document on first use so its command(s) become
/// available without a manual APPLOAD.
/// </summary>
public static class LispIntegration
{
private const string SRC = "LispIntegration";
public const string DimStylesLispFile = "SetupOverriteDimStyles.lsp";
public const string DimStylesCommand = "SetupOverriteDimStyles";
public const string MaskLispFile = "Background_Mask.lsp";
public const string MaskCommand = "bmask";
// Back-compat aliases (older code referenced these names directly).
public const string LispFileName = DimStylesLispFile;
public const string CommandName = DimStylesCommand;
// Tracks which LSP files have already been (load)-ed in the
// current document session, keyed by file name, so repeated
// commands don't re-queue a (load ...) every single click.
private static readonly HashSet<string> _loadedThisSession = new HashSet<string>();
private static string LispDir =>
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Lisp");
public static string LispPath => Path.Combine(LispDir, DimStylesLispFile);
public static string MaskLispPath => Path.Combine(LispDir, MaskLispFile);
// ─────────────────────────────────────────────────────────────
// SetupOverriteDimStyles — match-properties workspace manager
// ─────────────────────────────────────────────────────────────
public static bool EnsureLoaded(Document doc = null)
{
doc ??= AcadApp.DocumentManager.MdiActiveDocument;
return EnsureLoadedInternal(doc, DimStylesLispFile);
}
/// <summary>
/// Run SetupOverriteDimStyles, loading the LSP first if needed.
/// Load + command trigger are sent as ONE combined queued string
/// so AutoCAD processes them atomically in order (avoids the
/// command running before the LSP defining it has finished
/// loading). The AutoCAD main window is focused first since the
/// entsel pick prompt needs the drawing viewport, not the panel.
/// </summary>
public static void RunSetupOverriteDimStyles()
{
RunLispCommand(DimStylesLispFile, DimStylesCommand,
"click an object in the drawing to match its properties.");
}
// ─────────────────────────────────────────────────────────────
// Background_Mask (bmask) — Lee Mac's background mask utility
// ─────────────────────────────────────────────────────────────
public static bool EnsureMaskLoaded(Document doc = null)
{
doc ??= AcadApp.DocumentManager.MdiActiveDocument;
return EnsureLoadedInternal(doc, MaskLispFile);
}
/// <summary>
/// Runs the interactive 'bmask' command (shows its own dialog,
/// same as typing bmask at the command line) so the user can
/// select MText/MLeader/Dimension entities and apply a background
/// mask, including dimensions — which the native
/// BackgroundMaskHelper intentionally does not handle since
/// dimension masking requires raw XDATA writes only this proven
/// LISP implementation does safely.
///
/// CAVEAT (from the LSP's own header comments): masking a
/// Dimension via bmask is applied as a per-dimension style
/// override UNLESS the dimension's named style already has
/// matching background-mask settings stored. In other words,
/// running bmask on dimensions can itself recreate the exact
/// "<style overrides>" ghost branch this plugin's
/// PurgeDimStyleOverrides methods exist to clean up. If you mask
/// a dimension with bmask, consider running "Delete Overrides"
/// afterward if a clean style tree matters for that drawing.
///
/// Before first launch in a session, seeds the LISP's persisted
/// default settings (stored via SETENV under "LMac\mask") to
/// match this plugin's defaults — Use Mask=on ("1"), Offset=1.00,
/// Transparent=on ("1") — instead of the LISP's own built-in
/// fallback of Offset=1.5 / mask off. bmask reads these via
/// mask:getdefaults the next time its dialog opens.
/// </summary>
public static void RunBackgroundMask()
{
if (!_maskDefaultsSeeded)
SeedBackgroundMaskDefaults(true, 1.0, true);
RunLispCommand(MaskLispFile, MaskCommand,
"select MText, MLeader, or Dimension entities to mask.");
}
/// <summary>
/// Seeds bmask's persisted SETENV defaults with explicit values —
/// used so the native BackgroundMaskDialog's confirmed settings
/// (whatever the user actually typed/toggled) flow through to the
/// LISP's own dialog the next time it opens for Dimension masking,
/// rather than always re-seeding the plugin's hardcoded defaults.
/// Re-seeds every call (no "already seeded" guard) since the
/// caller is explicitly providing fresh values to apply.
/// </summary>
public static void SeedBackgroundMaskDefaults(bool useMask, double offset, bool transparent)
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
// LISP list format: (msk off trn col) — see mask:getdefaults.
string msk = useMask ? "1" : "0";
string trn = transparent ? "1" : "0";
string off = offset.ToString(System.Globalization.CultureInfo.InvariantCulture);
doc.SendStringToExecute(
$"(setenv \"LMac\\\\mask\" \"(\\\"{msk}\\\" {off} \\\"{trn}\\\" ((62 . 1)))\") ",
true, false, false);
_maskDefaultsSeeded = true;
LogService.Info(SRC,
$"Seeded bmask defaults: mask={useMask}, offset={offset:0.00}, transparent={transparent}.");
}
// Only needs seeding once per AutoCAD session — bmask persists
// the value itself via SETENV/GETENV after that.
private static bool _maskDefaultsSeeded;
// ─────────────────────────────────────────────────────────────
// Shared internals
// ─────────────────────────────────────────────────────────────
private static bool EnsureLoadedInternal(Document doc, string fileName)
{
if (doc == null)
{
LogService.Warn(SRC, "No active document; integrated LSP was not loaded.");
return false;
}
string path = ResolveExistingPath(fileName);
if (path == null)
{
string expected = Path.Combine(LispDir, fileName);
LogService.Error(SRC, $"Integrated LSP not found: {expected}");
doc.Editor.WriteMessage($"\n[ERROR] Integrated LSP not found: {expected}");
return false;
}
doc.SendStringToExecute(BuildLoadExpr(path, fileName), true, false, false);
_loadedThisSession.Add(fileName);
LogService.Info(SRC, $"Queued LSP load: {path}");
doc.Editor.WriteMessage($"\n Integrated LSP : {path}");
return true;
}
private static void RunLispCommand(string fileName, string commandName, string hint)
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null)
{
LogService.Warn(SRC, "No active document; command was not started.");
return;
}
string path = ResolveExistingPath(fileName);
if (path == null)
{
string expected = Path.Combine(LispDir, fileName);
LogService.Error(SRC, $"Integrated LSP not found: {expected}");
doc.Editor.WriteMessage($"\n[ERROR] Integrated LSP not found: {expected}");
return;
}
// Give the AutoCAD viewport focus BEFORE queuing the command,
// so any interactive selection prompt that follows can
// actually receive the user's click instead of it landing on
// the floating modeless panel.
try { AcadApp.MainWindow.Focus(); } catch { /* best-effort */ }
bool alreadyLoaded = _loadedThisSession.Contains(fileName);
string combined = alreadyLoaded
? $"\n{commandName} "
: BuildLoadExpr(path, fileName) + $"\n{commandName} ";
doc.SendStringToExecute(combined, true, false, true);
_loadedThisSession.Add(fileName);
LogService.Info(SRC,
$"Queued {(alreadyLoaded ? "" : "load + ")}command: {commandName}");
doc.Editor.WriteMessage($"\n[INFO] {commandName} started — {hint}");
}
private static string BuildLoadExpr(string path, string fileName)
{
string lispPath = ToAutoLispString(path);
return
$"(progn " +
$"(if (findfile \"{lispPath}\") " +
$"(progn (load \"{lispPath}\") (princ \"\\n[INFO] Integrated LSP loaded: {fileName}\")) " +
$"(princ \"\\n[ERROR] Integrated LSP not found: {lispPath}\")) " +
$"(princ)) ";
}
private static string ResolveExistingPath(string fileName)
{
string preferred = Path.Combine(LispDir, fileName);
if (File.Exists(preferred)) return preferred;
// Backward-compatible fallback if a user manually places the
// LSP directly beside the DLL instead of in the Lisp subfolder.
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string fallback = Path.Combine(dir, fileName);
return File.Exists(fallback) ? fallback : null;
}
private static string ToAutoLispString(string path)
{
// AutoLISP accepts forward slashes; this avoids backslash escaping problems.
return path.Replace('\\', '/').Replace("\"", "\\\"");
}
}
}
// ===== FILE: Core\BackgroundMaskHelper.cs =====
using Autodesk.AutoCAD.DatabaseServices;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Applies Background Mask settings to MText/MLeader entities natively
/// via the documented MText.BackgroundFill / UseBackgroundColor /
/// BackgroundScaleFactor properties — the same fields the bundled
/// Background_Mask.lsp ("bmask") dialog exposes, now also editable
/// from the native BackgroundMaskDialog on each Setup Styles sub-tab.
///
/// NOTE: a custom (non-transparent) fill colour is NOT applied here.
/// MText.BackgroundColor does not exist in this Civil 3D ObjectARX
/// binding — only BackgroundFill / UseBackgroundColor /
/// BackgroundScaleFactor are proven to compile. Toggling
/// UseBackgroundColor on without a way to set the actual colour would
/// leave AutoCAD using whatever colour was last set (or none), which
/// is misleading, so this helper only ever applies the mask as
/// Transparent (UseBackgroundColor = false) regardless of the
/// FillColorAci setting. For a genuine custom fill colour, use the
/// bmask LISP path (Dimensions sub-tab routes there already; Text/
/// MLeader could be pointed at it too if this becomes a real need).
/// </summary>
public static class BackgroundMaskHelper
{
private const string SRC = "BackgroundMaskHelper";
// Defaults matching the Background_Mask.lsp dialog screenshot —
// the plugin's built-in fallback, used to seed CurrentDefaults
// below the first time this class is touched in a session.
public const double DefaultMaskOffset = 1.00;
public const bool DefaultTransparent = true;
public const bool DefaultUseMask = true;
/// <summary>Plain settings bag mirroring the dialog's three fields.</summary>
public class MaskSettings
{
public bool UseMask = DefaultUseMask;
public double MaskOffset = DefaultMaskOffset;
public bool Transparent = DefaultTransparent;
// Reserved for a future custom-fill-colour implementation —
// see the class-level NOTE above on why it isn't wired yet.
public short FillColorAci = 1;
public static MaskSettings Default => new MaskSettings();
public MaskSettings Clone() => (MaskSettings)MemberwiseClone();
}
/// <summary>
/// The plugin-wide default mask settings, applied automatically
/// to every newly-placed MLeader's MText by PlaceMLeader — no
/// per-entity selection required. Starts equal to the built-in
/// defaults above; "OK" in the native BackgroundMaskDialog
/// updates this for the rest of the AutoCAD session (does not
/// persist across NETLOAD reloads — that would need a sidecar
/// file, which is a reasonable next step if this needs to survive
/// a restart).
/// </summary>
public static MaskSettings CurrentDefaults { get; private set; } = MaskSettings.Default;
/// <summary>
/// Updates the session-wide defaults used by every future
/// PlaceMLeader call. This is what makes the Background Mask
/// dialog act as a default-setter rather than requiring the user
/// to pick an entity every time.
/// </summary>
public static void SetCurrentDefaults(MaskSettings settings)
{
CurrentDefaults = settings.Clone();
LogService.Info(SRC,
$"SetCurrentDefaults() -> mask={settings.UseMask}, offset={settings.MaskOffset:0.00}, " +
$"transparent={settings.Transparent}. Applies to all new MLeaders from now on.");
}
/// <summary>Applies the CURRENT session defaults to an MText entity.</summary>
public static void ApplyToMText(MText mt) => ApplyToMText(mt, CurrentDefaults);
/// <summary>Applies the given background mask settings to an MText entity.</summary>
public static void ApplyToMText(MText mt, MaskSettings settings)
{
mt.BackgroundFill = settings.UseMask;
// Always transparent — see class-level NOTE: MText has no
// proven BackgroundColor property in this binding to set a
// custom fill colour against, so UseBackgroundColor is never
// turned on here (that would mean "use a colour" with no way
// to actually choose one).
mt.UseBackgroundColor = false;
mt.BackgroundScaleFactor = settings.MaskOffset;
LogService.Debug(SRC,
$"ApplyToMText() -> mask={settings.UseMask}, offset={settings.MaskOffset:0.00}, " +
$"transparent=true (forced; see NOTE)");
}
/// <summary>
/// Applies the CURRENT session defaults to an MLeader's MText
/// content. Call while ml.MText is still attached.
/// </summary>
public static void ApplyToMLeader(MLeader ml) => ApplyToMLeader(ml, CurrentDefaults);
public static void ApplyToMLeader(MLeader ml, MaskSettings settings)
{
if (ml.MText != null)
ApplyToMText(ml.MText, settings);
LogService.Debug(SRC, "ApplyToMLeader() -> delegated to ApplyToMText()");
}
}
}
// ===== FILE: Core\BlockDatabase.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LayerLeaderPlugin.Models;
using Newtonsoft.Json;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Manages the JSON block→label database for dynamic block visibility states.
///
/// Collapsing example from the user's request:
/// "X_2.80_2.30_Mit_Text" and "X_2.80_2.30_Ohne_Text" → Label "X"
/// This is achieved with a single entry whose VisibilityState is the
/// wildcard pattern "X_2.80_2.30_*", matching both states.
///
/// Matching priority: exact (BlockName + VisibilityState) → wildcard.
/// </summary>
public static class BlockDatabase
{
private const string SRC = "BlockDatabase";
public static string DbPath { get; private set; } = BuildDefaultPath();
private static string BuildDefaultPath()
{
string dir = Path.GetDirectoryName(typeof(BlockDatabase).Assembly.Location);
return Path.Combine(dir, "block_database.json");
}
public static void SetPath(string path) => DbPath = path;
// ── Defaults — values shown in the Blocks tab screenshot.
// Wildcard rows catch the detailed dynamic-block visibility
// states, and exact rows catch simplified states V/VII/IX/X. ─
private static List<BlockEntry> DefaultEntries() => new List<BlockEntry>
{
new BlockEntry { BlockName = "*", VisibilityState = "V_1.65x1.00_*",
DisplayLabel = "KAS Gr. V", Color = "11", Linetype = "Continuous", Description = "" },
new BlockEntry { BlockName = "*", VisibilityState = "VII_1.60x1.40_*",
DisplayLabel = "KAS Gr. VII", Color = "11", Linetype = "Continuous", Description = "" },
new BlockEntry { BlockName = "*", VisibilityState = "IX_2.30x1.80_*",
DisplayLabel = "KAS Gr. IX", Color = "11", Linetype = "Continuous", Description = "" },
new BlockEntry { BlockName = "*", VisibilityState = "X_2.80_2.30_*",
DisplayLabel = "KAS Gr. X", Color = "11", Linetype = "Continuous", Description = "" },
new BlockEntry { BlockName = "*", VisibilityState = "V",
DisplayLabel = "KAS Gr. V", Color = "11", Linetype = "Continuous", Description = "" },
new BlockEntry { BlockName = "*", VisibilityState = "VII",
DisplayLabel = "KAS Gr. VII", Color = "11", Linetype = "Continuous", Description = "" },
new BlockEntry { BlockName = "*", VisibilityState = "IX",
DisplayLabel = "KAS Gr. IX", Color = "11", Linetype = "Continuous", Description = "" },
new BlockEntry { BlockName = "*", VisibilityState = "X",
DisplayLabel = "KAS Gr. X", Color = "11", Linetype = "Continuous", Description = "" },
};
public static void EnsureDbFile()
{
try
{
string dir = Path.GetDirectoryName(DbPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
if (!File.Exists(DbPath) || string.IsNullOrWhiteSpace(File.ReadAllText(DbPath)))
{
File.WriteAllText(DbPath,
JsonConvert.SerializeObject(DefaultEntries(), Formatting.Indented));
LogService.Info(SRC, $"Created default block DB with {DefaultEntries().Count} entries: {DbPath}");
}
else
{
MergeDefaultEntriesIntoExistingFile();
}
}
catch (Exception ex)
{
LogService.Error(SRC, $"EnsureDbFile failed: {ex.Message}");
}
}
/// <summary>
/// Adds the new default block rows to an existing user DB without
/// deleting custom rows. Known old defaults (V/VII/IX/X labels) are
/// upgraded to the requested KAS Gr. labels; custom labels are kept.
/// </summary>
private static void MergeDefaultEntriesIntoExistingFile()
{
try
{
var existing = JsonConvert.DeserializeObject<List<BlockEntry>>(
File.ReadAllText(DbPath))
?? new List<BlockEntry>();
bool changed = false;
foreach (var d in DefaultEntries())
{
var row = existing.FirstOrDefault(e =>
string.Equals(e.BlockName, d.BlockName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.VisibilityState, d.VisibilityState, StringComparison.OrdinalIgnoreCase));
if (row == null)
{
existing.Add(d);
changed = true;
continue;
}
// Upgrade only the previous bundled defaults, not a
// user's deliberate custom label.
if (IsOldBundledLabel(row.DisplayLabel, d.VisibilityState))
{
row.DisplayLabel = d.DisplayLabel;
if (string.IsNullOrWhiteSpace(row.Color)) row.Color = d.Color;
if (string.IsNullOrWhiteSpace(row.Linetype)) row.Linetype = d.Linetype;
changed = true;
}
}
if (changed)
{
File.WriteAllText(DbPath,
JsonConvert.SerializeObject(existing, Formatting.Indented));
LogService.Info(SRC, $"Merged/updated default block rows in {DbPath}");
}
}
catch (Exception ex)
{
LogService.Warn(SRC, $"Default block DB merge skipped: {ex.Message}");
}
}
private static bool IsOldBundledLabel(string label, string visibilityState)
{
string v = visibilityState;
if (v.Contains("_")) v = v.Substring(0, v.IndexOf("_", StringComparison.Ordinal));
return string.Equals(label, v, StringComparison.OrdinalIgnoreCase);
}
// ── Load / Save ───────────────────────────────────────────────
public static List<BlockEntry> Load()
{
EnsureDbFile();
try
{
var list = JsonConvert.DeserializeObject<List<BlockEntry>>(
File.ReadAllText(DbPath))
?? DefaultEntries();
LogService.Info(SRC, $"Loaded {list.Count} entries from {DbPath}");
return list;
}
catch (Exception ex)
{
LogService.Error(SRC, $"Load failed: {ex.Message}");
return DefaultEntries();
}
}
public static void Save(List<BlockEntry> entries)
{
try
{
File.WriteAllText(DbPath,
JsonConvert.SerializeObject(entries, Formatting.Indented));
LogService.Info(SRC, $"Saved {entries.Count} entries to {DbPath}");
}
catch (Exception ex) { LogService.Error(SRC, $"Save failed: {ex.Message}"); }
}
// ── Lookup with wildcard support on BOTH BlockName and
// VisibilityState. Exact match on both fields wins first;
// otherwise the first wildcard match (file order) is used. ──
public static BlockEntry GetEntry(string blockName, string visibilityState)
{
var entries = Load();
// 1. Exact match on both fields
var exact = entries.FirstOrDefault(e =>
!LayerDatabase.IsWildcard(e.BlockName) &&
!LayerDatabase.IsWildcard(e.VisibilityState) &&
string.Equals(e.BlockName, blockName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.VisibilityState, visibilityState, StringComparison.OrdinalIgnoreCase));
if (exact != null)
{
LogService.Info(SRC,
$"Exact match \"{blockName}\"/\"{visibilityState}\" → \"{exact.DisplayLabel}\"");
return exact;
}
// 2. Wildcard match (file order = priority)
foreach (var e in entries)
{
bool blockMatch = LayerDatabase.WildcardMatch(e.BlockName, blockName)
|| string.Equals(e.BlockName, blockName, StringComparison.OrdinalIgnoreCase);
bool visMatch = LayerDatabase.WildcardMatch(e.VisibilityState, visibilityState)
|| string.Equals(e.VisibilityState, visibilityState, StringComparison.OrdinalIgnoreCase);
if (blockMatch && visMatch)
{
LogService.Info(SRC,
$"Wildcard \"{e.BlockName}\"/\"{e.VisibilityState}\" matched " +
$"\"{blockName}\"/\"{visibilityState}\" → \"{e.DisplayLabel}\"");
return e;
}
}
LogService.Warn(SRC, $"No match for \"{blockName}\"/\"{visibilityState}\"");
return null;
}
public static string GetLabel(string blockName, string visibilityState) =>
GetEntry(blockName, visibilityState)?.DisplayLabel;
// ── Add / Update / Remove (keyed by BlockName + VisibilityState) ─
public static bool AddOrUpdate(BlockEntry entry)
{
var entries = Load();
var existing = entries.FirstOrDefault(e =>
string.Equals(e.BlockName, entry.BlockName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.VisibilityState, entry.VisibilityState, StringComparison.OrdinalIgnoreCase));
if (existing != null)
{
existing.DisplayLabel = entry.DisplayLabel;
existing.Color = entry.Color;
existing.Linetype = entry.Linetype;
existing.Description = entry.Description;
LogService.Info(SRC, $"Updated \"{entry.BlockName}\"/\"{entry.VisibilityState}\"");
}
else
{
entries.Add(entry);
LogService.Info(SRC, $"Added \"{entry.BlockName}\"/\"{entry.VisibilityState}\"");
}
Save(entries);
return existing != null;
}
public static bool Remove(string blockName, string visibilityState)
{
var entries = Load();
int n = entries.RemoveAll(e =>
string.Equals(e.BlockName, blockName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(e.VisibilityState, visibilityState, StringComparison.OrdinalIgnoreCase));
if (n > 0) Save(entries);
return n > 0;
}
}
}
// ===== FILE: Core\PluginVersion.cs =====
using System;
using System.IO;
using System.Reflection;
namespace LayerLeaderPlugin.Core
{
public static class PluginVersion
{
public static readonly string Number = "1.0.027";
public static readonly DateTime BuildDate = GetBuildDate();
public static string Full =>
$"{Number}_[{BuildDate:dd.MM.yyyy_HH.mm.ss}]";
public static string Banner =>
$"LayerLeaderPlugin v{Full} loaded successfully.";
private static DateTime GetBuildDate()
{
try
{
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string file = Path.Combine(dir, "BuildDate.txt");
if (File.Exists(file) &&
DateTime.TryParse(File.ReadAllText(file).Trim(), out DateTime d))
return d;
}
catch { /* ignore */ }
return File.GetLastWriteTime(Assembly.GetExecutingAssembly().Location);
}
}
}
// ===== FILE: Core\StyleMatchHelper.cs =====
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Native C# replacement for the AutoLISP "match properties" workflow
/// (SetupOverriteDimStyles.lsp). Lets the Setup Styles tab pick a Text/
/// MText, Dimension, or MLeader entity directly and read its style,
/// then apply that style as the current drawing default — without any
/// of the SendStringToExecute timing/focus fragility the LISP bridge
/// has, and without needing AutoCAD's own DCL dialog.
///
/// IMPORTANT — UI sync: every "set current" value here goes through
/// Application.SetSystemVariable (CLAYER / TEXTSTYLE / DIMSTYLE /
/// CMLEADERSTYLE) instead of writing the Database property directly.
/// Both approaches change the database, but only SetSystemVariable
/// reliably notifies AutoCAD's ribbon/toolbar controls to refresh —
/// a direct Database.Clayer (etc.) write from a WinForms event handler
/// can silently leave the ribbon's layer/style dropdown showing the
/// stale value even though the drawing default actually changed.
/// </summary>
public static class StyleMatchHelper
{
private const string SRC = "StyleMatchHelper";
public class TextStyleInfo
{
public string LayerName;
public string TextStyleName;
public double TextHeight;
}
public class DimStyleInfo
{
public string LayerName;
public string DimStyleName;
}
public class MLeaderStyleInfo
{
public string LayerName;
public string MLeaderStyleName;
public string TextStyleName;
public double TextHeight;
}
// ── TEXT / MTEXT ──────────────────────────────────────────────
public static TextStyleInfo PickTextStyle(Document doc)
{
var ed = doc.Editor;
using (doc.LockDocument())
{
var opts = new PromptEntityOptions("\nSelect Text or MText: ");
opts.SetRejectMessage("\nMust be Text or MText.");
opts.AddAllowedClass(typeof(DBText), false);
opts.AddAllowedClass(typeof(MText), false);
var res = ed.GetEntity(opts);
if (res.Status != PromptStatus.OK) return null;
using var tr = doc.Database.TransactionManager.StartTransaction();
var info = new TextStyleInfo();
if (tr.GetObject(res.ObjectId, OpenMode.ForRead) is DBText t)
{
info.LayerName = t.Layer;
info.TextHeight = t.Height;
var tsr = tr.GetObject(t.TextStyleId, OpenMode.ForRead) as TextStyleTableRecord;
info.TextStyleName = tsr?.Name ?? "Standard";
}
else if (tr.GetObject(res.ObjectId, OpenMode.ForRead) is MText mt)
{
info.LayerName = mt.Layer;
info.TextHeight = mt.TextHeight;
var tsr = tr.GetObject(mt.TextStyleId, OpenMode.ForRead) as TextStyleTableRecord;
info.TextStyleName = tsr?.Name ?? "Standard";
}
tr.Commit();
LogService.Info(SRC,
$"PickTextStyle() -> Layer=\"{info.LayerName}\" Style=\"{info.TextStyleName}\" H={info.TextHeight}");
return info;
}
}
public static void ApplyTextStyle(Document doc, TextStyleInfo info,
bool setLayer, bool setStyle)
{
using (doc.LockDocument())
{
var db = doc.Database;
if (setLayer && !string.IsNullOrEmpty(info.LayerName))
{
EnsureLayerExists(db, info.LayerName);
SetSysVarSafe("CLAYER", info.LayerName);
}
if (setStyle && !string.IsNullOrEmpty(info.TextStyleName))
{
bool exists;
using (var tr = db.TransactionManager.StartTransaction())
{
var tst = tr.GetObject(db.TextStyleTableId, OpenMode.ForRead) as TextStyleTable;
exists = tst.Has(info.TextStyleName);
tr.Commit();
}
if (exists)
SetSysVarSafe("TEXTSTYLE", info.TextStyleName);
else
LogService.Warn(SRC, $"Text style \"{info.TextStyleName}\" not found in drawing.");
}
}
LogService.Info(SRC,
$"ApplyTextStyle() -> Layer={setLayer} (\"{info.LayerName}\") Style={setStyle} (\"{info.TextStyleName}\")");
}
// ── DIMENSION ─────────────────────────────────────────────────
public static DimStyleInfo PickDimStyle(Document doc)
{
var ed = doc.Editor;
using (doc.LockDocument())
{
var opts = new PromptEntityOptions("\nSelect a Dimension: ");
opts.SetRejectMessage("\nMust be a Dimension.");
// exactMatch MUST be false — Dimension is abstract; every
// real dimension is a concrete subtype (RotatedDimension,
// AlignedDimension, RadialDimension, DiametricDimension,
// ArcDimension, OrdinateDimension, ...). exactMatch:true
// rejected every single click regardless of what was picked.
opts.AddAllowedClass(typeof(Dimension), false);
var res = ed.GetEntity(opts);
if (res.Status != PromptStatus.OK) return null;
using var tr = doc.Database.TransactionManager.StartTransaction();
var info = new DimStyleInfo();
if (tr.GetObject(res.ObjectId, OpenMode.ForRead) is Dimension dim)
{
info.LayerName = dim.Layer;
var dsr = tr.GetObject(dim.DimensionStyle, OpenMode.ForRead) as DimStyleTableRecord;
info.DimStyleName = dsr?.Name ?? "Standard";
}
tr.Commit();
LogService.Info(SRC,
$"PickDimStyle() -> Layer=\"{info.LayerName}\" Style=\"{info.DimStyleName}\"");
return info;
}
}
public static void ApplyDimStyle(Document doc, DimStyleInfo info,
bool setLayer, bool setStyle)
{
bool styleExists = false;
// FIX: SendStringToExecute must NOT be called while
// doc.LockDocument() is held. SendStringToExecute queues a
// command onto AutoCAD's normal command-line input pipeline,
// which manages its own document locking internally — if a
// managed-code lock is still held when AutoCAD's idle loop
// tries to process that queued command, the command can
// silently fail to execute (or execute against stale state)
// by the time the lock is released right after this method
// returns. This was confirmed directly: the log showed
// "Set current dimension style via -DIMSTYLE Restore" as if
// it succeeded, but the very next PurgeCurrentDimStyleOverrides
// call read DIMSTYLE back as the OLD style ("@Dan"), proving
// the restore never actually took effect despite no error
// being thrown — SendStringToExecute had simply queued the
// string and returned; it never got to execute before the
// lock below released.
//
// The proven-correct pattern (see LispIntegration.cs, which
// has worked reliably since the very first version of this
// file) never wraps SendStringToExecute in LockDocument at
// all. Database reads (checking the style exists) still need
// a transaction and are kept inside their own short-lived
// lock; SendStringToExecute is called afterward, unlocked.
using (doc.LockDocument())
{
var db = doc.Database;
if (setLayer && !string.IsNullOrEmpty(info.LayerName))
{
EnsureLayerExists(db, info.LayerName);
SetSysVarSafe("CLAYER", info.LayerName);
}
if (setStyle && !string.IsNullOrEmpty(info.DimStyleName))
{
using var tr = db.TransactionManager.StartTransaction();
var dst = tr.GetObject(db.DimStyleTableId, OpenMode.ForRead) as DimStyleTable;
styleExists = dst.Has(info.DimStyleName);
tr.Commit();
}
}
if (setStyle && !string.IsNullOrEmpty(info.DimStyleName))
{
if (styleExists)
{
// "-DIMSTYLE Restore <name>" both sets the named style
// current AND resets every individual DIMVAR to match
// it (eliminating any stray "<style overrides>" ghost
// branch), so one command does the complete job
// correctly — called here, OUTSIDE any document lock.
string escName = info.DimStyleName.Contains(" ")
? $"\"{info.DimStyleName}\"" : info.DimStyleName;
doc.SendStringToExecute(
$"\n_.-dimstyle Restore {escName}\n", true, false, false);
LogService.Info(SRC, $"Queued -DIMSTYLE Restore \"{info.DimStyleName}\" (unlocked).");
}
else
{
LogService.Warn(SRC, $"Dimension style \"{info.DimStyleName}\" not found in drawing.");
}
}
LogService.Info(SRC,
$"ApplyDimStyle() -> Layer={setLayer} (\"{info.LayerName}\") Style={setStyle} (\"{info.DimStyleName}\")");
}
/// <summary>
/// Forces the CURRENT dimension style's overrides to be discarded
/// without changing which style is current — i.e. removes the
/// "<style overrides>" ghost branch AutoCAD/Civil 3D shows
/// under a style in the Dimension Style Manager. Equivalent to
/// opening Dimension Style Manager, right-clicking the override
/// branch, and choosing "Delete", just scriptable from the panel.
/// </summary>
public static void PurgeCurrentDimStyleOverrides(Document doc)
{
// FIX: same lock-wrapping bug as ApplyDimStyle — see the
// detailed comment there. GetSystemVariable is a simple
// read and doesn't need a document lock either; only the
// database transaction in PurgeAllDimStyleOverrides below
// genuinely requires one, and SendStringToExecute must
// always run unlocked.
string current = (string)AcadApp.GetSystemVariable("DIMSTYLE");
if (string.IsNullOrEmpty(current)) return;
string escName = current.Contains(" ") ? $"\"{current}\"" : current;
doc.SendStringToExecute($"\n_.-dimstyle Restore {escName}\n", true, false, false);
LogService.Info(SRC, $"PurgeCurrentDimStyleOverrides() -> queued restore for \"{current}\" (unlocked).");
}
/// <summary>
/// Restores EVERY named dimension style in the drawing, clearing
/// the "<style overrides>" ghost branch under each one —
/// not just the current style. Iterates the DimStyleTable and
/// runs "-DIMSTYLE Restore" once per style, then restores
/// whichever style was current before the cleanup started.
/// </summary>
public static int PurgeAllDimStyleOverrides(Document doc)
{
// FIX: same lock-wrapping bug — see ApplyDimStyle's comment.
// The database transaction (reading style names) genuinely
// needs a lock; every SendStringToExecute call below it must
// not be inside one.
var names = new System.Collections.Generic.List<string>();
using (doc.LockDocument())
{
var db = doc.Database;
using var tr = db.TransactionManager.StartTransaction();
var dst = tr.GetObject(db.DimStyleTableId, OpenMode.ForRead) as DimStyleTable;
foreach (ObjectId id in dst)
{
var rec = tr.GetObject(id, OpenMode.ForRead) as DimStyleTableRecord;
if (rec != null && !rec.IsErased) names.Add(rec.Name);
}
tr.Commit();
}
string originalCurrent = (string)AcadApp.GetSystemVariable("DIMSTYLE");
foreach (var name in names)
{
string esc = name.Contains(" ") ? $"\"{name}\"" : name;
doc.SendStringToExecute($"\n_.-dimstyle Restore {esc}\n", true, false, false);
}
if (!string.IsNullOrEmpty(originalCurrent))
{
string esc = originalCurrent.Contains(" ") ? $"\"{originalCurrent}\"" : originalCurrent;
doc.SendStringToExecute($"\n_.-dimstyle Restore {esc}\n", true, false, false);
}
LogService.Info(SRC, $"PurgeAllDimStyleOverrides() -> queued restore for {names.Count} styles (unlocked).");
return names.Count;
}
// ── MLEADER ───────────────────────────────────────────────────
public static MLeaderStyleInfo PickMLeaderStyle(Document doc)
{
var ed = doc.Editor;
using (doc.LockDocument())
{
var opts = new PromptEntityOptions("\nSelect an MLeader: ");
opts.SetRejectMessage("\nMust be an MLeader.");
opts.AddAllowedClass(typeof(MLeader), false);
var res = ed.GetEntity(opts);
if (res.Status != PromptStatus.OK) return null;
using var tr = doc.Database.TransactionManager.StartTransaction();
var info = new MLeaderStyleInfo();
if (tr.GetObject(res.ObjectId, OpenMode.ForRead) is MLeader ml)
{
info.LayerName = ml.Layer;
info.MLeaderStyleName = GetMLeaderStyleName(tr, doc.Database, ml.MLeaderStyle);
info.TextHeight = ml.TextHeight;
var tsr = tr.GetObject(ml.TextStyleId, OpenMode.ForRead) as TextStyleTableRecord;
info.TextStyleName = tsr?.Name ?? "Standard";
}
tr.Commit();
LogService.Info(SRC,
$"PickMLeaderStyle() -> Layer=\"{info.LayerName}\" Style=\"{info.MLeaderStyleName}\"");
return info;
}
}
public static void ApplyMLeaderStyle(Document doc, MLeaderStyleInfo info,
bool setLayer, bool setStyle)
{
using (doc.LockDocument())
{
var db = doc.Database;
if (setLayer && !string.IsNullOrEmpty(info.LayerName))
{
EnsureLayerExists(db, info.LayerName);
SetSysVarSafe("CLAYER", info.LayerName);
}
if (setStyle && !string.IsNullOrEmpty(info.MLeaderStyleName))
{
bool exists;
using (var tr = db.TransactionManager.StartTransaction())
{
var dict = tr.GetObject(db.MLeaderStyleDictionaryId, OpenMode.ForRead) as DBDictionary;
exists = dict.Contains(info.MLeaderStyleName);
tr.Commit();
}
if (exists)
SetSysVarSafe("CMLEADERSTYLE", info.MLeaderStyleName);
else
LogService.Warn(SRC, $"MLeader style \"{info.MLeaderStyleName}\" not found in drawing.");
}
}
LogService.Info(SRC,
$"ApplyMLeaderStyle() -> Layer={setLayer} (\"{info.LayerName}\") Style={setStyle} (\"{info.MLeaderStyleName}\")");
}
// ── Shared helpers ────────────────────────────────────────────
/// <summary>
/// Sets an AutoCAD system variable through the documented
/// Application API. This is what actually keeps the ribbon's
/// current-layer / current-style dropdowns in sync — writing the
/// equivalent Database property directly (e.g. db.Clayer) changes
/// the drawing default but does NOT reliably trigger the ribbon's
/// reactor-driven refresh from a background/event-handler context.
/// </summary>
private static void SetSysVarSafe(string name, string value)
{
try
{
AcadApp.SetSystemVariable(name, value);
LogService.Debug(SRC, $"SetSystemVariable(\"{name}\", \"{value}\")");
}
catch (System.Exception ex)
{
LogService.Error(SRC, $"SetSystemVariable(\"{name}\", \"{value}\") failed: {ex.Message}");
}
}
private static void EnsureLayerExists(Database db, string name)
{
using var tr = db.TransactionManager.StartTransaction();
var lt = tr.GetObject(db.LayerTableId, OpenMode.ForRead) as LayerTable;
if (!lt.Has(name))
{
lt.UpgradeOpen();
var ltr = new LayerTableRecord { Name = name };
lt.Add(ltr);
tr.AddNewlyCreatedDBObject(ltr, true);
LogService.Info(SRC, $"Created missing layer \"{name}\" before setting it current.");
}
tr.Commit();
}
private static string GetMLeaderStyleName(Transaction tr, Database db, ObjectId styleId)
{
if (styleId.IsNull) return "Standard";
var dict = tr.GetObject(db.MLeaderStyleDictionaryId, OpenMode.ForRead) as DBDictionary;
foreach (DBDictionaryEntry entry in dict)
{
if (entry.Value == styleId) return entry.Key;
}
return "Standard";
}
}
}
// ===== FILE: Core\ArrowheadHelper.cs =====
using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Resolves AutoCAD's standard built-in arrowhead block names (the
/// same set offered in the Symbol dropdown of AutoCAD's own Modify
/// Multileader Style dialog) to ObjectIds, so MLeaderStyle.ArrowSymbolId
/// can actually be set to something other than ObjectId.Null.
///
/// These are well-known anonymous block names AutoCAD has shipped
/// since R14 for DIMBLK/DIMBLK1/LEADER arrowheads. AutoCAD creates
/// their block definitions on first reference automatically — this
/// helper triggers that by checking the block table and, if missing,
/// inserting a zero-geometry placeholder reference via the documented
/// pattern AutoCAD itself uses internally (acquiring the definition
/// from the running application's built-in resources). If a name
/// can't be resolved for any reason, the caller falls back to "None"
/// (ObjectId.Null) rather than throwing, so a bad/unsupported name
/// never blocks placing the MLeader itself.
/// </summary>
public static class ArrowheadHelper
{
private const string SRC = "ArrowheadHelper";
/// <summary>
/// Display name -> AutoCAD internal block name, in the same order
/// as AutoCAD's own Modify Multileader Style "Symbol" dropdown.
/// </summary>
public static readonly (string Display, string BlockName)[] StandardArrowheads =
{
("None", null),
("Closed filled", "_CLOSEDFILLED"),
("Dot", "_DOT"),
("Dot small", "_DOTSMALL"),
("Dot blank", "_DOTBLANK"),
("Origin indicator", "_ORIGIN"),
("Origin indicator 2", "_ORIGIN2"),
("Open", "_OPEN"),
("Right angle", "_OPEN90"),
("Open 30", "_OPEN30"),
("Closed", "_CLOSED"),
("Dot small blank", "_SMALL"),
("None (block)", "_NONE"),
("Oblique", "_OBLIQUE"),
("Box filled", "_BOXFILLED"),
("Box", "_BOXBLANK"),
("Closed blank", "_CLOSEDBLANK"),
("Datum triangle filled", "_DATUMFILLED"),
("Datum triangle", "_DATUMBLANK"),
("Integral", "_INTEGRAL"),
("Architectural tick", "_ARCHTICK"),
};
public static IEnumerable<string> DisplayNames()
{
foreach (var (display, _) in StandardArrowheads)
yield return display;
}
/// <summary>
/// Resolves a display name (from the dropdown) to an ObjectId
/// usable as MLeaderStyle.ArrowSymbolId / MLeader.ArrowSymbolId.
/// Returns ObjectId.Null for "None" or if the block can't be
/// resolved for any reason — that is the correct, safe value for
/// "no arrowhead" and never throws.
/// </summary>
public static ObjectId ResolveByDisplayName(Database db, Transaction tr, string displayName)
{
foreach (var (display, blockName) in StandardArrowheads)
{
if (string.Equals(display, displayName, System.StringComparison.OrdinalIgnoreCase))
return ResolveBlockName(db, tr, blockName);
}
LogService.Warn(SRC, $"Unknown arrowhead \"{displayName}\" — falling back to None.");
return ObjectId.Null;
}
private static ObjectId ResolveBlockName(Database db, Transaction tr, string blockName)
{
if (string.IsNullOrEmpty(blockName)) return ObjectId.Null; // "None"
var bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
if (bt.Has(blockName))
{
LogService.Debug(SRC, $"Resolved arrowhead block \"{blockName}\" (already defined).");
return bt[blockName];
}
// Not yet defined in this drawing. AutoCAD auto-creates these
// standard arrow blocks the moment any DIMBLK/leader/style
// references them by name — the documented, supported way to
// trigger that from managed code is to set a DIMSTYLE's
// DIMBLK to this name via SetSystemVariable, which forces
// AutoCAD to materialise the block definition, then look it
// up again. This mirrors exactly what happens when a user
// picks the same name from AutoCAD's own arrowhead dropdown.
try
{
object prevDimblk = Autodesk.AutoCAD.ApplicationServices.Application
.GetSystemVariable("DIMBLK");
Autodesk.AutoCAD.ApplicationServices.Application
.SetSystemVariable("DIMBLK", blockName);
Autodesk.AutoCAD.ApplicationServices.Application
.SetSystemVariable("DIMBLK", prevDimblk);
}
catch (System.Exception ex)
{
LogService.Warn(SRC, $"Could not trigger auto-creation of \"{blockName}\": {ex.Message}");
}
// Re-check — note: bt was opened in this transaction before
// the sysvar round-trip, which can create the block table
// record outside this transaction's view. Re-fetch fresh.
var btFresh = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
if (btFresh.Has(blockName))
{
LogService.Info(SRC, $"Auto-created and resolved arrowhead block \"{blockName}\".");
return btFresh[blockName];
}
LogService.Warn(SRC,
$"Could not resolve or auto-create arrowhead block \"{blockName}\" — using None instead.");
return ObjectId.Null;
}
}
}
// ===== FILE: Core\LayerDatabase.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using LayerLeaderPlugin.Models;
using Newtonsoft.Json;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Manages the JSON layer→label database.
///
/// Wildcard matching:
/// LayerName may contain * (any sequence) and ? (single char).
/// Exact matches are preferred; wildcard matches are used as fallback
/// in the order they appear in the file (first match wins).
/// </summary>
public static class LayerDatabase
{
private const string SRC = "LayerDatabase";
public static string DbPath { get; private set; } = BuildDefaultPath();
private static string BuildDefaultPath()
{
string dir = Path.GetDirectoryName(typeof(LayerDatabase).Assembly.Location);
return Path.Combine(dir, "layer_database.json");
}
public static void SetPath(string path) => DbPath = path;
// ── Defaults ─────────────────────────────────────────────────
private static List<LayerEntry> DefaultEntries()
{
return new List<LayerEntry>
{
new LayerEntry
{
LayerName = "VA_KTB_KK_Gr._I_i.F_220X275",
DisplayLabel = "KK Gr. I i.F",
Color = "11",
Linetype = "Continuous",
Description = ""
},
new LayerEntry
{
LayerName = "VA_KTB_KK_Gr._II_i.F_400X275",
DisplayLabel = "KK Gr. II i.F",
Color = "11",
Linetype = "Continuous",
Description = ""
},
new LayerEntry
{
LayerName = "VA_KTB_KK_Gr._IIIa_i.F_515X275",
DisplayLabel = "KK Gr. IIIa i.F",
Color = "11",
Linetype = "Continuous",
Description = ""
},
new LayerEntry
{
LayerName = "VA_KTB_KK_Gr._IV_i.F_700X285",
DisplayLabel = "KK Gr. IV i.F",
Color = "11",
Linetype = "Continuous",
Description = ""
}
};
}
/// <summary>
/// Ensures the runtime DB file exists beside the loaded DLL.
/// If missing or blank, it is seeded with the four Excel rows.
/// </summary>
public static void EnsureDbFile()
{
try
{
string dir = Path.GetDirectoryName(DbPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
if (!File.Exists(DbPath) || string.IsNullOrWhiteSpace(File.ReadAllText(DbPath)))
{
File.WriteAllText(DbPath,
JsonConvert.SerializeObject(DefaultEntries(), Formatting.Indented));
LogService.Info(SRC, $"Created default DB with 4 entries: {DbPath}");
}
}
catch (Exception ex)
{
LogService.Error(SRC, $"EnsureDbFile failed: {ex.Message}");
}
}
// ── Load / Save ───────────────────────────────────────────────
public static List<LayerEntry> Load()
{
EnsureDbFile();
try
{
var list = JsonConvert.DeserializeObject<List<LayerEntry>>(
File.ReadAllText(DbPath))
?? DefaultEntries();
LogService.Info(SRC, $"Loaded {list.Count} entries from {DbPath}");
return list;
}
catch (Exception ex)
{
LogService.Error(SRC, $"Load failed: {ex.Message}");
var defaults = DefaultEntries();
try
{
File.WriteAllText(DbPath,
JsonConvert.SerializeObject(defaults, Formatting.Indented));
LogService.Warn(SRC, $"Recreated default DB after load failure: {DbPath}");
}
catch (Exception writeEx)
{
LogService.Error(SRC, $"Default DB write failed: {writeEx.Message}");
}
return defaults;
}
}
public static void Save(List<LayerEntry> entries)
{
try
{
File.WriteAllText(DbPath,
JsonConvert.SerializeObject(entries, Formatting.Indented));
LogService.Info(SRC, $"Saved {entries.Count} entries to {DbPath}");
}
catch (Exception ex) { LogService.Error(SRC, $"Save failed: {ex.Message}"); }
}
// ── Lookup with wildcard support ──────────────────────────────
/// <summary>
/// Returns the best-matching LayerEntry for the given layer name.
/// Priority: exact match (case-insensitive) → first wildcard match.
/// Returns null if nothing matches.
/// </summary>
public static LayerEntry GetEntry(string layerName)
{
var entries = Load();
// 1. Exact match
var exact = entries.FirstOrDefault(e =>
!IsWildcard(e.LayerName) &&
string.Equals(e.LayerName, layerName, StringComparison.OrdinalIgnoreCase));
if (exact != null)
{
LogService.Info(SRC, $"Exact match \"{layerName}\" → \"{exact.DisplayLabel}\"");
return exact;
}
// 2. First wildcard match (order in file = priority)
foreach (var e in entries.Where(e => IsWildcard(e.LayerName)))
{
if (WildcardMatch(e.LayerName, layerName))
{
LogService.Info(SRC,
$"Wildcard \"{e.LayerName}\" matched \"{layerName}\" → \"{e.DisplayLabel}\"");
return e;
}
}
LogService.Warn(SRC, $"No match for \"{layerName}\"");
return null;
}
/// <summary>Convenience overload — returns just the display label or null.</summary>
public static string GetLabel(string layerName) => GetEntry(layerName)?.DisplayLabel;
// ── Add / Update / Remove ─────────────────────────────────────
public static bool AddOrUpdate(LayerEntry entry)
{
var entries = Load();
var existing = entries.FirstOrDefault(e =>
string.Equals(e.LayerName, entry.LayerName, StringComparison.OrdinalIgnoreCase));
if (existing != null)
{
existing.DisplayLabel = entry.DisplayLabel;
existing.Color = entry.Color;
existing.Linetype = entry.Linetype;
existing.Description = entry.Description;
LogService.Info(SRC, $"Updated \"{entry.LayerName}\"");
}
else
{
entries.Add(entry);
LogService.Info(SRC, $"Added \"{entry.LayerName}\"");
}
Save(entries);
return existing != null;
}
public static bool Remove(string layerName)
{
var entries = Load();
int n = entries.RemoveAll(e =>
string.Equals(e.LayerName, layerName, StringComparison.OrdinalIgnoreCase));
if (n > 0) Save(entries);
return n > 0;
}
// ── Wildcard helpers ──────────────────────────────────────────
public static bool IsWildcard(string pattern) =>
pattern != null && (pattern.Contains('*') || pattern.Contains('?'));
/// <summary>
/// Matches a shell-style wildcard pattern (* = any chars, ? = one char)
/// against a target string, case-insensitive.
/// </summary>
public static bool WildcardMatch(string pattern, string input)
{
if (pattern == null || input == null) return false;
// Convert * → .* and ? → . for regex
string rx = "^" +
Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") +
"$";
return Regex.IsMatch(input, rx, RegexOptions.IgnoreCase);
}
}
}
// ===== FILE: Core\LogService.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using LayerLeaderPlugin.Models;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Thread-safe in-memory log with optional file persistence.
/// All UI subscribers receive new entries via the EntryAdded event.
/// </summary>
public static class LogService
{
private static readonly object _lock = new object();
private static readonly List<LogEntry> _entries = new List<LogEntry>();
// Fired on the thread that called Add() — marshal to UI thread in handlers
public static event Action<LogEntry> EntryAdded;
// ── Path for persisted log (next to DLL) ─────────────────────
private static string LogFilePath
{
get
{
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
return Path.Combine(dir, "LayerLeaderPlugin.log");
}
}
public static bool PersistToDisk { get; set; } = false;
// ── Public API ────────────────────────────────────────────────
public static void Info (string source, string msg) => Add(LogLevel.INFO, source, msg);
public static void Warn (string source, string msg) => Add(LogLevel.WARN, source, msg);
public static void Error(string source, string msg) => Add(LogLevel.ERROR, source, msg);
public static void Debug(string source, string msg) => Add(LogLevel.DEBUG, source, msg);
public static IReadOnlyList<LogEntry> Entries
{
get { lock (_lock) { return _entries.AsReadOnly(); } }
}
public static void Add(LogLevel level, string source, string message)
{
var entry = new LogEntry
{
Timestamp = DateTime.Now,
Level = level,
Source = source,
Message = message
};
lock (_lock)
{
_entries.Add(entry);
if (PersistToDisk) WriteToDisk(entry);
}
EntryAdded?.Invoke(entry);
}
/// <summary>
/// Save the entire in-memory log to disk (user-triggered "Store log" action).
/// The header includes the loaded DLL path and version so a stored log can be
/// used later to reproduce/improve the exact loaded code path.
/// </summary>
public static string SaveLogSnapshot()
{
string dllPath = Assembly.GetExecutingAssembly().Location;
string dir = Path.GetDirectoryName(dllPath);
string path = Path.Combine(dir,
$"LayerLeaderPlugin_snapshot_{DateTime.Now:yyyyMMdd_HHmmss}.log");
lock (_lock)
{
var lines = new List<string>
{
$"LayerLeaderPlugin v{PluginVersion.Full}",
$"Saved: {DateTime.Now:yyyy-MM-dd HH:mm:ss}",
$"Loaded code: {dllPath}",
$"Database: {LayerDatabase.DbPath}",
""
};
lines.AddRange(_entries.ConvertAll(e => e.ToString()));
File.WriteAllLines(path, lines);
}
return path;
}
public static void Clear()
{
lock (_lock) { _entries.Clear(); }
}
private static void WriteToDisk(LogEntry entry)
{
try { File.AppendAllText(LogFilePath, entry + Environment.NewLine); }
catch { /* don't let logging crash the plugin */ }
}
}
}
// ===== FILE: Core\PreviewCollector.cs =====
using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Extracts a list of 2-D world-space points from a Line, Polyline or Spline
/// so the preview panel can draw them without touching AutoCAD drawing APIs.
/// </summary>
public static class PreviewCollector
{
private const string SRC = "PreviewCollector";
public static List<Point2d> Collect(Entity entity)
{
var pts = new List<Point2d>();
switch (entity)
{
case Line line:
pts.Add(new Point2d(line.StartPoint.X, line.StartPoint.Y));
pts.Add(new Point2d(line.EndPoint.X, line.EndPoint.Y));
break;
case Polyline pl:
for (int i = 0; i < pl.NumberOfVertices; i++)
pts.Add(pl.GetPoint2dAt(i));
if (pl.Closed && pts.Count > 0)
pts.Add(pts[0]); // close the loop visually
break;
case Polyline2d pl2:
using (var tr = pl2.Database.TransactionManager.StartTransaction())
{
foreach (ObjectId vid in pl2)
{
var v = tr.GetObject(vid, OpenMode.ForRead) as Vertex2d;
if (v != null)
pts.Add(new Point2d(v.Position.X, v.Position.Y));
}
tr.Commit();
}
break;
case Spline spline:
// Sample the spline at 64 points for a smooth preview curve
int samples = 64;
double pStart = spline.StartParam;
double pEnd = spline.EndParam;
double step = (pEnd - pStart) / samples;
for (int i = 0; i <= samples; i++)
{
double param = pStart + i * step;
Point3d p3 = spline.GetPointAtParameter(param);
pts.Add(new Point2d(p3.X, p3.Y));
}
break;
}
LogService.Debug(SRC, $"Collected {pts.Count} points from {entity.GetType().Name}");
return pts;
}
}
}
// ===== FILE: Core\BlockInspector.cs =====
using System;
using System.Linq;
using Autodesk.AutoCAD.DatabaseServices;
namespace LayerLeaderPlugin.Core
{
/// <summary>
/// Reads dynamic block name and current visibility state from a
/// BlockReference, used by the Blocks sub-tab "Place Label" workflow.
/// </summary>
public static class BlockInspector
{
private const string SRC = "BlockInspector";
/// <summary>
/// Returns the effective block name (the visible/anonymous-resolved
/// dynamic block definition name).
/// </summary>
public static string GetEffectiveName(BlockReference br, Transaction tr)
{
ObjectId defId = br.IsDynamicBlock ? br.DynamicBlockTableRecord : br.BlockTableRecord;
var btr = tr.GetObject(defId, OpenMode.ForRead) as BlockTableRecord;
return btr?.Name ?? br.Name;
}
/// <summary>
/// Returns the current value of the block's visibility-state
/// dynamic property, or null if the block has none.
///
/// FIX: the previous implementation matched on
/// PropertyTypeCode == 1 first, which is NOT unique to visibility
/// properties — generic "no units" numeric/string properties (for
/// example a stretch distance parameter like "XX" seen on
/// _Alle_KAS) share the same type code in this ObjectARX binding.
/// That caused the lookup to grab "XX" (e.g. "3.9833...") instead
/// of "Visibility1" (e.g. "X_2.80_2.30_Mit_Text").
///
/// The fix matches by NAME first ("Visibility1", case-insensitive,
/// which is the AutoCAD-assigned name for every block's visibility
/// parameter and never changes). Only if no such named property
/// exists does it fall back to detecting a property that has a
/// non-empty fixed allowed-values list AND is read-only — both of
/// which are true for visibility states and false for free-form
/// numeric parameters like "XX".
/// </summary>
public static string GetVisibilityState(BlockReference br)
{
if (!br.IsDynamicBlock) return null;
var props = br.DynamicBlockReferencePropertyCollection
.Cast<DynamicBlockReferenceProperty>()
.ToArray();
// 1. Authoritative: exact name match "Visibility1"
var byName = props.FirstOrDefault(p =>
string.Equals(p.PropertyName, "Visibility1", StringComparison.OrdinalIgnoreCase));
if (byName != null)
{
LogService.Debug(SRC, $"GetVisibilityState() -> matched by name \"Visibility1\".");
return byName.Value?.ToString();
}
// 2. Fallback: a property with a fixed allowed-values list and
// ReadOnly = true is the structural signature of a
// visibility parameter; numeric stretch/distance params
// (like "XX") are NOT read-only and have no allowed list.
var byShape = props.FirstOrDefault(p =>
{
try
{
return p.ReadOnly && (p.GetAllowedValues()?.Length ?? 0) > 0;
}
catch { return false; }
});
if (byShape != null)
{
LogService.Warn(SRC,
$"GetVisibilityState() -> no \"Visibility1\" property found; " +
$"used fallback match on \"{byShape.PropertyName}\".");
return byShape.Value?.ToString();
}
LogService.Warn(SRC, "GetVisibilityState() -> no visibility-shaped property found.");
return null;
}
/// <summary>
/// Lists all visibility-state names available on the block, in
/// dropdown order.
/// </summary>
public static string[] GetAllVisibilityStates(BlockReference br)
{
if (!br.IsDynamicBlock) return Array.Empty<string>();
var props = br.DynamicBlockReferencePropertyCollection
.Cast<DynamicBlockReferenceProperty>()
.ToArray();
var prop = props.FirstOrDefault(p =>
string.Equals(p.PropertyName, "Visibility1", StringComparison.OrdinalIgnoreCase))
?? props.FirstOrDefault(p =>
{
try { return p.ReadOnly && (p.GetAllowedValues()?.Length ?? 0) > 0; }
catch { return false; }
});
if (prop == null) return Array.Empty<string>();
var values = prop.GetAllowedValues();
var result = new string[values.Length];
for (int i = 0; i < values.Length; i++)
result[i] = values[i]?.ToString();
return result;
}
}
}
// ===== FILE: LayerLeaderCommands.cs =====
using System;
using System.IO;
using System.Reflection;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using LayerLeaderPlugin.Core;
using LayerLeaderPlugin.Forms;
[assembly: CommandClass(typeof(LayerLeaderPlugin.LayerLeaderCommands))]
[assembly: ExtensionApplication(typeof(LayerLeaderPlugin.PluginInit))]
namespace LayerLeaderPlugin
{
// ─────────────────────────────────────────────────────────────────
// Plugin initialisation — fires once on NETLOAD
// ─────────────────────────────────────────────────────────────────
public class PluginInit : IExtensionApplication
{
private const string SRC = "PluginInit";
private static MainForm _form;
internal static MainForm Form => _form;
public void Initialize()
{
string dllPath = Assembly.GetExecutingAssembly().Location;
string dllDir = Path.GetDirectoryName(dllPath);
LayerDatabase.EnsureDbFile();
BlockDatabase.EnsureDbFile();
LabelFieldUpdater.Initialize();
LogService.Info(SRC, PluginVersion.Banner);
LogService.Info(SRC, $"Loaded code : {dllPath}");
LogService.Info(SRC, $"DLL folder : {dllDir}");
LogService.Info(SRC, $"DB path : {LayerDatabase.DbPath}");
var doc = Application.DocumentManager.MdiActiveDocument;
EnsureCurrentDrawingStyle(doc);
doc?.Editor.WriteMessage($"\n{PluginVersion.Banner}");
doc?.Editor.WriteMessage($"\n Loaded code : {dllPath}");
doc?.Editor.WriteMessage($"\n DLL folder : {dllDir}");
doc?.Editor.WriteMessage($"\n DB path : {LayerDatabase.DbPath}");
// Queue load of the integrated AutoLISP helper into the active drawing.
LispIntegration.EnsureLoaded(doc);
doc?.Editor.WriteMessage($"\n MLeader style: {MLeaderStyleHelper.StyleName} is current");
doc?.Editor.WriteMessage(
"\nCommands: LLP PLACELEADER ADDLAYER REMOVELAYER LISTLAYERS " +
"ADDBLOCK REMOVEBLOCK LISTBLOCKS LLUPDATEFIELDS SETUPDIMSTYLES SetupOverriteDimStyles\n");
_form = new MainForm();
}
public void Terminate() => _form?.Dispose();
internal static void RecreateFormIfNeeded()
{
if (_form == null || _form.IsDisposed)
_form = new MainForm();
}
private static void EnsureCurrentDrawingStyle(Document doc)
{
if (doc == null) return;
try
{
using (doc.LockDocument())
using (var tr = doc.Database.TransactionManager.StartTransaction())
{
MLeaderStyleHelper.EnsureStyle(doc.Database, tr);
tr.Commit();
}
LogService.Info(SRC, $"MLeader style \"{MLeaderStyleHelper.StyleName}\" ensured and set current.");
}
catch (System.Exception ex)
{
LogService.Error(SRC, $"Failed to ensure current MLeader style: {ex.Message}");
doc.Editor.WriteMessage($"\n[ERROR] Failed to set DB MLeader style current: {ex.Message}");
}
}
}
// ─────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────
public class LayerLeaderCommands
{
private const string SRC = "LayerLeaderCommands";
// ── LLP / SHOWPANEL — open the panel ─────────────────────────
[CommandMethod("LLP")]
public void ShowPanelShort() => OpenPanel();
[CommandMethod("SHOWPANEL")]
public void ShowPanel() => OpenPanel();
// ── PLACELEADER — open panel then start Place Leader workflow ─
// The workflow (entity pick → point picks → MLeader) is driven
// from the form's "▶ Place Leader" button / StartPlaceLeader().
// This command opens the panel and clicks that button for you.
[CommandMethod("PLACELEADER")]
public void PlaceLeader()
{
LogService.Info(SRC, "PlaceLeader()");
OpenPanel();
// Trigger the Place Leader button on the AutoCAD main thread
// (we're already on it here since this is a command method)
PluginInit.Form?.BeginInvoke(
new Action(() => PluginInit.Form?.InvokePlaceLeader()));
}
private static void OpenPanel()
{
PluginInit.RecreateFormIfNeeded();
Application.ShowModelessDialog(PluginInit.Form);
LogService.Info(SRC, "OpenPanel() -> panel opened.");
}
// ── ADDLAYER — command-line shortcut to the Add Layer dialog ─
[CommandMethod("ADDLAYER")]
public void AddLayer()
{
var ed = Application.DocumentManager.MdiActiveDocument.Editor;
LogService.Info(SRC, "AddLayer()");
string layerName = PromptString(ed,
"\nFull layer name (e.g. VA_KTB_KK_Gr._IIIa_i.F_515X275): ");
if (layerName == null) return;
string label = PromptString(ed, "\nDisplay label (e.g. KK Gr. IIIa i.F): ");
if (label == null) return;
string color = PromptString(ed,
"\nColor index (default 11): ", allowEmpty: true);
if (string.IsNullOrWhiteSpace(color)) color = "11";
string linetype = PromptString(ed,
"\nLinetype (default Continuous): ", allowEmpty: true);
if (string.IsNullOrWhiteSpace(linetype)) linetype = "Continuous";
string desc = PromptString(ed,
"\nDescription (optional – Enter to skip): ", allowEmpty: true) ?? "";
bool updated = LayerDatabase.AddOrUpdate(new Models.LayerEntry
{
LayerName = layerName,
DisplayLabel = label,
Color = color,
Linetype = linetype,
Description = desc
});
ed.WriteMessage(updated
? $"\n[INFO] Updated \"{layerName}\"."
: $"\n[INFO] Added \"{layerName}\".");
LogService.Info(SRC,
$"AddLayer() -> {(updated ? "updated" : "added")} \"{layerName}\" " +
$"Label=\"{label}\", Color={color}, Linetype={linetype}.");
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
ed.WriteMessage($"\n[INFO] Refreshed {refreshed} linked label field(s).");
if (PluginInit.Form?.Visible == true)
PluginInit.Form.BeginInvoke(new Action(() => PluginInit.Form.LoadGrid()));
}
// ── REMOVELAYER ───────────────────────────────────────────────
[CommandMethod("REMOVELAYER")]
public void RemoveLayer()
{
var ed = Application.DocumentManager.MdiActiveDocument.Editor;
LogService.Info(SRC, "RemoveLayer()");
string n = PromptString(ed, "\nLayer name to remove: ");
if (n == null) return;
bool ok = LayerDatabase.Remove(n);
ed.WriteMessage(ok ? $"\n[INFO] Removed \"{n}\"." : $"\n[WARN] Not found: \"{n}\".");
LogService.Info(SRC, ok ? $"RemoveLayer() -> removed \"{n}\"." : $"RemoveLayer() -> not found \"{n}\".");
if (ok)
{
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
ed.WriteMessage($"\n[INFO] Refreshed {refreshed} linked label field(s).");
}
if (ok && PluginInit.Form?.Visible == true)
PluginInit.Form.BeginInvoke(new Action(() => PluginInit.Form.LoadGrid()));
}
// ── LISTLAYERS ────────────────────────────────────────────────
[CommandMethod("LISTLAYERS")]
public void ListLayers()
{
var ed = Application.DocumentManager.MdiActiveDocument.Editor;
LogService.Info(SRC, "ListLayers()");
var entries = LayerDatabase.Load();
ed.WriteMessage($"\n DB: {LayerDatabase.DbPath}");
if (entries.Count == 0)
{ ed.WriteMessage("\n (empty — use ADDLAYER or the panel ▶ Add Layer button)"); return; }
ed.WriteMessage($"\n── {entries.Count} entries ──");
foreach (var e in entries)
{
ed.WriteMessage($"\n {e.LayerName} → {e.DisplayLabel} | Color={e.Color} | Linetype={e.Linetype}");
if (!string.IsNullOrEmpty(e.Description))
ed.WriteMessage($" [{e.Description}]");
}
ed.WriteMessage("\n");
}
// ── ADDBLOCK — command-line shortcut for block DB entries ────
[CommandMethod("ADDBLOCK")]
public void AddBlock()
{
var ed = Application.DocumentManager.MdiActiveDocument.Editor;
LogService.Info(SRC, "AddBlock()");
string blockName = PromptString(ed, "\nBlock name (e.g. * for any): ");
if (blockName == null) return;
string visState = PromptString(ed,
"\nVisibility state pattern (e.g. X_2.80_2.30_*): ");
if (visState == null) return;
string label = PromptString(ed, "\nDisplay label (e.g. X): ");
if (label == null) return;
string color = PromptString(ed, "\nColor index (default 11): ", allowEmpty: true);
if (string.IsNullOrWhiteSpace(color)) color = "11";
string linetype = PromptString(ed, "\nLinetype (default Continuous): ", allowEmpty: true);
if (string.IsNullOrWhiteSpace(linetype)) linetype = "Continuous";
string desc = PromptString(ed,
"\nDescription (optional – Enter to skip): ", allowEmpty: true) ?? "";
bool updated = BlockDatabase.AddOrUpdate(new Models.BlockEntry
{
BlockName = blockName, VisibilityState = visState,
DisplayLabel = label, Color = color, Linetype = linetype, Description = desc
});
ed.WriteMessage(updated
? $"\n[INFO] Updated \"{blockName}\"/\"{visState}\"."
: $"\n[INFO] Added \"{blockName}\"/\"{visState}\".");
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
ed.WriteMessage($"\n[INFO] Refreshed {refreshed} linked label field(s).");
if (PluginInit.Form?.Visible == true)
PluginInit.Form.BeginInvoke(new Action(() => PluginInit.Form.LoadBlockGrid()));
}
// ── REMOVEBLOCK ───────────────────────────────────────────────
[CommandMethod("REMOVEBLOCK")]
public void RemoveBlock()
{
var ed = Application.DocumentManager.MdiActiveDocument.Editor;
string bn = PromptString(ed, "\nBlock name to remove: ");
if (bn == null) return;
string vs = PromptString(ed, "\nVisibility state to remove: ");
if (vs == null) return;
bool ok = BlockDatabase.Remove(bn, vs);
ed.WriteMessage(ok ? $"\n[INFO] Removed \"{bn}\"/\"{vs}\"." : $"\n[WARN] Not found.");
if (ok)
{
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
ed.WriteMessage($"\n[INFO] Refreshed {refreshed} linked label field(s).");
}
if (ok && PluginInit.Form?.Visible == true)
PluginInit.Form.BeginInvoke(new Action(() => PluginInit.Form.LoadBlockGrid()));
}
// ── LISTBLOCKS ────────────────────────────────────────────────
[CommandMethod("LISTBLOCKS")]
public void ListBlocks()
{
var ed = Application.DocumentManager.MdiActiveDocument.Editor;
var entries = BlockDatabase.Load();
ed.WriteMessage($"\n DB: {BlockDatabase.DbPath}");
if (entries.Count == 0)
{ ed.WriteMessage("\n (empty — use ADDBLOCK or the panel Blocks tab)"); return; }
ed.WriteMessage($"\n── {entries.Count} entries ──");
foreach (var e in entries)
ed.WriteMessage($"\n {e.BlockName} / {e.VisibilityState} → {e.DisplayLabel}");
ed.WriteMessage("\n");
}
// ── LLUPDATEFIELDS — force-refresh all DB-linked MLeader labels ─
[CommandMethod("LLUPDATEFIELDS")]
public void UpdateLinkedLabelFields()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null) return;
LogService.Info(SRC, "LLUPDATEFIELDS");
int refreshed = LabelFieldUpdater.RefreshDocument(doc, quiet: false);
doc.Editor.WriteMessage($"\n[INFO] Refreshed {refreshed} linked label field(s).\n");
}
// ── SETUPDIMSTYLES — run integrated LSP property matching tool ─
[CommandMethod("SETUPDIMSTYLES")]
public void SetupDimStyles()
{
LogService.Info(SRC, "SetupDimStyles() -> integrated LSP command requested.");
LispIntegration.RunSetupOverriteDimStyles();
}
// ── LOADSETUPDIMSTYLES — reload the bundled LSP only ─────────
[CommandMethod("LOADSETUPDIMSTYLES")]
public void LoadSetupDimStyles()
{
LogService.Info(SRC, "LoadSetupDimStyles() -> integrated LSP reload requested.");
LispIntegration.EnsureLoaded();
}
// ─────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────
private static string PromptString(Editor ed, string prompt, bool allowEmpty = false)
{
var opt = new PromptStringOptions(prompt) { AllowSpaces = true };
var res = ed.GetString(opt);
if (res.Status != PromptStatus.OK) return null;
string v = res.StringResult?.Trim();
return (!allowEmpty && string.IsNullOrEmpty(v)) ? null : v;
}
}
}
// ===== FILE: Forms\LeaderStyleEditorControl.cs =====
using SysFont = System.Drawing.Font;
using SysColor = System.Drawing.Color;
using System;
using System.Windows.Forms;
using LayerLeaderPlugin.Core;
using LayerLeaderPlugin.Models;
namespace LayerLeaderPlugin.Forms
{
/// <summary>
/// Replicates AutoCAD's "Modify Multileader Style: DB" dialog as an
/// embeddable control with the same 3 sub-tabs: Leader Format,
/// Leader Structure, Content. Binds to a LeaderStyleConfig instance.
/// </summary>
public class LeaderStyleEditorControl : UserControl
{
public LeaderStyleConfig Config { get; private set; } = new LeaderStyleConfig();
public event EventHandler ConfigChanged;
private TabControl _subTabs;
// Leader Format
private ComboBox _cmbType, _cmbColor, _cmbLinetype, _cmbWeight, _cmbArrowSymbol;
private NumericUpDown _numArrowSize, _numBreakSize;
// Leader Structure
private CheckBox _chkMaxPoints, _chkFirstAngle, _chkSecondAngle,
_chkAutoLanding, _chkSetLandingDist, _chkAnnotative;
private NumericUpDown _numMaxPoints, _numFirstAngle, _numSecondAngle,
_numLandingDist, _numScale;
// Content
private ComboBox _cmbMlType, _cmbTextStyle, _cmbTextAngle, _cmbTextColor,
_cmbLeftAttach, _cmbRightAttach;
private TextBox _txtDefaultText;
private NumericUpDown _numTextHeight, _numLandingGap;
private CheckBox _chkLeftJustify, _chkFrameText, _chkHorizAttach, _chkExtendLeader;
private bool _suppressEvents;
public LeaderStyleEditorControl()
{
BuildUi();
}
// ── Public API ────────────────────────────────────────────────
public void LoadConfig(LeaderStyleConfig cfg)
{
Config = cfg.Clone();
PushToControls();
}
// ─────────────────────────────────────────────────────────────
private void BuildUi()
{
Dock = DockStyle.Fill;
BackColor = C3DTheme.Panel;
_subTabs = new TabControl
{
Dock = DockStyle.Fill,
Appearance = TabAppearance.FlatButtons,
BackColor = C3DTheme.Panel,
ForeColor = C3DTheme.Fg,
Font = C3DTheme.Normal
};
Controls.Add(_subTabs);
var tabFormat = new TabPage("Leader Format");
var tabStructure = new TabPage("Leader Structure");
var tabContent = new TabPage("Content");
C3DTheme.StyleTabPage(tabFormat);
C3DTheme.StyleTabPage(tabStructure);
C3DTheme.StyleTabPage(tabContent);
_subTabs.TabPages.Add(tabFormat);
_subTabs.TabPages.Add(tabStructure);
_subTabs.TabPages.Add(tabContent);
BuildFormatTab(tabFormat);
BuildStructureTab(tabStructure);
BuildContentTab(tabContent);
}
// ── Leader Format tab ────────────────────────────────────────
private void BuildFormatTab(TabPage page)
{
var grp = GroupedTable("General", 4, out var general);
_cmbType = LabeledCombo(general, 0, "Type:",
new[] { "Straight", "Spline" });
_cmbColor = LabeledCombo(general, 1, "Color:",
new[] { "ByBlock", "ByLayer", "1", "2", "3", "5", "7", "11" });
_cmbLinetype = LabeledCombo(general, 2, "Linetype:",
new[] { "ByBlock", "ByLayer", "Continuous" });
_cmbWeight = LabeledCombo(general, 3, "Lineweight:",
new[] { "ByBlock", "ByLayer" });
var grp2 = GroupedTable("Arrowhead", 2, out var arrow);
_cmbArrowSymbol = LabeledCombo(arrow, 0, "Symbol:",
System.Linq.Enumerable.ToArray(ArrowheadHelper.DisplayNames()));
_numArrowSize = LabeledNumeric(arrow, 1, "Size:", 0, 10, 0.01m);
var grp3 = GroupedTable("Leader break", 1, out var brk);
_numBreakSize = LabeledNumeric(brk, 0, "Break size:", 0, 10, 0.001m);
var stack = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = System.Windows.Forms.FlowDirection.TopDown,
AutoScroll = true, BackColor = C3DTheme.Panel, Padding = new Padding(10)
};
stack.Controls.Add(grp);
stack.Controls.Add(grp2);
stack.Controls.Add(grp3);
page.Controls.Add(stack);
WireChange(_cmbType, () => Config.LeaderType = _cmbType.Text);
WireChange(_cmbColor, () => Config.LineColor = _cmbColor.Text);
WireChange(_cmbLinetype, () => Config.Linetype = _cmbLinetype.Text);
WireChange(_cmbWeight, () => Config.LineWeight = _cmbWeight.Text);
WireChange(_cmbArrowSymbol, () => Config.ArrowSymbol = _cmbArrowSymbol.Text);
WireChange(_numArrowSize, () => Config.ArrowSize = (double)_numArrowSize.Value);
WireChange(_numBreakSize, () => Config.LeaderBreakSize = (double)_numBreakSize.Value);
}
// ── Leader Structure tab ─────────────────────────────────────
private void BuildStructureTab(TabPage page)
{
var grp = GroupedTable("Constraints", 3, out var t);
_chkMaxPoints = LabeledCheckNumeric(t, 0, "Maximum leader points",
out _numMaxPoints, 2, 10, 1m);
_chkFirstAngle = LabeledCheckNumeric(t, 1, "First segment angle",
out _numFirstAngle, 0, 360, 1m);
_chkSecondAngle = LabeledCheckNumeric(t, 2, "Second segment angle",
out _numSecondAngle, 0, 360, 1m);
var grp2 = GroupedTable("Landing settings", 3, out var land);
_chkAutoLanding = CheckOnly(land, 0, "Automatically include landing");
_chkSetLandingDist = LabeledCheckNumeric(land, 1, "Set landing distance",
out _numLandingDist, 0, 10, 0.01m);
var grp3 = GroupedTable("Scale", 2, out var sc);
_chkAnnotative = CheckOnly(sc, 0, "Annotative");
_numScale = LabeledNumeric(sc, 1, "Specify scale:", 0.01, 100, 0.1m);
var stack = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = System.Windows.Forms.FlowDirection.TopDown,
AutoScroll = true, BackColor = C3DTheme.Panel, Padding = new Padding(10)
};
stack.Controls.Add(grp);
stack.Controls.Add(grp2);
stack.Controls.Add(grp3);
page.Controls.Add(stack);
WireChange(_chkMaxPoints, () => Config.MaxLeaderPointsEnabled = _chkMaxPoints.Checked);
WireChange(_numMaxPoints, () => Config.MaxLeaderPoints = (int)_numMaxPoints.Value);
WireChange(_chkFirstAngle, () => Config.FirstSegAngleEnabled = _chkFirstAngle.Checked);
WireChange(_numFirstAngle, () => Config.FirstSegAngle = (double)_numFirstAngle.Value);
WireChange(_chkSecondAngle, () => Config.SecondSegAngleEnabled = _chkSecondAngle.Checked);
WireChange(_numSecondAngle, () => Config.SecondSegAngle = (double)_numSecondAngle.Value);
WireChange(_chkAutoLanding, () => Config.AutoIncludeLanding = _chkAutoLanding.Checked);
WireChange(_chkSetLandingDist, () => Config.SetLandingDistance = _chkSetLandingDist.Checked);
WireChange(_numLandingDist, () => Config.LandingDistance = (double)_numLandingDist.Value);
WireChange(_chkAnnotative, () => Config.Annotative = _chkAnnotative.Checked);
WireChange(_numScale, () => Config.Scale = (double)_numScale.Value);
}
// ── Content tab ──────────────────────────────────────────────
private void BuildContentTab(TabPage page)
{
var grp = GroupedTable("Text options", 6, out var t);
_cmbMlType = LabeledCombo(t, 0, "Multileader type:", new[] { "Mtext", "Block" });
var lbl = new Label { Text = "Default text:", Dock = DockStyle.Fill,
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleRight };
_txtDefaultText = C3DTheme.MakeTextBox();
t.Controls.Add(lbl, 0, 1); t.Controls.Add(_txtDefaultText, 1, 1);
_cmbTextStyle = LabeledCombo(t, 2, "Text style:", new[] { "K_ISOCP", "Standard" });
_cmbTextAngle = LabeledCombo(t, 3, "Text angle:", new[] { "Keep horizontal" });
_cmbTextColor = LabeledCombo(t, 4, "Text color:", new[] { "ByBlock", "ByLayer", "1", "2", "3" });
_numTextHeight = LabeledNumeric(t, 5, "Text height:", 0.01, 100, 0.1m);
var grp1b = GroupedTable("", 2, out var t1b);
_chkLeftJustify = CheckOnly(t1b, 0, "Always left justify");
_chkFrameText = CheckOnly(t1b, 1, "Frame text");
var grp2 = GroupedTable("Leader connection", 5, out var lc);
_chkHorizAttach = CheckOnly(lc, 0, "Horizontal attachment");
_cmbLeftAttach = LabeledCombo(lc, 1, "Left attachment:", AttachmentOptions);
_cmbRightAttach = LabeledCombo(lc, 2, "Right attachment:", AttachmentOptions);
_numLandingGap = LabeledNumeric(lc, 3, "Landing gap:", 0, 10, 0.01m);
_chkExtendLeader = CheckOnly(lc, 4, "Extend leader to text");
var stack = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = System.Windows.Forms.FlowDirection.TopDown,
AutoScroll = true, BackColor = C3DTheme.Panel, Padding = new Padding(10)
};
stack.Controls.Add(grp);
stack.Controls.Add(grp1b);
stack.Controls.Add(grp2);
page.Controls.Add(stack);
WireChange(_cmbMlType, () => Config.MultileaderType = _cmbMlType.Text);
WireChange(_txtDefaultText, () => Config.DefaultText = _txtDefaultText.Text);
WireChange(_cmbTextStyle, () => Config.TextStyle = _cmbTextStyle.Text);
WireChange(_cmbTextAngle, () => Config.TextAngle = _cmbTextAngle.Text);
WireChange(_cmbTextColor, () => Config.TextColor = _cmbTextColor.Text);
WireChange(_numTextHeight, () => Config.TextHeight = (double)_numTextHeight.Value);
WireChange(_chkLeftJustify, () => Config.AlwaysLeftJustify = _chkLeftJustify.Checked);
WireChange(_chkFrameText, () => Config.FrameText = _chkFrameText.Checked);
WireChange(_chkHorizAttach, () => Config.HorizontalAttachment = _chkHorizAttach.Checked);
WireChange(_cmbLeftAttach, () => Config.LeftAttachment = _cmbLeftAttach.Text);
WireChange(_cmbRightAttach, () => Config.RightAttachment = _cmbRightAttach.Text);
WireChange(_numLandingGap, () => Config.LandingGap = (double)_numLandingGap.Value);
WireChange(_chkExtendLeader, () => Config.ExtendLeaderToText = _chkExtendLeader.Checked);
}
// Only "Underline top line" (AttachmentBottomOfTopLine) is a proven
// enum member in this Civil 3D binding — see MLeaderStyleHelper.
private static readonly string[] AttachmentOptions =
{
"Underline top line"
};
// ── Push Config values into controls (on load) ──────────────
private void PushToControls()
{
_suppressEvents = true;
_cmbType.Text = Config.LeaderType;
_cmbColor.Text = Config.LineColor;
_cmbLinetype.Text = Config.Linetype;
_cmbWeight.Text = Config.LineWeight;
_cmbArrowSymbol.Text = Config.ArrowSymbol;
_numArrowSize.Value = ClampDecimal(_numArrowSize, (decimal)Config.ArrowSize);
_numBreakSize.Value = ClampDecimal(_numBreakSize, (decimal)Config.LeaderBreakSize);
_chkMaxPoints.Checked = Config.MaxLeaderPointsEnabled;
_numMaxPoints.Value = ClampDecimal(_numMaxPoints, Config.MaxLeaderPoints);
_chkFirstAngle.Checked = Config.FirstSegAngleEnabled;
_numFirstAngle.Value = ClampDecimal(_numFirstAngle, (decimal)Config.FirstSegAngle);
_chkSecondAngle.Checked = Config.SecondSegAngleEnabled;
_numSecondAngle.Value = ClampDecimal(_numSecondAngle, (decimal)Config.SecondSegAngle);
_chkAutoLanding.Checked = Config.AutoIncludeLanding;
_chkSetLandingDist.Checked = Config.SetLandingDistance;
_numLandingDist.Value = ClampDecimal(_numLandingDist, (decimal)Config.LandingDistance);
_chkAnnotative.Checked = Config.Annotative;
_numScale.Value = ClampDecimal(_numScale, (decimal)Config.Scale);
_cmbMlType.Text = Config.MultileaderType;
_txtDefaultText.Text = Config.DefaultText;
_cmbTextStyle.Text = Config.TextStyle;
_cmbTextAngle.Text = Config.TextAngle;
_cmbTextColor.Text = Config.TextColor;
_numTextHeight.Value = ClampDecimal(_numTextHeight, (decimal)Config.TextHeight);
_chkLeftJustify.Checked = Config.AlwaysLeftJustify;
_chkFrameText.Checked = Config.FrameText;
_chkHorizAttach.Checked = Config.HorizontalAttachment;
_cmbLeftAttach.Text = Config.LeftAttachment;
_cmbRightAttach.Text = Config.RightAttachment;
_numLandingGap.Value = ClampDecimal(_numLandingGap, (decimal)Config.LandingGap);
_chkExtendLeader.Checked = Config.ExtendLeaderToText;
_suppressEvents = false;
}
private static decimal ClampDecimal(NumericUpDown nud, decimal v) =>
Math.Max(nud.Minimum, Math.Min(nud.Maximum, v));
// ── Small control-builder helpers ────────────────────────────
// FIX: GroupBox previously had AutoSize=true *and* a fixed
// Width=460 while living inside a TopDown FlowLayoutPanel. That
// combination made WinForms collapse the box to its minimum
// content width (a few px), which squeezed the title text into
// a single vertical column of letters — the "C/e/n/t/e/r/..."
// garble seen in the screenshot. Explicit, non-auto-sized
// dimensions fix it.
private GroupBox GroupedTable(string title, int rows, out TableLayoutPanel table)
{
int rowHeight = 28;
int tableHeight = rows * rowHeight;
int boxHeight = tableHeight + 30; // header + padding
var grp = new GroupBox
{
Text = title,
ForeColor = C3DTheme.Fg,
BackColor = C3DTheme.Panel,
Font = C3DTheme.Small,
Width = 480,
Height = boxHeight,
Padding = new Padding(8, 14, 8, 8),
AutoSize = false,
Margin = new Padding(0, 0, 0, 8)
};
table = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = rows,
AutoSize = false,
BackColor = C3DTheme.Panel
};
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 55));
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45));
for (int i = 0; i < rows; i++)
table.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));
grp.Controls.Add(table);
return grp;
}
private ComboBox LabeledCombo(TableLayoutPanel t, int row, string label, string[] items)
{
var lbl = new Label { Text = label, Dock = DockStyle.Fill,
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleLeft };
var cmb = new ComboBox
{
Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDown,
BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg,
Font = C3DTheme.CodeSmall, FlatStyle = FlatStyle.Flat
};
cmb.Items.AddRange(items);
t.Controls.Add(lbl, 0, row);
t.Controls.Add(cmb, 1, row);
return cmb;
}
private NumericUpDown LabeledNumeric(TableLayoutPanel t, int row, string label,
double min, double max, decimal step)
{
var lbl = new Label { Text = label, Dock = DockStyle.Fill,
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleLeft };
var nud = new NumericUpDown
{
Dock = DockStyle.Fill, DecimalPlaces = 3,
Minimum = (decimal)min, Maximum = (decimal)max, Increment = step,
BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg, Font = C3DTheme.CodeSmall
};
t.Controls.Add(lbl, 0, row);
t.Controls.Add(nud, 1, row);
return nud;
}
private CheckBox CheckOnly(TableLayoutPanel t, int row, string label)
{
var chk = new CheckBox
{
Text = label, Dock = DockStyle.Fill, ForeColor = C3DTheme.Fg,
Font = C3DTheme.Small, AutoSize = false
};
t.Controls.Add(chk, 0, row);
t.SetColumnSpan(chk, 2);
return chk;
}
private CheckBox LabeledCheckNumeric(TableLayoutPanel t, int row, string label,
out NumericUpDown nud, double min, double max, decimal step)
{
var chk = new CheckBox
{
Text = label, Dock = DockStyle.Fill, ForeColor = C3DTheme.Fg,
Font = C3DTheme.Small
};
nud = new NumericUpDown
{
Dock = DockStyle.Fill, DecimalPlaces = 2,
Minimum = (decimal)min, Maximum = (decimal)max, Increment = step,
BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg, Font = C3DTheme.CodeSmall
};
t.Controls.Add(chk, 0, row);
t.Controls.Add(nud, 1, row);
return chk;
}
private void WireChange(Control c, Action apply)
{
void Handler(object s, EventArgs e)
{
if (_suppressEvents) return;
apply();
ConfigChanged?.Invoke(this, EventArgs.Empty);
}
if (c is ComboBox cmb) { cmb.SelectedIndexChanged += Handler; cmb.TextChanged += Handler; }
else if (c is NumericUpDown nud) nud.ValueChanged += Handler;
else if (c is CheckBox chk) chk.CheckedChanged += Handler;
else if (c is TextBox tb) tb.TextChanged += Handler;
}
}
}
// ===== FILE: Forms\BackgroundMaskDialog.cs =====
using SysFont = System.Drawing.Font;
using SysColor = System.Drawing.Color;
using SysSize = System.Drawing.Size;
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Colors;
using LayerLeaderPlugin.Core;
namespace LayerLeaderPlugin.Forms
{
/// <summary>
/// Native WinForms replica of Background_Mask.lsp's "Background Mask"
/// DCL dialog — same fields, same layout, same defaults:
/// ☑ Use Background Mask
/// Mask Offset: [1.00] [>>]
/// Fill Color: ☑ Transparent [colour swatch]
///
/// Used by all three Setup Styles sub-tabs (Text / Dimensions /
/// MLeader) so each can show and edit these settings inline without
/// launching the LISP's own dialog. "OK" applies the settings:
/// - MText/MLeader: written natively via BackgroundMaskHelper.
/// - Dimension: routed through the proven bmask LISP command,
/// since dimension masking needs raw XDATA writes with no
/// strongly-typed .NET equivalent in this binding.
/// </summary>
public class BackgroundMaskDialog : Form
{
public bool UseMask { get; private set; }
public double MaskOffset { get; private set; }
public bool Transparent { get; private set; }
public SysColor FillColor { get; private set; } = SysColor.Red; // ACI 1 fallback
private CheckBox _chkUseMask;
private TextBox _txtOffset;
private Button _btnPick;
private CheckBox _chkTransparent;
private Panel _swatch;
private Button _btnOk, _btnCancel;
/// <param name="useMask">Initial "Use Background Mask" state.</param>
/// <param name="offset">Initial Mask Offset value.</param>
/// <param name="transparent">Initial "Transparent" state.</param>
public BackgroundMaskDialog(bool useMask = true, double offset = 1.00, bool transparent = true)
{
UseMask = useMask;
MaskOffset = offset;
Transparent = transparent;
BuildUi();
PushToControls();
}
private void BuildUi()
{
Text = "Background Mask";
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
Size = new SysSize(300, 320);
BackColor = C3DTheme.Bg;
ForeColor = C3DTheme.Fg;
Font = C3DTheme.Normal;
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 5, ColumnCount = 1,
Padding = new Padding(12), BackColor = C3DTheme.Bg
};
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 30)); // Use Background Mask
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 10)); // spacer
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 84)); // Mask Offset group
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 90)); // Fill Color group
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); // OK/Cancel anchored bottom
Controls.Add(outer);
// ── Use Background Mask ──────────────────────────────────
_chkUseMask = new CheckBox
{
Text = "Use Background Mask", Dock = DockStyle.Fill,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Normal
};
_chkUseMask.CheckedChanged += (s, e) => UpdateEnabledState();
outer.Controls.Add(_chkUseMask, 0, 0);
// ── Mask Offset group ────────────────────────────────────
var grpOffset = new GroupBox
{
Text = "Mask Offset", Dock = DockStyle.Fill,
ForeColor = C3DTheme.Fg, BackColor = C3DTheme.Panel, Font = C3DTheme.Small
};
var offsetRow = new TableLayoutPanel
{
Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1,
Padding = new Padding(8, 18, 8, 8), BackColor = C3DTheme.Panel
};
offsetRow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
offsetRow.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 46));
_txtOffset = new TextBox
{
Dock = DockStyle.Fill, TextAlign = HorizontalAlignment.Center,
BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg, Font = C3DTheme.CodeSmall,
BorderStyle = BorderStyle.FixedSingle
};
_btnPick = C3DTheme.MakeButton(">>", "neutral", 40);
_btnPick.Click += (s, e) => DoPickOffset();
offsetRow.Controls.Add(_txtOffset, 0, 0);
offsetRow.Controls.Add(_btnPick, 1, 0);
grpOffset.Controls.Add(offsetRow);
outer.Controls.Add(grpOffset, 0, 2);
// ── Fill Color group ─────────────────────────────────────
var grpFill = new GroupBox
{
Text = "Fill Color", Dock = DockStyle.Fill,
ForeColor = C3DTheme.Fg, BackColor = C3DTheme.Panel, Font = C3DTheme.Small
};
var fillRow = new TableLayoutPanel
{
Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 1,
Padding = new Padding(8, 18, 8, 8), BackColor = C3DTheme.Panel
};
fillRow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
fillRow.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 50));
_chkTransparent = new CheckBox
{
Text = "Transparent", Dock = DockStyle.Fill,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Small
};
_chkTransparent.CheckedChanged += (s, e) => UpdateSwatch();
_swatch = new Panel
{
Dock = DockStyle.Fill, BorderStyle = BorderStyle.FixedSingle,
BackColor = FillColor
};
_swatch.Click += (s, e) => DoPickColor();
fillRow.Controls.Add(_chkTransparent, 0, 0);
fillRow.Controls.Add(_swatch, 1, 0);
grpFill.Controls.Add(fillRow);
outer.Controls.Add(grpFill, 0, 3);
// ── OK / Cancel ───────────────────────────────────────────
var btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Bottom, FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft,
Height = 36, BackColor = C3DTheme.Bg
};
_btnCancel = C3DTheme.MakeButton("Cancel", "neutral", 80);
_btnCancel.DialogResult = DialogResult.Cancel;
_btnOk = C3DTheme.MakeButton("OK", "ok", 80);
_btnOk.Click += BtnOk_Click;
btnRow.Controls.Add(_btnCancel);
btnRow.Controls.Add(_btnOk);
outer.Controls.Add(btnRow, 0, 4);
AcceptButton = _btnOk;
CancelButton = _btnCancel;
}
private void PushToControls()
{
_chkUseMask.Checked = UseMask;
_txtOffset.Text = MaskOffset.ToString("0.00");
_chkTransparent.Checked = Transparent;
_swatch.BackColor = FillColor;
UpdateEnabledState();
UpdateSwatch();
}
// Mirrors the LSP's fn3: when "Use Background Mask" is off, the
// offset and fill-color controls are disabled (greyed), matching
// the DCL's mode_tile behaviour exactly.
private void UpdateEnabledState()
{
bool en = _chkUseMask.Checked;
_txtOffset.Enabled = en;
_btnPick.Enabled = en;
_chkTransparent.Enabled = en;
_swatch.Enabled = en;
}
// Mirrors the LSP's fn1: swatch shows the fill colour only when
// not transparent; otherwise shows background (here: dialog bg).
private void UpdateSwatch()
{
_swatch.BackColor = _chkTransparent.Checked ? C3DTheme.Panel : FillColor;
}
private void DoPickOffset()
{
// The LSP's ">>" button lets the user pick two points in the
// drawing to measure a distance for the offset. That requires
// a live AutoCAD document; skip gracefully if none is active.
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
Hide();
try
{
using (doc.LockDocument())
{
var ed = doc.Editor;
var p1 = ed.GetPoint("\nSpecify first point: ");
if (p1.Status != Autodesk.AutoCAD.EditorInput.PromptStatus.OK) return;
var p2 = ed.GetPoint(new Autodesk.AutoCAD.EditorInput.PromptPointOptions(
"\nSpecify second point: ") { UseBasePoint = true, BasePoint = p1.Value });
if (p2.Status != Autodesk.AutoCAD.EditorInput.PromptStatus.OK) return;
double dist = p1.Value.DistanceTo(p2.Value);
_txtOffset.Text = dist.ToString("0.00");
}
}
finally
{
Show();
}
}
private void DoPickColor()
{
if (_chkTransparent.Checked) return; // swatch is inert while transparent
using var cd = new ColorDialog { Color = FillColor };
if (cd.ShowDialog(this) == DialogResult.OK)
{
FillColor = cd.Color;
UpdateSwatch();
}
}
private void BtnOk_Click(object sender, EventArgs e)
{
UseMask = _chkUseMask.Checked;
if (!double.TryParse(_txtOffset.Text, out double off) || off < 0)
{
MessageBox.Show("Mask Offset must be a valid non-negative number.",
"Validation", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
MaskOffset = off;
Transparent = _chkTransparent.Checked;
DialogResult = DialogResult.OK;
Close();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape) { DialogResult = DialogResult.Cancel; Close(); return true; }
return base.ProcessCmdKey(ref msg, keyData);
}
}
}
// ===== FILE: Forms\MainForm.cs =====
using SysFont = System.Drawing.Font;
using SysColor = System.Drawing.Color;
using SysBrushes = System.Drawing.Brushes;
using SysPen = System.Drawing.Pen;
using SysSolidBrush = System.Drawing.SolidBrush;
using SysSize = System.Drawing.Size;
using SysPoint = System.Drawing.Point;
using SysFlowDirection = System.Windows.Forms.FlowDirection;
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using LayerLeaderPlugin.Core;
using LayerLeaderPlugin.Models;
namespace LayerLeaderPlugin.Forms
{
/// <summary>
/// Main modeless tool window.
///
/// Top-level tabs:
/// Leader — embedded LeaderStyleEditorControl (3 sub-tabs matching
/// AutoCAD's Modify Multileader Style dialog) + MLeader preview.
/// Layer — sub-tabs: Database (Lines/Polylines/Splines/Arcs) | Log
/// Blocks — dynamic-block visibility-state label database + preview
///
/// Toolbar order (per latest request):
/// [Place Leader] [Add Layer] [Apply] [Setup Styles] [Refresh] [Log]
/// </summary>
public class MainForm : Form
{
// ── Toolbar ───────────────────────────────────────────────────
private Button _btnSetupDimStyles, _btnShowLog;
// ── Top-level tabs ───────────────────────────────────────────
private TabControl _topTabs;
private TabPage _tabLeader, _tabLayer, _tabBlocks, _tabSetupStyles, _tabLog;
// ── Leader tab ───────────────────────────────────────────────
private LeaderStyleEditorControl _leaderEditor;
private PictureBox _mlPreviewLeaderTab;
private string _previewLabel = "Default Text";
private Button _btnLeaderOk, _btnLeaderCancel, _btnLeaderApply, _btnLeaderUndo, _btnLeaderRefresh;
// ── Setup Styles tab ─────────────────────────────────────────
private SetupStylesControl _setupStylesControl;
// ── Layer tab (sub-tabs: Database | Log) ─────────────────────
// (Layer tab no longer uses a sub-tab control — see BuildLayerTab.)
private DataGridView _grid;
private TextBox _txtLayer, _txtLabel, _txtColor, _txtLinetype, _txtDesc;
private Button _btnOk, _btnCancel, _btnApply, _btnUndo, _btnActionRefresh, _btnDelete;
// Log tab content (shared rendering, lives under Layer tab)
private RichTextBox _logBox;
private Button _btnSaveLog, _btnCopyLog, _btnClearLog, _btnTogglePersist;
// ── Blocks tab ────────────────────────────────────────────────
private DataGridView _blockGrid;
private TextBox _txtBlockName, _txtVisState, _txtBlockLabel,
_txtBlockColor, _txtBlockLinetype, _txtBlockDesc;
private Button _btnBlockAdd, _btnBlockSave, _btnBlockUndo, _btnBlockRefresh,
_btnBlockDelete, _btnBlockPlaceLabel;
private PictureBox _mlPreviewBlocksTab;
private List<BlockEntry> _blockEntries = new List<BlockEntry>();
// Status bar
private Label _statusLabel;
// State
private List<LayerEntry> _entries = new List<LayerEntry>();
// ── Constructor ───────────────────────────────────────────────
public MainForm()
{
BuildUi();
WireLog();
LoadGrid();
LoadBlockGrid();
Text = $"Layer Leader – v{PluginVersion.Full}";
LogService.Info("MainForm", PluginVersion.Banner);
}
// ─────────────────────────────────────────────────────────────
// UI BUILDER
// ─────────────────────────────────────────────────────────────
private void BuildUi()
{
FormBorderStyle = FormBorderStyle.SizableToolWindow;
ShowInTaskbar = false;
StartPosition = FormStartPosition.Manual;
Size = new SysSize(980, 700);
MinimumSize = new SysSize(760, 520);
Location = new SysPoint(70, 60);
BackColor = C3DTheme.Bg;
ForeColor = C3DTheme.Fg;
Font = C3DTheme.Normal;
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 4, ColumnCount = 1,
BackColor = C3DTheme.Bg, Padding = new Padding(0)
};
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
Controls.Add(outer);
// Header
var header = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
BackColor = C3DTheme.HeaderBg, Padding = new Padding(10, 6, 10, 3)
};
header.RowStyles.Add(new RowStyle(SizeType.Absolute, 21));
header.RowStyles.Add(new RowStyle(SizeType.Absolute, 17));
header.Controls.Add(new Label
{
Dock = DockStyle.Fill, Text = $"Layer Leader — v{PluginVersion.Full}",
BackColor = C3DTheme.HeaderBg, ForeColor = C3DTheme.TextW,
Font = C3DTheme.Title, TextAlign = ContentAlignment.MiddleLeft
}, 0, 0);
header.Controls.Add(new Label
{
Dock = DockStyle.Fill, Text = $"DB: {LayerDatabase.DbPath}",
BackColor = C3DTheme.HeaderBg, ForeColor = C3DTheme.Muted,
Font = C3DTheme.Small, TextAlign = ContentAlignment.MiddleLeft
}, 0, 1);
outer.Controls.Add(header, 0, 0);
// ── Toolbar — trimmed to global-only actions. Place Leader,
// Add Layer, Apply, Undo, Refresh all live in each tab's
// own local action row (Leader / Layer / Blocks), so they
// are not duplicated up here anymore.
var toolbar = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = SysFlowDirection.LeftToRight,
BackColor = C3DTheme.HeaderBg, Padding = new Padding(8, 5, 0, 0)
};
_btnSetupDimStyles = C3DTheme.MakeButton("⚙ Setup Styles", "neutral", 128);
_btnShowLog = C3DTheme.MakeButton("▣ Log", "neutral", 74);
_btnSetupDimStyles.Click += (s, e) => _topTabs.SelectedTab = _tabSetupStyles;
_btnShowLog.Click += (s, e) => { _topTabs.SelectedTab = _tabLog; };
toolbar.Controls.AddRange(new Control[] { _btnSetupDimStyles, _btnShowLog });
outer.Controls.Add(toolbar, 0, 1);
// ── Top-level tabs ───────────────────────────────────────
_topTabs = new TabControl
{
Dock = DockStyle.Fill, Appearance = TabAppearance.FlatButtons,
BackColor = C3DTheme.Panel, ForeColor = C3DTheme.Fg, Font = C3DTheme.Normal
};
_tabLeader = new TabPage("Leader");
_tabLayer = new TabPage("Layer");
_tabBlocks = new TabPage("Blocks");
_tabSetupStyles = new TabPage("Setup Styles");
_tabLog = new TabPage("Log");
C3DTheme.StyleTabPage(_tabLeader);
C3DTheme.StyleTabPage(_tabLayer);
C3DTheme.StyleTabPage(_tabBlocks);
C3DTheme.StyleTabPage(_tabSetupStyles);
C3DTheme.StyleTabPage(_tabLog);
_topTabs.TabPages.Add(_tabLeader);
_topTabs.TabPages.Add(_tabLayer);
_topTabs.TabPages.Add(_tabBlocks);
_topTabs.TabPages.Add(_tabSetupStyles);
_topTabs.TabPages.Add(_tabLog);
outer.Controls.Add(_topTabs, 0, 2);
// Status bar
_statusLabel = new Label
{
Dock = DockStyle.Fill, Height = 24, BackColor = C3DTheme.HeaderBg,
ForeColor = C3DTheme.Muted, Font = C3DTheme.CodeSmall,
Text = $"v{PluginVersion.Full}", Padding = new Padding(8, 4, 0, 0)
};
outer.Controls.Add(_statusLabel, 0, 3);
BuildLeaderTab();
BuildLayerTab();
BuildBlocksTab();
BuildSetupStylesTab();
BuildLogTab();
}
// ═════════════════════════════════════════════════════════════
// LEADER TAB — style editor (3 sub-tabs) + MLeader preview
// ═════════════════════════════════════════════════════════════
private void BuildLeaderTab()
{
var split = new SplitContainer
{
Dock = DockStyle.Fill, Orientation = Orientation.Vertical,
SplitterDistance = 560, BackColor = C3DTheme.Border
};
split.Panel1.BackColor = C3DTheme.Panel;
split.Panel2.BackColor = C3DTheme.Panel;
_tabLeader.Controls.Add(split);
// LEFT: style editor + action row
var left = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
Padding = new Padding(8, 8, 6, 8), BackColor = C3DTheme.Panel
};
left.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
left.RowStyles.Add(new RowStyle(SizeType.Absolute, 72));
split.Panel1.Controls.Add(left);
_leaderEditor = new LeaderStyleEditorControl();
_leaderEditor.ConfigChanged += (s, e) =>
{
_previewLabel = string.IsNullOrWhiteSpace(_leaderEditor.Config.DefaultText)
? "Default Text" : _leaderEditor.Config.DefaultText;
_mlPreviewLeaderTab.Invalidate();
};
left.Controls.Add(_leaderEditor, 0, 0);
// Place Leader BEFORE Apply, per request — local action row
var btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = SysFlowDirection.LeftToRight,
WrapContents = true, Padding = new Padding(0, 4, 0, 0), BackColor = C3DTheme.Bg2
};
var btnPlaceLeaderLocal = C3DTheme.MakeButton("▶ Place Leader", "refresh", 108);
_btnLeaderOk = C3DTheme.MakeButton("OK", "ok", 62);
_btnLeaderCancel = C3DTheme.MakeButton("Cancel", "neutral", 70);
_btnLeaderApply = C3DTheme.MakeButton("Apply", "apply", 70);
_btnLeaderUndo = C3DTheme.MakeButton("Undo", "undo", 64);
_btnLeaderRefresh = C3DTheme.MakeButton("Refresh", "refresh", 76);
btnPlaceLeaderLocal.Click += (s, e) => StartPlaceLeader();
_btnLeaderOk.Click += (s, e) => { if (ApplyLeaderStyle()) _topTabs.SelectedTab = _tabLayer; };
_btnLeaderCancel.Click += (s, e) => LoadLeaderStyleIntoEditor();
_btnLeaderApply.Click += (s, e) => ApplyLeaderStyle();
_btnLeaderUndo.Click += (s, e) => LoadLeaderStyleIntoEditor();
_btnLeaderRefresh.Click += (s, e) => LoadLeaderStyleIntoEditor();
btnRow.Controls.AddRange(new Control[]
{ btnPlaceLeaderLocal, _btnLeaderOk, _btnLeaderCancel, _btnLeaderApply, _btnLeaderUndo, _btnLeaderRefresh });
left.Controls.Add(btnRow, 0, 1);
// RIGHT: MLeader preview
var right = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
Padding = new Padding(6, 8, 8, 8), BackColor = C3DTheme.Panel
};
right.RowStyles.Add(new RowStyle(SizeType.Absolute, 20));
right.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
split.Panel2.Controls.Add(right);
right.Controls.Add(new Label
{
Dock = DockStyle.Fill, Text = "MLeader preview (DB style)",
BackColor = C3DTheme.Panel, ForeColor = C3DTheme.Muted,
Font = C3DTheme.Small, TextAlign = ContentAlignment.MiddleLeft
}, 0, 0);
_mlPreviewLeaderTab = new PictureBox
{
Dock = DockStyle.Fill, BackColor = SysColor.Black, BorderStyle = BorderStyle.FixedSingle
};
_mlPreviewLeaderTab.Paint += (s, e) => DrawMLeaderPreview(e.Graphics, _mlPreviewLeaderTab, _previewLabel);
_mlPreviewLeaderTab.Resize += (s, e) => _mlPreviewLeaderTab.Invalidate();
right.Controls.Add(_mlPreviewLeaderTab, 0, 1);
// Load current drawing style into the editor on first show
LoadLeaderStyleIntoEditor();
}
private void LoadLeaderStyleIntoEditor()
{
try
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
LeaderStyleConfig cfg;
if (doc != null)
{
using (doc.LockDocument())
using (var tr = doc.Database.TransactionManager.StartTransaction())
{
cfg = MLeaderStyleHelper.ReadConfig(doc.Database, tr);
tr.Commit();
}
}
else cfg = MLeaderStyleHelper.DefaultConfig();
_leaderEditor.LoadConfig(cfg);
_previewLabel = string.IsNullOrWhiteSpace(cfg.DefaultText) ? "Default Text" : cfg.DefaultText;
_mlPreviewLeaderTab.Invalidate();
SetStatus("Leader style loaded from current drawing.");
LogService.Info("MainForm", "LoadLeaderStyleIntoEditor() -> config loaded.");
}
catch (Exception ex)
{
LogService.Error("MainForm", $"LoadLeaderStyleIntoEditor failed: {ex.Message}");
_leaderEditor.LoadConfig(MLeaderStyleHelper.DefaultConfig());
}
}
private bool ApplyLeaderStyle()
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return false; }
try
{
using (doc.LockDocument())
using (var tr = doc.Database.TransactionManager.StartTransaction())
{
MLeaderStyleHelper.WriteConfig(doc.Database, tr, _leaderEditor.Config);
tr.Commit();
}
SetStatus($"Leader style \"{MLeaderStyleHelper.StyleName}\" updated.");
LogService.Info("MainForm", "ApplyLeaderStyle() -> style written to drawing.");
return true;
}
catch (Exception ex)
{
LogService.Error("MainForm", $"ApplyLeaderStyle failed: {ex.Message}");
MessageBox.Show($"Failed to apply leader style:\n{ex.Message}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
// ═════════════════════════════════════════════════════════════
// LAYER TAB — Database (Lines/Polylines/Splines/Arcs)
//
// FIX: previously this tab had two sub-tabs, "Database" and
// "Log" — but Log being nested two levels deep under a top-level
// tab named "Layer" (which has nothing to do with logging) was
// confusing: the only way to see the log was to go into Layer
// first, which read as "the Layer tab is giving me Log content."
// Log is now its own top-level tab (see BuildLogTab below),
// reachable directly regardless of which other tab is active.
// The Layer tab itself no longer needs a sub-tab control since
// it only has one thing to show.
// ═════════════════════════════════════════════════════════════
private void BuildLayerTab()
{
BuildLayerDatabaseSubTab();
}
// ═════════════════════════════════════════════════════════════
// LOG TAB — top-level, reachable from anywhere
// ═════════════════════════════════════════════════════════════
private void BuildLogTab()
{
BuildLogSubTabContent(_tabLog);
}
private void BuildLayerDatabaseSubTab()
{
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 4, ColumnCount = 1,
Padding = new Padding(8, 8, 8, 8), BackColor = C3DTheme.Panel
};
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 20));
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 136));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 72));
_tabLayer.Controls.Add(outer);
// Header — preserves the "Database (Lines, Polylines, Splines,
// Arcs)" description that used to be this content's sub-tab
// title, now that the Layer tab no longer has a sub-tab control.
outer.Controls.Add(new Label
{
Dock = DockStyle.Fill,
Text = "Database — Lines, Polylines, Splines, Arcs",
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = ContentAlignment.MiddleLeft
}, 0, 0);
// Grid
_grid = new DataGridView
{
Dock = DockStyle.Fill, AutoGenerateColumns = false, AllowUserToAddRows = false,
AllowUserToDeleteRows = false, ReadOnly = true,
SelectionMode = DataGridViewSelectionMode.FullRowSelect, MultiSelect = false,
ColumnHeadersHeight = 24, RowHeadersVisible = false,
Font = C3DTheme.CodeSmall, RowTemplate = { Height = 24 }
};
C3DTheme.StyleGrid(_grid);
_grid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colLayer", HeaderText="Layer name",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=38 });
_grid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colLabel", HeaderText="Label",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=24 });
_grid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colColor", HeaderText="Color",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=10 });
_grid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colLinetype", HeaderText="Linetype",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=14 });
_grid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colDesc", HeaderText="Description",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=14 });
_grid.SelectionChanged += Grid_SelectionChanged;
_grid.CellDoubleClick += (s, e) =>
{
if (e.RowIndex >= 0 && e.RowIndex < _entries.Count)
OpenAddLayerDialog(_entries[e.RowIndex]);
};
outer.Controls.Add(_grid, 0, 1);
// Edit strip — wider label column fixes the text/field
// misalignment reported (labels were clipping at 80px / Linetype:).
var edit = new TableLayoutPanel
{
Dock = DockStyle.Fill, ColumnCount = 4, RowCount = 3,
Padding = new Padding(0, 7, 0, 2), BackColor = C3DTheme.Panel
};
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92));
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92));
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
for (int i = 0; i < 3; i++)
edit.RowStyles.Add(new RowStyle(SizeType.Absolute, 30));
_txtLayer = MkTb(); _txtLabel = MkTb();
_txtColor = MkTb(); _txtLinetype = MkTb();
_txtDesc = MkTb();
edit.Controls.Add(MkLbl("Layer:"), 0, 0); edit.Controls.Add(_txtLayer, 1, 0);
edit.Controls.Add(MkLbl("Label:"), 2, 0); edit.Controls.Add(_txtLabel, 3, 0);
edit.Controls.Add(MkLbl("Color:"), 0, 1); edit.Controls.Add(_txtColor, 1, 1);
edit.Controls.Add(MkLbl("Linetype:"), 2, 1); edit.Controls.Add(_txtLinetype, 3, 1);
edit.Controls.Add(MkLbl("Desc:"), 0, 2);
edit.SetColumnSpan(_txtDesc, 3);
edit.Controls.Add(_txtDesc, 1, 2);
outer.Controls.Add(edit, 0, 2);
// Action bar — sized to fit ALL 6 buttons (previous 38px row
// was too short and clipped Apply/Undo/Refresh off-screen).
var btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = SysFlowDirection.LeftToRight,
WrapContents = true,
Padding = new Padding(0, 4, 0, 0), BackColor = C3DTheme.Bg2
};
_btnOk = C3DTheme.MakeButton("OK", "ok", 62);
_btnCancel = C3DTheme.MakeButton("Cancel", "neutral", 70);
_btnApply = C3DTheme.MakeButton("Apply", "apply", 70);
_btnUndo = C3DTheme.MakeButton("Undo", "undo", 64);
_btnActionRefresh = C3DTheme.MakeButton("Refresh", "refresh", 76);
_btnDelete = C3DTheme.MakeButton("Delete", "danger", 68);
_btnOk.Click += (s, e) => { if (TryApplyCurrentEntry()) Hide(); };
_btnCancel.Click += (s, e) => { UndoCurrentEdit(); Hide(); };
_btnApply.Click += BtnApply_Click;
_btnUndo.Click += (s, e) => UndoCurrentEdit();
_btnActionRefresh.Click += (s, e) => LoadGrid(_txtLayer.Text.Trim());
_btnDelete.Click += BtnDelete_Click;
btnRow.Controls.AddRange(new Control[]
{ _btnOk, _btnCancel, _btnApply, _btnUndo, _btnActionRefresh, _btnDelete });
outer.Controls.Add(btnRow, 0, 3);
}
private void BuildLogSubTabContent(TabPage targetPage)
{
var layout = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
Padding = new Padding(8), BackColor = C3DTheme.Panel
};
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 34));
targetPage.Controls.Add(layout);
_logBox = new RichTextBox
{
Dock = DockStyle.Fill, ReadOnly = true,
BackColor = C3DTheme.CodeBg, ForeColor = C3DTheme.CodeFg,
Font = C3DTheme.CodeSmall, ScrollBars = RichTextBoxScrollBars.Vertical,
BorderStyle = BorderStyle.FixedSingle
};
layout.Controls.Add(_logBox, 0, 0);
var btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = SysFlowDirection.LeftToRight,
Padding = new Padding(0, 4, 0, 0), BackColor = C3DTheme.Bg2
};
_btnSaveLog = C3DTheme.MakeButton("Store log", "neutral", 110);
_btnCopyLog = C3DTheme.MakeButton("📋 Copy", "neutral", 90);
_btnClearLog = C3DTheme.MakeButton("Clear", "danger", 62);
_btnTogglePersist = C3DTheme.MakeButton("Auto-persist: OFF", "neutral", 138);
_btnSaveLog.Click += (s, e) =>
{
string p = LogService.SaveLogSnapshot();
SetStatus($"Saved → {p}");
};
_btnCopyLog.Click += (s, e) =>
{
try
{
if (_logBox.Text.Length > 0)
{
Clipboard.SetText(_logBox.Text);
SetStatus("Log copied to clipboard.");
}
else
{
SetStatus("Log is empty — nothing to copy.");
}
}
catch (System.Exception ex)
{
SetStatus($"Copy failed: {ex.Message}");
}
};
_btnClearLog.Click += (s, e) => { LogService.Clear(); _logBox.Clear(); };
_btnTogglePersist.Click += (s, e) =>
{
LogService.PersistToDisk = !LogService.PersistToDisk;
_btnTogglePersist.Text = $"Auto-persist: {(LogService.PersistToDisk?"ON":"OFF")}";
C3DTheme.StyleButton(_btnTogglePersist, LogService.PersistToDisk ? "success" : "neutral");
};
btnRow.Controls.AddRange(new Control[]
{ _btnSaveLog, _btnCopyLog, _btnClearLog, _btnTogglePersist });
layout.Controls.Add(btnRow, 0, 1);
}
// ═════════════════════════════════════════════════════════════
// BLOCKS TAB — dynamic block visibility-state label database
// ═════════════════════════════════════════════════════════════
private void BuildBlocksTab()
{
var split = new SplitContainer
{
Dock = DockStyle.Fill, Orientation = Orientation.Vertical,
SplitterDistance = 560, BackColor = C3DTheme.Border
};
split.Panel1.BackColor = C3DTheme.Panel;
split.Panel2.BackColor = C3DTheme.Panel;
_tabBlocks.Controls.Add(split);
var left = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 3, ColumnCount = 1,
Padding = new Padding(8, 8, 6, 8), BackColor = C3DTheme.Panel
};
left.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
left.RowStyles.Add(new RowStyle(SizeType.Absolute, 168));
left.RowStyles.Add(new RowStyle(SizeType.Absolute, 72));
split.Panel1.Controls.Add(left);
// Grid
_blockGrid = new DataGridView
{
Dock = DockStyle.Fill, AutoGenerateColumns = false, AllowUserToAddRows = false,
AllowUserToDeleteRows = false, ReadOnly = true,
SelectionMode = DataGridViewSelectionMode.FullRowSelect, MultiSelect = false,
ColumnHeadersHeight = 24, RowHeadersVisible = false,
Font = C3DTheme.CodeSmall, RowTemplate = { Height = 24 }
};
C3DTheme.StyleGrid(_blockGrid);
_blockGrid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colBlock", HeaderText="Block name",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=22 });
_blockGrid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colVis", HeaderText="Visibility state",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=28 });
_blockGrid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colBLabel", HeaderText="Label",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=16 });
_blockGrid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colBColor", HeaderText="Color",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=10 });
_blockGrid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colBLinetype", HeaderText="Linetype",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=12 });
_blockGrid.Columns.Add(new DataGridViewTextBoxColumn
{ Name="colBDesc", HeaderText="Description",
AutoSizeMode=DataGridViewAutoSizeColumnMode.Fill, FillWeight=12 });
_blockGrid.SelectionChanged += BlockGrid_SelectionChanged;
_blockGrid.CellDoubleClick += (s, e) =>
{
if (e.RowIndex >= 0 && e.RowIndex < _blockEntries.Count)
OpenAddBlockDialog(_blockEntries[e.RowIndex]);
};
left.Controls.Add(_blockGrid, 0, 0);
// Edit strip
var edit = new TableLayoutPanel
{
Dock = DockStyle.Fill, ColumnCount = 4, RowCount = 4,
Padding = new Padding(0, 7, 0, 2), BackColor = C3DTheme.Panel
};
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 100));
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92));
edit.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
for (int i = 0; i < 4; i++)
edit.RowStyles.Add(new RowStyle(SizeType.Absolute, 30));
_txtBlockName = MkTb(); _txtVisState = MkTb();
_txtBlockLabel = MkTb(); _txtBlockColor = MkTb();
_txtBlockLinetype = MkTb(); _txtBlockDesc = MkTb();
edit.Controls.Add(MkLbl("Block:"), 0, 0); edit.Controls.Add(_txtBlockName, 1, 0);
edit.Controls.Add(MkLbl("Vis state:"),2, 0); edit.Controls.Add(_txtVisState, 3, 0);
edit.Controls.Add(MkLbl("Label:"), 0, 1); edit.Controls.Add(_txtBlockLabel, 1, 1);
edit.Controls.Add(MkLbl("Color:"), 2, 1); edit.Controls.Add(_txtBlockColor, 3, 1);
edit.Controls.Add(MkLbl("Linetype:"), 0, 2); edit.Controls.Add(_txtBlockLinetype, 1, 2);
edit.Controls.Add(MkLbl("Desc:"), 2, 2); edit.Controls.Add(_txtBlockDesc, 3, 2);
left.Controls.Add(edit, 0, 1);
var btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = SysFlowDirection.LeftToRight,
WrapContents = true, Padding = new Padding(0, 4, 0, 0), BackColor = C3DTheme.Bg2
};
_btnBlockPlaceLabel = C3DTheme.MakeButton("▶ Place on Block", "refresh", 120);
_btnBlockAdd = C3DTheme.MakeButton("+ Add", "primary", 64);
_btnBlockSave = C3DTheme.MakeButton("Save", "apply", 62);
_btnBlockUndo = C3DTheme.MakeButton("Undo", "undo", 64);
_btnBlockRefresh = C3DTheme.MakeButton("Refresh", "refresh", 76);
_btnBlockDelete = C3DTheme.MakeButton("Delete", "danger", 68);
_btnBlockPlaceLabel.Click += (s, e) => StartPlaceLabelOnBlock();
_btnBlockAdd.Click += (s, e) => OpenAddBlockDialog(null);
_btnBlockSave.Click += BtnBlockSave_Click;
_btnBlockUndo.Click += (s, e) => UndoCurrentBlockEdit();
_btnBlockRefresh.Click += (s, e) => LoadBlockGrid();
_btnBlockDelete.Click += BtnBlockDelete_Click;
btnRow.Controls.AddRange(new Control[]
{ _btnBlockPlaceLabel, _btnBlockAdd, _btnBlockSave, _btnBlockUndo, _btnBlockRefresh, _btnBlockDelete });
left.Controls.Add(btnRow, 0, 2);
// RIGHT: preview (per request — same MLeader preview, also on Blocks tab)
var right = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
Padding = new Padding(6, 8, 8, 8), BackColor = C3DTheme.Panel
};
right.RowStyles.Add(new RowStyle(SizeType.Absolute, 20));
right.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
split.Panel2.Controls.Add(right);
right.Controls.Add(new Label
{
Dock = DockStyle.Fill, Text = "MLeader preview (DB style)",
BackColor = C3DTheme.Panel, ForeColor = C3DTheme.Muted,
Font = C3DTheme.Small, TextAlign = ContentAlignment.MiddleLeft
}, 0, 0);
_mlPreviewBlocksTab = new PictureBox
{
Dock = DockStyle.Fill, BackColor = SysColor.Black, BorderStyle = BorderStyle.FixedSingle
};
_mlPreviewBlocksTab.Paint += (s, e) =>
DrawMLeaderPreview(e.Graphics, _mlPreviewBlocksTab, _txtBlockLabel.Text);
_mlPreviewBlocksTab.Resize += (s, e) => _mlPreviewBlocksTab.Invalidate();
right.Controls.Add(_mlPreviewBlocksTab, 0, 1);
}
// ─────────────────────────────────────────────────────────────
// SHARED MLEADER PREVIEW PAINT (used by both Leader and Blocks tabs)
// Draws a to-scale diagram of the DB-style MLeader: straight
// line, no arrowhead, underline-top-line attachment, dogleg,
// landing, label text — exactly as it will look in model space.
// ─────────────────────────────────────────────────────────────
private void DrawMLeaderPreview(Graphics g, PictureBox box, string labelText)
{
float pw = box.ClientSize.Width;
float ph = box.ClientSize.Height;
g.Clear(SysColor.Black);
if (pw < 10 || ph < 10) return;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
float margin = 14f;
float arrowX = margin + 30f;
float arrowY = ph * 0.62f;
float doglegLen = pw * 0.22f;
float riseLen = ph * 0.28f;
float textX = pw - margin - 10f;
float doglegEndX = textX - doglegLen;
float doglegY = arrowY - riseLen;
var leaderColor = SysColor.FromArgb(220, 80, 120);
using var leaderPen = new SysPen(leaderColor, 1.5f);
g.DrawLine(leaderPen, arrowX, arrowY, doglegEndX, doglegY);
g.DrawLine(leaderPen, doglegEndX, doglegY, textX, doglegY);
string label = string.IsNullOrWhiteSpace(labelText) ? "Default Text" : labelText;
using var tf = new SysFont("Arial", 11f, FontStyle.Regular, GraphicsUnit.Pixel);
var sz = g.MeasureString(label, tf);
float tdx = textX - sz.Width;
float tdy = doglegY - sz.Height - 1f;
using var tb = new SysSolidBrush(leaderColor);
g.DrawString(label, tf, tb, tdx, tdy);
g.DrawLine(leaderPen, tdx, doglegY, textX, doglegY);
using var gapPen = new SysPen(SysColor.FromArgb(80, 80, 80), 1f)
{ DashStyle = System.Drawing.Drawing2D.DashStyle.Dot };
g.DrawLine(gapPen, doglegEndX, doglegY - 6f, doglegEndX, doglegY + 6f);
var cfg = _leaderEditor?.Config ?? MLeaderStyleHelper.DefaultConfig();
using var captionF = new SysFont("Segoe UI", 7f);
using var captionB = new SysSolidBrush(SysColor.FromArgb(90, 90, 90));
g.DrawString(
$"Style: {MLeaderStyleHelper.StyleName} | Layer: {MLeaderStyleHelper.LayerName} | " +
$"No arrow | Dogleg: {cfg.LandingDistance:0.00} | Gap: {cfg.LandingGap:0.00} | H: {cfg.TextHeight:0.00}",
captionF, captionB, margin, ph - 18f);
}
// ─────────────────────────────────────────────────────────────
// PLACE LEADER WORKFLOW (Layer database, line/polyline/spline/arc)
// ─────────────────────────────────────────────────────────────
private void StartPlaceLeader()
{
// Unified entry point: handles Lines/Polylines/Splines/Arcs
// (Layer database) AND block references (Blocks database) in
// one prompt, so the Leader tab's "Place Leader" button works
// for both — no need to switch to the Blocks tab separately.
SetStatus("Click a Line, Polyline, Spline, Arc, or Block in the viewport…");
LogService.Info("MainForm", "StartPlaceLeader()");
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
using (doc.LockDocument())
{
var db = doc.Database;
var ed = doc.Editor;
var selOpts = new PromptEntityOptions(
"\nPLACELEADER — Select Line, Polyline, Spline, Arc, or Block: ");
selOpts.SetRejectMessage("\nMust be a Line, Polyline, Spline, Arc, or Block reference.");
selOpts.AddAllowedClass(typeof(Line), false);
selOpts.AddAllowedClass(typeof(Polyline), false);
selOpts.AddAllowedClass(typeof(Polyline2d), false);
selOpts.AddAllowedClass(typeof(Polyline3d), false);
selOpts.AddAllowedClass(typeof(Spline), false);
selOpts.AddAllowedClass(typeof(Arc), false);
selOpts.AddAllowedClass(typeof(BlockReference), false);
var selRes = ed.GetEntity(selOpts);
if (selRes.Status != PromptStatus.OK) { SetStatus("Selection cancelled."); return; }
string label;
string sourceKind;
ObjectId sourceObjectId = selRes.ObjectId;
using (var tr = db.TransactionManager.StartTransaction())
{
var ent = tr.GetObject(selRes.ObjectId, OpenMode.ForRead) as Entity;
if (ent is BlockReference br)
{
sourceKind = LabelFieldUpdater.SourceBlock;
// ── Block path: resolve via BlockDatabase ─────
string blockName = BlockInspector.GetEffectiveName(br, tr);
string visState = BlockInspector.GetVisibilityState(br) ?? "(none)";
LogService.Info("MainForm", $"Block: \"{blockName}\" VisState: \"{visState}\"");
var bEntry = BlockDatabase.GetEntry(blockName, visState);
label = bEntry?.DisplayLabel ?? visState;
if (bEntry == null)
ed.WriteMessage($"\n[WARN] No block DB entry for \"{blockName}\"/\"{visState}\". Using state name.");
else
ed.WriteMessage($"\n[INFO] Label: \"{label}\"");
_txtBlockName.Text = blockName;
_txtVisState.Text = visState;
_txtBlockLabel.Text = label;
SelectBlockGridRow(blockName, visState);
}
else
{
sourceKind = LabelFieldUpdater.SourceLayer;
// ── Line/Polyline/Spline/Arc path: LayerDatabase ─
string layerName = ent?.Layer ?? "";
LogService.Info("MainForm", $"Layer: \"{layerName}\"");
var lEntry = LayerDatabase.GetEntry(layerName);
label = lEntry?.DisplayLabel ?? layerName;
if (lEntry == null)
ed.WriteMessage($"\n[WARN] No DB entry for \"{layerName}\". Using raw name.");
else
ed.WriteMessage($"\n[INFO] Label: \"{label}\"");
SelectGridRowByLayer(layerName);
}
tr.Commit();
}
_previewLabel = label;
_mlPreviewLeaderTab.Invalidate();
_mlPreviewBlocksTab.Invalidate();
SetStatus($"→ \"{label}\"");
var arrowRes = ed.GetPoint(
new PromptPointOptions("\nArrowhead point: ") { UseBasePoint = false });
if (arrowRes.Status != PromptStatus.OK) { SetStatus("Cancelled."); return; }
// UCS-to-WCS transform: GetPoint returns the picked point
// expressed in the CURRENT UCS, but MLeader/MText geometry
// is always stored in WCS. Transform every picked point
// through the editor's current UCS-to-WCS matrix — this
// works for whichever UCS is current (Lageplan, UCS_002,
// or World), not just World.
Matrix3d ucsToWcs = ed.CurrentUserCoordinateSystem;
Point3d arrowPt = arrowRes.Value.TransformBy(ucsToWcs);
var textRes = ed.GetPoint(new PromptPointOptions("\nText landing point: ")
{ UseBasePoint = true, BasePoint = arrowRes.Value })
;
if (textRes.Status != PromptStatus.OK) { SetStatus("Cancelled."); return; }
Point3d textPt = textRes.Value.TransformBy(ucsToWcs);
// REVERTED: an earlier version derived text rotation from
// the active UCS's X-axis angle, on the assumption that
// text should visually align with a rotated UCS (e.g.
// "Lageplan") the way AutoCAD's native TEXT/MTEXT commands
// do. A real-drawing screenshot under "Lageplan" (UCS
// rotated diagonally relative to World) showed that
// assumption was wrong for this drawing's convention:
// every other label in the sheet (e.g. "KK Gr.", "LTS")
// stays perfectly horizontal in WCS regardless of which
// UCS is current, and the UCS-derived rotation made our
// label rotate diagonally to match the UCS instead —
// visually inconsistent with everything else on the
// sheet. Position is still correctly transformed UCS→WCS
// above; rotation is now always 0 (horizontal in WCS),
// matching the drawing's actual labelling convention.
PlaceMLeader(db, arrowPt, textPt, label, ucsRotation: 0.0,
sourceObjectId: sourceObjectId, sourceKind: sourceKind);
ed.WriteMessage($"\n[INFO] Dynamic label field placed: \"{label}\"\n");
SetStatus($"Dynamic label field placed: \"{label}\"");
}
}
// ─────────────────────────────────────────────────────────────
// PLACE LABEL ON BLOCK WORKFLOW (Blocks tab)
// ─────────────────────────────────────────────────────────────
private void StartPlaceLabelOnBlock()
{
SetStatus("Click a dynamic block in the viewport…");
LogService.Info("MainForm", "StartPlaceLabelOnBlock()");
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
using (doc.LockDocument())
{
var db = doc.Database;
var ed = doc.Editor;
var selOpts = new PromptEntityOptions("\nPLACE LABEL ON BLOCK — Select a block reference: ");
selOpts.SetRejectMessage("\nMust be a block reference.");
selOpts.AddAllowedClass(typeof(BlockReference), false);
var selRes = ed.GetEntity(selOpts);
if (selRes.Status != PromptStatus.OK) { SetStatus("Selection cancelled."); return; }
string blockName, visState;
using (var tr = db.TransactionManager.StartTransaction())
{
var br = tr.GetObject(selRes.ObjectId, OpenMode.ForRead) as BlockReference;
blockName = BlockInspector.GetEffectiveName(br, tr);
visState = BlockInspector.GetVisibilityState(br) ?? "(none)";
tr.Commit();
}
LogService.Info("MainForm", $"Block: \"{blockName}\" VisState: \"{visState}\"");
var entry = BlockDatabase.GetEntry(blockName, visState);
string label = entry?.DisplayLabel ?? visState;
if (entry == null)
ed.WriteMessage($"\n[WARN] No block DB entry for \"{blockName}\"/\"{visState}\". Using state name.");
else
ed.WriteMessage($"\n[INFO] Label: \"{label}\"");
_txtBlockLabel.Text = label;
_txtBlockName.Text = blockName;
_txtVisState.Text = visState;
_mlPreviewBlocksTab.Invalidate();
SelectBlockGridRow(blockName, visState);
SetStatus($"Block: {blockName} / {visState} → \"{label}\"");
var arrowRes = ed.GetPoint(
new PromptPointOptions("\nArrowhead point: ") { UseBasePoint = false });
if (arrowRes.Status != PromptStatus.OK) { SetStatus("Cancelled."); return; }
// UCS-to-WCS POSITION fix (still needed and correct):
// GetPoint returns the picked point in the CURRENT UCS,
// but MLeader/MText geometry is stored in WCS — transform
// through ed.CurrentUserCoordinateSystem regardless of
// which UCS is active.
Matrix3d ucsToWcs = ed.CurrentUserCoordinateSystem;
Point3d arrowPt = arrowRes.Value.TransformBy(ucsToWcs);
var textRes = ed.GetPoint(new PromptPointOptions("\nText landing point: ")
{ UseBasePoint = true, BasePoint = arrowRes.Value });
if (textRes.Status != PromptStatus.OK) { SetStatus("Cancelled."); return; }
Point3d textPt = textRes.Value.TransformBy(ucsToWcs);
// Text rotation is ALWAYS 0 (horizontal in WCS), matching
// every other label already in the drawing — see the
// removed GetUcsRotationAngle() for why following the
// UCS's rotation here was wrong (confirmed against a
// screenshot under a 38.7° rotated "Lageplan" UCS: every
// existing label, including ones placed by other tools,
// stays horizontal regardless of which UCS is current).
PlaceMLeader(db, arrowPt, textPt, label, ucsRotation: 0.0,
sourceObjectId: selRes.ObjectId, sourceKind: LabelFieldUpdater.SourceBlock);
ed.WriteMessage($"\n[INFO] Dynamic label field placed on block: \"{label}\"\n");
SetStatus($"Dynamic label field placed: \"{label}\"");
}
}
// ── Shared MLeader placement (used by both workflows) ──────────
/// <param name="ucsRotation">
/// Text rotation (radians, WCS). Both call sites in this file
/// always pass 0.0 — text is placed horizontal in WCS regardless
/// of the active UCS. An earlier version derived this from the
/// current UCS's rotation (so text would visually align with a
/// rotated UCS like "Lageplan"), based on an unverified guess
/// about what was being requested. A screenshot under a 38.7°
/// rotated UCS proved that guess wrong: every existing label in
/// the drawing — including labels not placed by this plugin —
/// stays horizontal no matter which UCS is current, so that's
/// what this method does too. The parameter is kept (rather than
/// removed outright) only in case a genuine "follow the UCS"
/// requirement is confirmed later; until then, always pass 0.0.
/// </param>
private void PlaceMLeader(Database db, Point3d arrowPt, Point3d textPt, string label,
double ucsRotation = 0.0,
ObjectId sourceObjectId = default,
string sourceKind = null)
{
using (var tr = db.TransactionManager.StartTransaction())
{
var bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
var btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite)
as BlockTableRecord;
ObjectId styleId = MLeaderStyleHelper.EnsureStyle(db, tr);
var ml = new MLeader();
ml.SetDatabaseDefaults();
int li = ml.AddLeader();
int lli = ml.AddLeaderLine(li);
ml.SetFirstVertex(lli, arrowPt);
ml.AddLastVertex (lli, textPt);
var mt = new MText();
mt.SetDatabaseDefaults();
mt.Contents = label;
mt.TextHeight = MLeaderStyleHelper.TextHeight;
mt.Location = textPt;
// Always horizontal (rotation = 0) regardless of UCS —
// see the param doc above for why.
mt.Rotation = ucsRotation;
BackgroundMaskHelper.ApplyToMText(mt); // Use Mask=on, Offset=1.00, Transparent
ml.MText = mt;
btr.AppendEntity(ml);
tr.AddNewlyCreatedDBObject(ml, true);
MLeaderStyleHelper.ApplyStyle(ml, styleId, db, tr);
if (!sourceObjectId.IsNull && !string.IsNullOrWhiteSpace(sourceKind))
LabelFieldUpdater.AttachLink(ml, tr, sourceObjectId, sourceKind);
tr.Commit();
}
LogService.Info("MainForm",
$"PlaceMLeader() -> placed: \"{label}\" rotation={ucsRotation * 180.0 / Math.PI:0.##}°");
}
// ═════════════════════════════════════════════════════════════
// SETUP STYLES TAB — Text / Dimensions / MLeader sub-tabs
// ═════════════════════════════════════════════════════════════
private void BuildSetupStylesTab()
{
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
Padding = new Padding(0), BackColor = C3DTheme.Panel
};
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
_tabSetupStyles.Controls.Add(outer);
_setupStylesControl = new SetupStylesControl();
outer.Controls.Add(_setupStylesControl, 0, 0);
// Legacy LISP "Property Matching Workspace Manager" remains
// available as an alternative — it additionally shows
// current-vs-selected entity COUNTS per layer/style, which
// the native picker above does not (yet) replicate.
var legacyRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = SysFlowDirection.LeftToRight,
Padding = new Padding(8, 4, 0, 0), BackColor = C3DTheme.Bg2
};
var btnLegacy = C3DTheme.MakeButton(
"⚙ Legacy LISP Matcher (with counts)", "neutral", 250);
btnLegacy.Click += (s, e) => RunSetupDimStyles();
legacyRow.Controls.Add(btnLegacy);
outer.Controls.Add(legacyRow, 0, 1);
}
// ─────────────────────────────────────────────────────────────
// INTEGRATED LSP WORKFLOW
// ─────────────────────────────────────────────────────────────
private void RunSetupDimStyles()
{
SetStatus($"Starting {LispIntegration.CommandName}…");
LogService.Info("MainForm", $"RunSetupDimStyles() -> {LispIntegration.CommandName}");
LispIntegration.RunSetupOverriteDimStyles();
}
// ─────────────────────────────────────────────────────────────
// ADD / EDIT LAYER DIALOG
// ─────────────────────────────────────────────────────────────
private void OpenAddLayerDialog(LayerEntry existing)
{
using var dlg = new AddLayerForm(existing);
if (dlg.ShowDialog(this) == DialogResult.OK && dlg.Result != null)
{
LayerDatabase.AddOrUpdate(dlg.Result);
LoadGrid();
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
SetStatus($"Saved: \"{dlg.Result.LayerName}\" | refreshed {refreshed} label field(s)");
}
}
private void OpenAddBlockDialog(BlockEntry existing)
{
using var dlg = new AddBlockForm(existing);
if (dlg.ShowDialog(this) == DialogResult.OK && dlg.Result != null)
{
BlockDatabase.AddOrUpdate(dlg.Result);
LoadBlockGrid();
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
SetStatus($"Saved: \"{dlg.Result.BlockName}\" / \"{dlg.Result.VisibilityState}\" | refreshed {refreshed} label field(s)");
}
}
// ═════════════════════════════════════════════════════════════
// LAYER GRID
// ═════════════════════════════════════════════════════════════
internal void LoadGrid(string selectLayerName = null)
{
_entries = LayerDatabase.Load();
_grid.Rows.Clear();
foreach (var e in _entries)
_grid.Rows.Add(e.LayerName, e.DisplayLabel, e.Color ?? "", e.Linetype ?? "", e.Description ?? "");
bool selected = !string.IsNullOrWhiteSpace(selectLayerName) && SelectGridRowByLayer(selectLayerName);
if (!selected && _grid.Rows.Count > 0)
{
_grid.ClearSelection();
_grid.Rows[0].Selected = true;
_grid.CurrentCell = _grid.Rows[0].Cells[0];
Grid_SelectionChanged(_grid, EventArgs.Empty);
}
else if (_grid.Rows.Count == 0)
{
_txtLayer.Clear(); _txtLabel.Clear(); _txtColor.Clear();
_txtLinetype.Clear(); _txtDesc.Clear();
_previewLabel = "Default Text";
_mlPreviewLeaderTab?.Invalidate();
}
LogService.Info("MainForm", $"LoadGrid() -> {_entries.Count} entries from {LayerDatabase.DbPath}");
SetStatus($"{_entries.Count} entries | {LayerDatabase.DbPath}");
}
private void Grid_SelectionChanged(object sender, EventArgs e)
{
if (_grid.SelectedRows.Count == 0) return;
var row = _grid.SelectedRows[0];
_txtLayer.Text = row.Cells["colLayer"].Value?.ToString() ?? "";
_txtLabel.Text = row.Cells["colLabel"].Value?.ToString() ?? "";
_txtColor.Text = row.Cells["colColor"].Value?.ToString() ?? "";
_txtLinetype.Text = row.Cells["colLinetype"].Value?.ToString() ?? "";
_txtDesc.Text = row.Cells["colDesc"].Value?.ToString() ?? "";
_previewLabel = _txtLabel.Text;
_mlPreviewLeaderTab?.Invalidate();
}
private bool SelectGridRowByLayer(string layerName)
{
for (int i = 0; i < _grid.Rows.Count; i++)
{
string rn = _grid.Rows[i].Cells["colLayer"].Value?.ToString() ?? "";
bool match = string.Equals(rn, layerName, StringComparison.OrdinalIgnoreCase)
|| LayerDatabase.WildcardMatch(rn, layerName);
if (match)
{
_grid.ClearSelection();
_grid.Rows[i].Selected = true;
_grid.CurrentCell = _grid.Rows[i].Cells[0];
SafeScrollToRow(_grid, i);
Grid_SelectionChanged(_grid, EventArgs.Empty);
return true;
}
}
return false;
}
/// <summary>
/// Sets DataGridView.FirstDisplayedScrollingRowIndex without
/// crashing.
///
/// CRASH FIX: that setter throws
/// InvalidOperationException("No room is available to display
/// rows.") whenever the control's current client height is too
/// small to show even one full row — which happens when this is
/// called from the Leader tab's "Place Leader" button while the
/// Layer tab (and its grid) has never been laid out / made
/// visible yet this session, or while the form is mid-resize.
/// The property is purely a "scroll into view" convenience, so
/// skipping it when there's no room is harmless — the row is
/// still correctly selected via Rows[i].Selected and CurrentCell
/// above; only the auto-scroll is skipped.
/// </summary>
private static void SafeScrollToRow(DataGridView grid, int rowIndex)
{
try
{
if (grid.DisplayedRowCount(false) > 0 &&
rowIndex >= 0 && rowIndex < grid.RowCount)
{
grid.FirstDisplayedScrollingRowIndex = rowIndex;
}
}
catch (InvalidOperationException)
{
// Grid has no room yet (zero-height / not laid out) —
// selection itself already succeeded above; nothing more
// to do here.
}
}
// ─────────────────────────────────────────────────────────────
// OK / CANCEL / APPLY / UNDO / REFRESH / DELETE (Layer tab)
// ─────────────────────────────────────────────────────────────
private void BtnApply_Click(object sender, EventArgs e) => TryApplyCurrentEntry();
private bool TryApplyCurrentEntry()
{
if (string.IsNullOrWhiteSpace(_txtLayer.Text) || string.IsNullOrWhiteSpace(_txtLabel.Text))
{
MessageBox.Show("Layer name and Label are required.", "Validation",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
var entry = new LayerEntry
{
LayerName = _txtLayer.Text.Trim(),
DisplayLabel = _txtLabel.Text.Trim(),
Color = string.IsNullOrWhiteSpace(_txtColor.Text) ? "11" : _txtColor.Text.Trim(),
Linetype = string.IsNullOrWhiteSpace(_txtLinetype.Text) ? "Continuous" : _txtLinetype.Text.Trim(),
Description = _txtDesc.Text.Trim()
};
bool updated = LayerDatabase.AddOrUpdate(entry);
LoadGrid(entry.LayerName);
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
SetStatus($"{(updated ? "Updated" : "Added")}: \"{entry.LayerName}\" | refreshed {refreshed} label field(s)");
LogService.Info("MainForm", $"Apply -> {(updated ? "updated" : "added")} \"{entry.LayerName}\"; refreshed {refreshed} labels");
return true;
}
private void UndoCurrentEdit()
{
if (_grid.SelectedRows.Count > 0)
{
Grid_SelectionChanged(_grid, EventArgs.Empty);
SetStatus("Undo: restored the selected DB row into the edit fields.");
}
else
{
LoadGrid();
SetStatus("Undo: database reloaded.");
}
LogService.Info("MainForm", "UndoCurrentEdit()");
}
private void BtnDelete_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_txtLayer.Text)) return;
if (MessageBox.Show($"Delete \"{_txtLayer.Text}\"?", "Confirm",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
string deletedLayer = _txtLayer.Text.Trim();
bool removed = LayerDatabase.Remove(deletedLayer);
_txtLayer.Clear(); _txtLabel.Clear(); _txtColor.Clear();
_txtLinetype.Clear(); _txtDesc.Clear();
LoadGrid();
int refreshed = removed ? LabelFieldUpdater.RefreshActiveDocument() : 0;
SetStatus(removed ? $"Deleted: \"{deletedLayer}\" | refreshed {refreshed} label field(s)" : $"Not found: \"{deletedLayer}\"");
LogService.Info("MainForm", removed ? $"Delete -> removed \"{deletedLayer}\""
: $"Delete -> not found \"{deletedLayer}\"");
}
}
// ═════════════════════════════════════════════════════════════
// BLOCK GRID
// ═════════════════════════════════════════════════════════════
internal void LoadBlockGrid(string selectBlockName = null, string selectVisState = null)
{
_blockEntries = BlockDatabase.Load();
_blockGrid.Rows.Clear();
foreach (var e in _blockEntries)
_blockGrid.Rows.Add(e.BlockName, e.VisibilityState, e.DisplayLabel,
e.Color ?? "", e.Linetype ?? "", e.Description ?? "");
bool selected = !string.IsNullOrWhiteSpace(selectBlockName) &&
SelectBlockGridRow(selectBlockName, selectVisState);
if (!selected && _blockGrid.Rows.Count > 0)
{
_blockGrid.ClearSelection();
_blockGrid.Rows[0].Selected = true;
_blockGrid.CurrentCell = _blockGrid.Rows[0].Cells[0];
BlockGrid_SelectionChanged(_blockGrid, EventArgs.Empty);
}
LogService.Info("MainForm", $"LoadBlockGrid() -> {_blockEntries.Count} entries from {BlockDatabase.DbPath}");
}
private void BlockGrid_SelectionChanged(object sender, EventArgs e)
{
if (_blockGrid.SelectedRows.Count == 0) return;
var row = _blockGrid.SelectedRows[0];
_txtBlockName.Text = row.Cells["colBlock"].Value?.ToString() ?? "";
_txtVisState.Text = row.Cells["colVis"].Value?.ToString() ?? "";
_txtBlockLabel.Text = row.Cells["colBLabel"].Value?.ToString() ?? "";
_txtBlockColor.Text = row.Cells["colBColor"].Value?.ToString() ?? "";
_txtBlockLinetype.Text = row.Cells["colBLinetype"].Value?.ToString() ?? "";
_txtBlockDesc.Text = row.Cells["colBDesc"].Value?.ToString() ?? "";
_mlPreviewBlocksTab?.Invalidate();
}
private bool SelectBlockGridRow(string blockName, string visState)
{
for (int i = 0; i < _blockGrid.Rows.Count; i++)
{
string bn = _blockGrid.Rows[i].Cells["colBlock"].Value?.ToString() ?? "";
string vs = _blockGrid.Rows[i].Cells["colVis"].Value?.ToString() ?? "";
bool blockMatch = string.Equals(bn, blockName, StringComparison.OrdinalIgnoreCase)
|| LayerDatabase.WildcardMatch(bn, blockName);
bool visMatch = string.IsNullOrEmpty(visState)
|| string.Equals(vs, visState, StringComparison.OrdinalIgnoreCase)
|| LayerDatabase.WildcardMatch(vs, visState);
if (blockMatch && visMatch)
{
_blockGrid.ClearSelection();
_blockGrid.Rows[i].Selected = true;
_blockGrid.CurrentCell = _blockGrid.Rows[i].Cells[0];
SafeScrollToRow(_blockGrid, i);
BlockGrid_SelectionChanged(_blockGrid, EventArgs.Empty);
return true;
}
}
return false;
}
private void BtnBlockSave_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_txtBlockName.Text) ||
string.IsNullOrWhiteSpace(_txtVisState.Text) ||
string.IsNullOrWhiteSpace(_txtBlockLabel.Text))
{
MessageBox.Show("Block name, Visibility state and Label are required.",
"Validation", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var entry = new BlockEntry
{
BlockName = _txtBlockName.Text.Trim(),
VisibilityState = _txtVisState.Text.Trim(),
DisplayLabel = _txtBlockLabel.Text.Trim(),
Color = string.IsNullOrWhiteSpace(_txtBlockColor.Text) ? "11" : _txtBlockColor.Text.Trim(),
Linetype = string.IsNullOrWhiteSpace(_txtBlockLinetype.Text) ? "Continuous" : _txtBlockLinetype.Text.Trim(),
Description = _txtBlockDesc.Text.Trim()
};
bool updated = BlockDatabase.AddOrUpdate(entry);
LoadBlockGrid(entry.BlockName, entry.VisibilityState);
int refreshed = LabelFieldUpdater.RefreshActiveDocument();
SetStatus($"{(updated ? "Updated" : "Added")}: \"{entry.BlockName}\"/\"{entry.VisibilityState}\" | refreshed {refreshed} label field(s)");
}
private void BtnBlockDelete_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_txtBlockName.Text)) return;
if (MessageBox.Show($"Delete \"{_txtBlockName.Text}\" / \"{_txtVisState.Text}\"?", "Confirm",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
bool removed = BlockDatabase.Remove(_txtBlockName.Text.Trim(), _txtVisState.Text.Trim());
_txtBlockName.Clear(); _txtVisState.Clear(); _txtBlockLabel.Clear();
_txtBlockColor.Clear(); _txtBlockLinetype.Clear(); _txtBlockDesc.Clear();
LoadBlockGrid();
int refreshed = removed ? LabelFieldUpdater.RefreshActiveDocument() : 0;
SetStatus(removed ? $"Deleted. | refreshed {refreshed} label field(s)" : "Not found.");
}
}
private void UndoCurrentBlockEdit()
{
if (_blockGrid.SelectedRows.Count > 0)
{
BlockGrid_SelectionChanged(_blockGrid, EventArgs.Empty);
SetStatus("Undo: restored the selected block row into the edit fields.");
}
else
{
LoadBlockGrid();
SetStatus("Undo: block database reloaded.");
}
}
// ─────────────────────────────────────────────────────────────
// LOG WIRING
// ─────────────────────────────────────────────────────────────
private void WireLog()
{
LogService.EntryAdded += entry =>
{
if (_logBox.IsDisposed) return;
if (_logBox.InvokeRequired) _logBox.BeginInvoke(new Action<LogEntry>(AppendLog), entry);
else AppendLog(entry);
};
foreach (var e in LogService.Entries) AppendLog(e);
}
private void AppendLog(LogEntry entry)
{
SysColor col = entry.Level switch
{
LogLevel.ERROR => C3DTheme.Red,
LogLevel.WARN => C3DTheme.Warn,
LogLevel.DEBUG => C3DTheme.Muted,
_ => C3DTheme.Info
};
_logBox.SelectionStart = _logBox.TextLength;
_logBox.SelectionLength = 0;
_logBox.SelectionColor = col;
_logBox.AppendText(entry + Environment.NewLine);
_logBox.ScrollToCaret();
}
// ─────────────────────────────────────────────────────────────
// Public entry point called by PLACELEADER command
// ─────────────────────────────────────────────────────────────
public void InvokePlaceLeader() => StartPlaceLeader();
// ─────────────────────────────────────────────────────────────
// KEYBOARD + CLOSE
// ─────────────────────────────────────────────────────────────
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape) return true;
return base.ProcessCmdKey(ref msg, keyData);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; Hide(); }
base.OnFormClosing(e);
}
// ─────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────
private void SetStatus(string text)
{
if (_statusLabel.InvokeRequired)
_statusLabel.BeginInvoke(new Action(() => _statusLabel.Text = text));
else _statusLabel.Text = text;
}
// Wider label column (was 80px, clipped "Linetype:") fixes the
// reported "text is not align with the field" issue.
private Label MkLbl(string t) => new Label
{
Text = t, Dock = DockStyle.Fill, BackColor = C3DTheme.Panel,
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = ContentAlignment.MiddleRight, AutoEllipsis = false
};
private TextBox MkTb() => new TextBox
{
Dock = DockStyle.Fill, BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg,
BorderStyle = BorderStyle.FixedSingle, Font = C3DTheme.CodeSmall,
Margin = new Padding(3, 3, 0, 3)
};
}
}
// ===== FILE: Forms\AddLayerForm.cs =====
using SysFont = System.Drawing.Font;
using SysColor = System.Drawing.Color;
using SysSize = System.Drawing.Size;
using System;
using System.Windows.Forms;
using LayerLeaderPlugin.Core;
using LayerLeaderPlugin.Models;
namespace LayerLeaderPlugin.Forms
{
/// <summary>
/// Modal dialog for adding / editing a layer database entry.
/// Layer name supports * and ? wildcards.
/// </summary>
public class AddLayerForm : Form
{
public LayerEntry Result { get; private set; }
private TextBox _txtLayer, _txtLabel, _txtColor, _txtLinetype, _txtDesc;
private Button _btnOk, _btnCancel;
public AddLayerForm(LayerEntry existing = null)
{
BuildUi();
if (existing != null)
{
_txtLayer.Text = existing.LayerName;
_txtLabel.Text = existing.DisplayLabel;
_txtColor.Text = existing.Color ?? "";
_txtLinetype.Text = existing.Linetype ?? "";
_txtDesc.Text = existing.Description ?? "";
_txtLayer.ReadOnly = true;
Text = "Edit Layer Entry";
}
else
{
Text = "Add Layer Entry";
_txtColor.Text = "11";
_txtLinetype.Text = "Continuous";
}
}
private void BuildUi()
{
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
Size = new SysSize(540, 330);
BackColor = C3DTheme.Bg;
ForeColor = C3DTheme.Fg;
Font = C3DTheme.Normal;
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill,
RowCount = 3,
ColumnCount = 1,
BackColor = C3DTheme.Bg,
Padding = new Padding(0)
};
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 44));
Controls.Add(outer);
var header = new TableLayoutPanel
{
Dock = DockStyle.Fill,
RowCount = 2,
ColumnCount = 1,
BackColor = C3DTheme.HeaderBg,
Padding = new Padding(12, 6, 12, 3)
};
header.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
header.RowStyles.Add(new RowStyle(SizeType.Absolute, 17));
header.Controls.Add(new Label
{
Dock = DockStyle.Fill,
Text = "Layer database entry",
BackColor = C3DTheme.HeaderBg,
ForeColor = C3DTheme.TextW,
Font = C3DTheme.Title,
TextAlign = System.Drawing.ContentAlignment.MiddleLeft
}, 0, 0);
header.Controls.Add(new Label
{
Dock = DockStyle.Fill,
Text = "Tip: use * for any chars or ? for one char in Layer name",
BackColor = C3DTheme.HeaderBg,
ForeColor = C3DTheme.Muted,
Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleLeft
}, 0, 1);
outer.Controls.Add(header, 0, 0);
var tbl = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 5,
Padding = new Padding(12, 12, 12, 8),
BackColor = C3DTheme.Panel
};
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 118));
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
for (int i = 0; i < 5; i++)
tbl.RowStyles.Add(new RowStyle(SizeType.Absolute, 32));
outer.Controls.Add(tbl, 0, 1);
Label MkLbl(string t) => new Label
{
Text = t,
Dock = DockStyle.Fill,
BackColor = C3DTheme.Panel,
ForeColor = C3DTheme.Muted,
Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleRight
};
TextBox MkTb() => new TextBox
{
Dock = DockStyle.Fill,
BackColor = C3DTheme.Entry,
ForeColor = C3DTheme.Fg,
Font = C3DTheme.CodeSmall,
BorderStyle = BorderStyle.FixedSingle,
Margin = new Padding(4, 4, 0, 4)
};
_txtLayer = MkTb(); _txtLabel = MkTb();
_txtColor = MkTb(); _txtLinetype = MkTb();
_txtDesc = MkTb();
tbl.Controls.Add(MkLbl("Layer name:"), 0, 0); tbl.Controls.Add(_txtLayer, 1, 0);
tbl.Controls.Add(MkLbl("Display label:"),0, 1); tbl.Controls.Add(_txtLabel, 1, 1);
tbl.Controls.Add(MkLbl("Color index:"), 0, 2); tbl.Controls.Add(_txtColor, 1, 2);
tbl.Controls.Add(MkLbl("Linetype:"), 0, 3); tbl.Controls.Add(_txtLinetype, 1, 3);
tbl.Controls.Add(MkLbl("Description:"), 0, 4); tbl.Controls.Add(_txtDesc, 1, 4);
var btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft,
Padding = new Padding(0, 7, 12, 0),
BackColor = C3DTheme.Bg2
};
_btnCancel = C3DTheme.MakeButton("Cancel", "neutral", 86);
_btnCancel.DialogResult = DialogResult.Cancel;
_btnOk = C3DTheme.MakeButton("OK", "ok", 86);
_btnOk.Click += BtnOk_Click;
btnRow.Controls.Add(_btnCancel);
btnRow.Controls.Add(_btnOk);
outer.Controls.Add(btnRow, 0, 2);
AcceptButton = _btnOk;
CancelButton = _btnCancel;
}
private void BtnOk_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_txtLayer.Text) ||
string.IsNullOrWhiteSpace(_txtLabel.Text))
{
MessageBox.Show("Layer name and Display label are required.",
"Validation", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
Result = new LayerEntry
{
LayerName = _txtLayer.Text.Trim(),
DisplayLabel = _txtLabel.Text.Trim(),
Color = string.IsNullOrWhiteSpace(_txtColor.Text) ? "11" : _txtColor.Text.Trim(),
Linetype = string.IsNullOrWhiteSpace(_txtLinetype.Text) ? "Continuous" : _txtLinetype.Text.Trim(),
Description = _txtDesc.Text.Trim()
};
DialogResult = DialogResult.OK;
Close();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape) { DialogResult = DialogResult.Cancel; Close(); return true; }
return base.ProcessCmdKey(ref msg, keyData);
}
}
}
// ===== FILE: Forms\SetupStylesControl.cs =====
using SysFont = System.Drawing.Font;
using SysColor = System.Drawing.Color;
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using LayerLeaderPlugin.Core;
namespace LayerLeaderPlugin.Forms
{
/// <summary>
/// Native replacement for the AutoLISP "match properties" workflow.
/// Three sub-tabs — Text, Dimensions, MLeader — each let the user pick
/// an existing entity in the drawing and apply its layer/style as the
/// new current default, entirely in C# (no SendStringToExecute timing
/// or focus issues).
/// </summary>
public class SetupStylesControl : UserControl
{
private TabControl _subTabs;
private Label _statusLabel;
// Text sub-tab
private Label _lblTextLayer, _lblTextStyle, _lblTextHeight;
private CheckBox _chkTextLayer, _chkTextStyle;
private StyleMatchHelper.TextStyleInfo _textInfo;
// Dimension sub-tab
private Label _lblDimLayer, _lblDimStyle;
private CheckBox _chkDimLayer, _chkDimStyle;
private StyleMatchHelper.DimStyleInfo _dimInfo;
// MLeader sub-tab
private Label _lblMldLayer, _lblMldStyle, _lblMldTextStyle;
private CheckBox _chkMldLayer, _chkMldStyle;
private StyleMatchHelper.MLeaderStyleInfo _mldInfo;
private ComboBox _cmbMldArrowSymbol;
private NumericUpDown _numMldArrowSize;
public SetupStylesControl()
{
BuildUi();
}
private void BuildUi()
{
Dock = DockStyle.Fill;
BackColor = C3DTheme.Panel;
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
BackColor = C3DTheme.Panel
};
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 26));
Controls.Add(outer);
_subTabs = new TabControl
{
Dock = DockStyle.Fill, Appearance = TabAppearance.FlatButtons,
BackColor = C3DTheme.Panel, ForeColor = C3DTheme.Fg, Font = C3DTheme.Normal
};
outer.Controls.Add(_subTabs, 0, 0);
var tabText = new TabPage("Text");
var tabDim = new TabPage("Dimensions");
var tabMld = new TabPage("MLeader");
C3DTheme.StyleTabPage(tabText);
C3DTheme.StyleTabPage(tabDim);
C3DTheme.StyleTabPage(tabMld);
_subTabs.TabPages.Add(tabText);
_subTabs.TabPages.Add(tabDim);
_subTabs.TabPages.Add(tabMld);
BuildTextTab(tabText);
BuildDimTab(tabDim);
BuildMldTab(tabMld);
_statusLabel = new Label
{
Dock = DockStyle.Fill, BackColor = C3DTheme.Bg2,
ForeColor = C3DTheme.Muted, Font = C3DTheme.CodeSmall,
Text = "Pick an entity in any sub-tab to read its style.",
Padding = new Padding(6, 4, 0, 0)
};
outer.Controls.Add(_statusLabel, 0, 1);
}
// ── TEXT sub-tab ─────────────────────────────────────────────
private void BuildTextTab(TabPage page)
{
var layout = MakeSubTabLayout(out var info, out var btnRow);
page.Controls.Add(layout);
_lblTextLayer = InfoRow(info, 0, "Layer:");
_lblTextStyle = InfoRow(info, 1, "Text style:");
_lblTextHeight = InfoRow(info, 2, "Text height:");
_chkTextLayer = new CheckBox { Text = "Apply Layer", Checked = true,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Small, AutoSize = true,
Margin = new Padding(0, 8, 0, 0) };
_chkTextStyle = new CheckBox { Text = "Apply Text Style + Height", Checked = true,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Small, AutoSize = true,
Margin = new Padding(0, 8, 0, 0) };
info.SetColumnSpan(_chkTextLayer, 2);
info.Controls.Add(_chkTextLayer, 0, 3);
info.SetColumnSpan(_chkTextStyle, 2);
info.Controls.Add(_chkTextStyle, 0, 4);
var btnPick = C3DTheme.MakeButton("▶ Pick Text/MText", "refresh", 150);
var btnApply = C3DTheme.MakeButton("Apply as Current", "apply", 140);
btnPick.Click += (s, e) => DoPickText();
btnApply.Click += (s, e) => DoApplyText();
btnRow.Controls.AddRange(new Control[] { btnPick, btnApply });
btnRow.Controls.Add(MakeBackgroundMaskButton(MaskTabKind.Text));
btnRow.Controls.Add(MakeApplyMaskToExistingButton(MaskTabKind.Text));
}
private void DoPickText()
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
SetStatus("Select Text or MText in the drawing…");
_textInfo = StyleMatchHelper.PickTextStyle(doc);
if (_textInfo == null) { SetStatus("Selection cancelled."); return; }
_lblTextLayer.Text = _textInfo.LayerName;
_lblTextStyle.Text = _textInfo.TextStyleName;
_lblTextHeight.Text = _textInfo.TextHeight.ToString("0.###");
SetStatus($"Picked: Layer=\"{_textInfo.LayerName}\" Style=\"{_textInfo.TextStyleName}\"");
}
private void DoApplyText()
{
if (_textInfo == null) { SetStatus("Pick a Text/MText entity first."); return; }
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
StyleMatchHelper.ApplyTextStyle(doc, _textInfo, _chkTextLayer.Checked, _chkTextStyle.Checked);
SetStatus($"Applied: Layer={_chkTextLayer.Checked} Style={_chkTextStyle.Checked}");
}
// ── DIMENSIONS sub-tab ───────────────────────────────────────
private void BuildDimTab(TabPage page)
{
var layout = MakeSubTabLayout(out var info, out var btnRow);
page.Controls.Add(layout);
_lblDimLayer = InfoRow(info, 0, "Layer:");
_lblDimStyle = InfoRow(info, 1, "Dimension style:");
_chkDimLayer = new CheckBox { Text = "Apply Layer", Checked = true,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Small, AutoSize = true,
Margin = new Padding(0, 8, 0, 0) };
_chkDimStyle = new CheckBox { Text = "Apply Dimension Style", Checked = true,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Small, AutoSize = true,
Margin = new Padding(0, 8, 0, 0) };
info.SetColumnSpan(_chkDimLayer, 2);
info.Controls.Add(_chkDimLayer, 0, 2);
info.SetColumnSpan(_chkDimStyle, 2);
info.Controls.Add(_chkDimStyle, 0, 3);
var btnPick = C3DTheme.MakeButton("▶ Pick Dimension", "refresh", 150);
var btnApply = C3DTheme.MakeButton("Apply as Current", "apply", 140);
var btnPurge = C3DTheme.MakeButton("✕ Delete Overrides", "danger", 150);
var btnPurgeAll = C3DTheme.MakeButton("✕ Clean ALL Styles", "danger", 150);
btnPick.Click += (s, e) => DoPickDim();
btnApply.Click += (s, e) => DoApplyDim();
btnPurge.Click += (s, e) =>
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
StyleMatchHelper.PurgeCurrentDimStyleOverrides(doc);
SetStatus("Cleared <style overrides> for the current dimension style.");
};
btnPurgeAll.Click += (s, e) =>
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
int n = StyleMatchHelper.PurgeAllDimStyleOverrides(doc);
SetStatus($"Clearing <style overrides> for all {n} dimension styles…");
};
btnRow.Controls.AddRange(new Control[] { btnPick, btnApply, btnPurge, btnPurgeAll });
btnRow.Controls.Add(MakeBackgroundMaskButton(MaskTabKind.Dimensions));
btnRow.Controls.Add(MakeApplyMaskToExistingButton(MaskTabKind.Dimensions));
}
private void DoPickDim()
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
SetStatus("Select a dimension line in the drawing…");
_dimInfo = StyleMatchHelper.PickDimStyle(doc);
if (_dimInfo == null) { SetStatus("Selection cancelled."); return; }
_lblDimLayer.Text = _dimInfo.LayerName;
_lblDimStyle.Text = _dimInfo.DimStyleName;
SetStatus($"Picked: Layer=\"{_dimInfo.LayerName}\" Style=\"{_dimInfo.DimStyleName}\"");
}
private void DoApplyDim()
{
if (_dimInfo == null) { SetStatus("Pick a Dimension entity first."); return; }
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
StyleMatchHelper.ApplyDimStyle(doc, _dimInfo, _chkDimLayer.Checked, _chkDimStyle.Checked);
SetStatus($"Applied: Layer={_chkDimLayer.Checked} Style={_chkDimStyle.Checked}");
}
// ── MLEADER sub-tab ──────────────────────────────────────────
private void BuildMldTab(TabPage page)
{
var layout = MakeSubTabLayout(out var info, out var btnRow, infoRows: 8);
page.Controls.Add(layout);
_lblMldLayer = InfoRow(info, 0, "Layer:");
_lblMldStyle = InfoRow(info, 1, "MLeader style:");
_lblMldTextStyle = InfoRow(info, 2, "Text style:");
_chkMldLayer = new CheckBox { Text = "Apply Layer", Checked = true,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Small, AutoSize = true,
Margin = new Padding(0, 8, 0, 0) };
_chkMldStyle = new CheckBox { Text = "Apply MLeader Style", Checked = true,
ForeColor = C3DTheme.Fg, Font = C3DTheme.Small, AutoSize = true,
Margin = new Padding(0, 8, 0, 0) };
info.SetColumnSpan(_chkMldLayer, 2);
info.Controls.Add(_chkMldLayer, 0, 3);
info.SetColumnSpan(_chkMldStyle, 2);
info.Controls.Add(_chkMldStyle, 0, 4);
// ── Arrowhead / Arrow size — applied to the "DB" MLeader
// style via MLeaderStyleHelper, same mechanism the Leader
// tab's Leader Format sub-tab uses. ─────────────────────
var lblArrowSym = new Label { Text = "Arrowhead:", Dock = DockStyle.Fill,
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleRight };
_cmbMldArrowSymbol = new ComboBox
{
Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDown,
BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg,
Font = C3DTheme.CodeSmall, FlatStyle = FlatStyle.Flat
};
_cmbMldArrowSymbol.Items.AddRange(
System.Linq.Enumerable.ToArray(
System.Linq.Enumerable.Cast<object>(ArrowheadHelper.DisplayNames())));
_cmbMldArrowSymbol.Text = "None";
info.Controls.Add(lblArrowSym, 0, 5);
info.Controls.Add(_cmbMldArrowSymbol, 1, 5);
var lblArrowSize = new Label { Text = "Arrow size:", Dock = DockStyle.Fill,
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleRight };
_numMldArrowSize = new NumericUpDown
{
Dock = DockStyle.Fill, DecimalPlaces = 3,
Minimum = 0m, Maximum = 10m, Increment = 0.01m, Value = 0.18m,
BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg, Font = C3DTheme.CodeSmall
};
info.Controls.Add(lblArrowSize, 0, 6);
info.Controls.Add(_numMldArrowSize, 1, 6);
var btnApplyArrow = C3DTheme.MakeButton("Apply Arrowhead/Size to DB style", "apply", 240);
info.SetColumnSpan(btnApplyArrow, 2);
info.Controls.Add(btnApplyArrow, 0, 7);
btnApplyArrow.Click += (s, e) => DoApplyArrowhead();
var btnPick = C3DTheme.MakeButton("▶ Pick MLeader", "refresh", 150);
var btnApply = C3DTheme.MakeButton("Apply as Current", "apply", 140);
btnPick.Click += (s, e) => DoPickMld();
btnApply.Click += (s, e) => DoApplyMld();
btnRow.Controls.AddRange(new Control[] { btnPick, btnApply });
btnRow.Controls.Add(MakeBackgroundMaskButton(MaskTabKind.MLeader));
btnRow.Controls.Add(MakeApplyMaskToExistingButton(MaskTabKind.MLeader));
}
private void DoApplyArrowhead()
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
using (doc.LockDocument())
{
var db = doc.Database;
using var tr = db.TransactionManager.StartTransaction();
var cfg = MLeaderStyleHelper.ReadConfig(db, tr);
cfg.ArrowSymbol = _cmbMldArrowSymbol.Text;
cfg.ArrowSize = (double)_numMldArrowSize.Value;
MLeaderStyleHelper.WriteConfig(db, tr, cfg);
tr.Commit();
}
SetStatus($"Arrowhead=\"{_cmbMldArrowSymbol.Text}\" Size={_numMldArrowSize.Value:0.###} applied to \"{MLeaderStyleHelper.StyleName}\" style.");
}
private void DoPickMld()
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
SetStatus("Select an MLeader in the drawing…");
_mldInfo = StyleMatchHelper.PickMLeaderStyle(doc);
if (_mldInfo == null) { SetStatus("Selection cancelled."); return; }
_lblMldLayer.Text = _mldInfo.LayerName;
_lblMldStyle.Text = _mldInfo.MLeaderStyleName;
_lblMldTextStyle.Text = _mldInfo.TextStyleName;
SetStatus($"Picked: Layer=\"{_mldInfo.LayerName}\" Style=\"{_mldInfo.MLeaderStyleName}\"");
}
private void DoApplyMld()
{
if (_mldInfo == null) { SetStatus("Pick an MLeader entity first."); return; }
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
StyleMatchHelper.ApplyMLeaderStyle(doc, _mldInfo, _chkMldLayer.Checked, _chkMldStyle.Checked);
SetStatus($"Applied: Layer={_chkMldLayer.Checked} Style={_chkMldStyle.Checked}");
}
// ── Shared layout helpers ────────────────────────────────────
private TableLayoutPanel MakeSubTabLayout(out TableLayoutPanel infoTable,
out FlowLayoutPanel btnRow,
int infoRows = 6)
{
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
Padding = new Padding(12), BackColor = C3DTheme.Panel
};
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
infoTable = new TableLayoutPanel
{
Dock = DockStyle.Top, ColumnCount = 2, RowCount = infoRows,
AutoSize = false, Height = infoRows * 30 + 10, BackColor = C3DTheme.Panel
};
infoTable.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140));
infoTable.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
for (int i = 0; i < infoRows; i++)
infoTable.RowStyles.Add(new RowStyle(SizeType.Absolute, 30));
outer.Controls.Add(infoTable, 0, 0);
btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight,
WrapContents = true, BackColor = C3DTheme.Bg2
};
outer.Controls.Add(btnRow, 0, 1);
return outer;
}
/// <summary>
/// Shared "Background Mask Defaults…" button added to every Setup
/// Styles sub-tab. Opens the native BackgroundMaskDialog (same
/// fields as the bundled Background_Mask.lsp's DCL: Use
/// Background Mask, Mask Offset, Fill Color/Transparent),
/// pre-populated with the plugin's current session defaults.
///
/// FIX: this used to force the user to pick an entity every time
/// just to set what should have been a default. "OK" now instead
/// calls BackgroundMaskHelper.SetCurrentDefaults() — every MLeader
/// placed from this point on (via Place Leader / Place on Block)
/// automatically gets these settings, no selection required.
///
/// Dimensions sub-tab is the one exception: dimension masking
/// needs raw XDATA writes with no strongly-typed .NET equivalent
/// in this binding, so confirming settings there also seeds the
/// bmask LISP's persisted defaults for when it's eventually run
/// on a dimension (still requires the user to pick the dimension
/// at that point — there's no drawing-wide "all future dimensions"
/// equivalent without a reactor watching every new DimStyle, which
/// is out of scope here).
/// </summary>
private enum MaskTabKind { Text, Dimensions, MLeader }
private Button MakeBackgroundMaskButton(MaskTabKind kind)
{
var btn = C3DTheme.MakeButton("▦ Background Mask Defaults…", "neutral", 210);
btn.Click += (s, e) =>
{
var current = BackgroundMaskHelper.CurrentDefaults;
using var dlg = new BackgroundMaskDialog(
current.UseMask, current.MaskOffset, current.Transparent);
if (dlg.ShowDialog(this) != DialogResult.OK) return;
var settings = new BackgroundMaskHelper.MaskSettings
{
UseMask = dlg.UseMask,
MaskOffset = dlg.MaskOffset,
Transparent = dlg.Transparent,
FillColorAci = 1
};
if (kind == MaskTabKind.Dimensions)
{
// Dimensions still need an explicit pick via bmask —
// see class doc above — but seed its defaults now so
// that pick uses exactly what was just confirmed.
LispIntegration.SeedBackgroundMaskDefaults(
dlg.UseMask, dlg.MaskOffset, dlg.Transparent);
SetStatus(
$"Dimension mask defaults saved (offset={dlg.MaskOffset:0.00}). " +
"Use 'Legacy LISP Matcher' or run bmask to apply to a dimension.");
}
else
{
// Text / MLeader: this IS the drawing-wide default now.
BackgroundMaskHelper.SetCurrentDefaults(settings);
SetStatus(
$"Background mask default set: offset={settings.MaskOffset:0.00}, " +
$"mask={(settings.UseMask ? "on" : "off")}. " +
"Every new MLeader/leader label placed from now on uses this automatically.");
}
};
return btn;
}
/// <summary>
/// Secondary, explicitly opt-in button: applies the CURRENT
/// session defaults to an EXISTING MText/MLeader the user picks,
/// for retrofitting entities placed before the defaults were set
/// (or placed by something other than this plugin). Separate from
/// MakeBackgroundMaskButton so the common "just set my defaults"
/// path never requires picking anything.
/// </summary>
private Button MakeApplyMaskToExistingButton(MaskTabKind kind)
{
var btn = C3DTheme.MakeButton("▶ Apply to Existing…", "refresh", 150);
btn.Click += (s, e) =>
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) { SetStatus("No active document."); return; }
var settings = BackgroundMaskHelper.CurrentDefaults;
switch (kind)
{
case MaskTabKind.Text:
ApplyMaskToPickedText(doc, settings);
break;
case MaskTabKind.MLeader:
ApplyMaskToPickedMLeader(doc, settings);
break;
case MaskTabKind.Dimensions:
LispIntegration.RunBackgroundMask();
SetStatus("bmask started — select Dimension entities to mask.");
break;
}
};
return btn;
}
/// <summary>
/// Applies mask settings to the MText/MLeader text entity that was
/// most recently picked on the Text sub-tab, if any. Re-prompts
/// the user to pick one if nothing has been picked yet this
/// session, so the button is useful standalone too.
/// </summary>
private void ApplyMaskToPickedText(Document doc, BackgroundMaskHelper.MaskSettings settings)
{
using (doc.LockDocument())
{
var ed = doc.Editor;
var opts = new Autodesk.AutoCAD.EditorInput.PromptEntityOptions(
"\nSelect Text or MText to mask: ");
opts.SetRejectMessage("\nMust be Text or MText.");
opts.AddAllowedClass(typeof(DBText), false);
opts.AddAllowedClass(typeof(MText), false);
var res = ed.GetEntity(opts);
if (res.Status != Autodesk.AutoCAD.EditorInput.PromptStatus.OK)
{
SetStatus("Selection cancelled.");
return;
}
using var tr = doc.Database.TransactionManager.StartTransaction();
if (tr.GetObject(res.ObjectId, OpenMode.ForWrite) is MText mt)
{
BackgroundMaskHelper.ApplyToMText(mt, settings);
string colorNote = settings.Transparent ? ""
: " (note: custom fill colour isn't supported natively yet — applied as Transparent)";
SetStatus($"Background mask applied to MText (offset={settings.MaskOffset:0.00}).{colorNote}");
}
else
{
SetStatus("Plain Text (DBText) does not support a background mask — pick an MText instead.");
}
tr.Commit();
}
}
/// <summary>
/// Applies mask settings to the MLeader text that was most
/// recently picked, prompting for a pick if needed.
/// </summary>
private void ApplyMaskToPickedMLeader(Document doc, BackgroundMaskHelper.MaskSettings settings)
{
using (doc.LockDocument())
{
var ed = doc.Editor;
var opts = new Autodesk.AutoCAD.EditorInput.PromptEntityOptions(
"\nSelect an MLeader to mask: ");
opts.SetRejectMessage("\nMust be an MLeader.");
opts.AddAllowedClass(typeof(MLeader), false);
var res = ed.GetEntity(opts);
if (res.Status != Autodesk.AutoCAD.EditorInput.PromptStatus.OK)
{
SetStatus("Selection cancelled.");
return;
}
using var tr = doc.Database.TransactionManager.StartTransaction();
if (tr.GetObject(res.ObjectId, OpenMode.ForWrite) is MLeader ml)
{
BackgroundMaskHelper.ApplyToMLeader(ml, settings);
string colorNote = settings.Transparent ? ""
: " (note: custom fill colour isn't supported natively yet — applied as Transparent)";
SetStatus($"Background mask applied to MLeader (offset={settings.MaskOffset:0.00}).{colorNote}");
}
tr.Commit();
}
}
private Label InfoRow(TableLayoutPanel t, int row, string labelText)
{
var lbl = new Label
{
Text = labelText, Dock = DockStyle.Fill,
ForeColor = C3DTheme.Muted, Font = C3DTheme.Small,
TextAlign = System.Drawing.ContentAlignment.MiddleRight
};
var val = new Label
{
Text = "(none picked)", Dock = DockStyle.Fill,
ForeColor = C3DTheme.TextW, Font = C3DTheme.CodeSmall,
TextAlign = System.Drawing.ContentAlignment.MiddleLeft,
AutoEllipsis = true
};
t.Controls.Add(lbl, 0, row);
t.Controls.Add(val, 1, row);
return val;
}
private void SetStatus(string text)
{
if (_statusLabel.InvokeRequired)
_statusLabel.BeginInvoke(new Action(() => _statusLabel.Text = text));
else
_statusLabel.Text = text;
LogService.Info("SetupStylesControl", text);
}
}
}
// ===== FILE: Forms\AddBlockForm.cs =====
using SysSize = System.Drawing.Size;
using System;
using System.Windows.Forms;
using LayerLeaderPlugin.Core;
using LayerLeaderPlugin.Models;
namespace LayerLeaderPlugin.Forms
{
/// <summary>
/// Modal dialog for adding / editing a block database entry.
/// BlockName and VisibilityState both support * and ? wildcards,
/// so e.g. "X_2.80_2.30_Mit_Text" and "X_2.80_2.30_Ohne_Text" can be
/// collapsed into a single entry "X_2.80_2.30_*" → label "X".
/// </summary>
public class AddBlockForm : Form
{
public BlockEntry Result { get; private set; }
private TextBox _txtBlock, _txtVis, _txtLabel, _txtColor, _txtLinetype, _txtDesc;
private Button _btnOk, _btnCancel;
public AddBlockForm(BlockEntry existing = null)
{
BuildUi();
if (existing != null)
{
_txtBlock.Text = existing.BlockName;
_txtVis.Text = existing.VisibilityState;
_txtLabel.Text = existing.DisplayLabel;
_txtColor.Text = existing.Color ?? "";
_txtLinetype.Text = existing.Linetype ?? "";
_txtDesc.Text = existing.Description ?? "";
_txtBlock.ReadOnly = true;
_txtVis.ReadOnly = true;
Text = "Edit Block Entry";
}
else
{
Text = "Add Block Entry";
_txtBlock.Text = "*";
_txtColor.Text = "11";
_txtLinetype.Text = "Continuous";
}
}
private void BuildUi()
{
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
Size = new SysSize(560, 370);
BackColor = C3DTheme.Bg;
ForeColor = C3DTheme.Fg;
Font = C3DTheme.Normal;
var outer = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 3, ColumnCount = 1,
BackColor = C3DTheme.Bg, Padding = new Padding(0)
};
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
outer.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
outer.RowStyles.Add(new RowStyle(SizeType.Absolute, 44));
Controls.Add(outer);
var header = new TableLayoutPanel
{
Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1,
BackColor = C3DTheme.HeaderBg, Padding = new Padding(12, 6, 12, 3)
};
header.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
header.RowStyles.Add(new RowStyle(SizeType.Absolute, 17));
header.Controls.Add(new Label
{
Dock = DockStyle.Fill, Text = "Block database entry",
BackColor = C3DTheme.HeaderBg, ForeColor = C3DTheme.TextW,
Font = C3DTheme.Title, TextAlign = System.Drawing.ContentAlignment.MiddleLeft
}, 0, 0);
header.Controls.Add(new Label
{
Dock = DockStyle.Fill,
Text = "Tip: * / ? wildcards work in both Block name and Visibility state",
BackColor = C3DTheme.HeaderBg, ForeColor = C3DTheme.Muted,
Font = C3DTheme.Small, TextAlign = System.Drawing.ContentAlignment.MiddleLeft
}, 0, 1);
outer.Controls.Add(header, 0, 0);
var tbl = new TableLayoutPanel
{
Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 6,
Padding = new Padding(12, 12, 12, 8), BackColor = C3DTheme.Panel
};
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
for (int i = 0; i < 6; i++)
tbl.RowStyles.Add(new RowStyle(SizeType.Absolute, 32));
outer.Controls.Add(tbl, 0, 1);
Label MkLbl(string t) => new Label
{
Text = t, Dock = DockStyle.Fill,
BackColor = C3DTheme.Panel, ForeColor = C3DTheme.Muted,
Font = C3DTheme.Small, TextAlign = System.Drawing.ContentAlignment.MiddleRight,
AutoEllipsis = true
};
TextBox MkTb() => new TextBox
{
Dock = DockStyle.Fill, BackColor = C3DTheme.Entry, ForeColor = C3DTheme.Fg,
Font = C3DTheme.CodeSmall, BorderStyle = BorderStyle.FixedSingle,
Margin = new Padding(4, 4, 0, 4)
};
_txtBlock = MkTb(); _txtVis = MkTb();
_txtLabel = MkTb(); _txtColor = MkTb();
_txtLinetype = MkTb(); _txtDesc = MkTb();
tbl.Controls.Add(MkLbl("Block name:"), 0, 0); tbl.Controls.Add(_txtBlock, 1, 0);
tbl.Controls.Add(MkLbl("Visibility state:"), 0, 1); tbl.Controls.Add(_txtVis, 1, 1);
tbl.Controls.Add(MkLbl("Display label:"), 0, 2); tbl.Controls.Add(_txtLabel, 1, 2);
tbl.Controls.Add(MkLbl("Color index:"), 0, 3); tbl.Controls.Add(_txtColor, 1, 3);
tbl.Controls.Add(MkLbl("Linetype:"), 0, 4); tbl.Controls.Add(_txtLinetype, 1, 4);
tbl.Controls.Add(MkLbl("Description:"), 0, 5); tbl.Controls.Add(_txtDesc, 1, 5);
var btnRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill, FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft,
Padding = new Padding(0, 7, 12, 0), BackColor = C3DTheme.Bg2
};
_btnCancel = C3DTheme.MakeButton("Cancel", "neutral", 86);
_btnCancel.DialogResult = DialogResult.Cancel;
_btnOk = C3DTheme.MakeButton("OK", "ok", 86);
_btnOk.Click += BtnOk_Click;
btnRow.Controls.Add(_btnCancel);
btnRow.Controls.Add(_btnOk);
outer.Controls.Add(btnRow, 0, 2);
AcceptButton = _btnOk;
CancelButton = _btnCancel;
}
private void BtnOk_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_txtBlock.Text) ||
string.IsNullOrWhiteSpace(_txtVis.Text) ||
string.IsNullOrWhiteSpace(_txtLabel.Text))
{
MessageBox.Show("Block name, Visibility state and Display label are required.",
"Validation", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
Result = new BlockEntry
{
BlockName = _txtBlock.Text.Trim(),
VisibilityState = _txtVis.Text.Trim(),
DisplayLabel = _txtLabel.Text.Trim(),
Color = string.IsNullOrWhiteSpace(_txtColor.Text) ? "11" : _txtColor.Text.Trim(),
Linetype = string.IsNullOrWhiteSpace(_txtLinetype.Text) ? "Continuous" : _txtLinetype.Text.Trim(),
Description = _txtDesc.Text.Trim()
};
DialogResult = DialogResult.OK;
Close();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape) { DialogResult = DialogResult.Cancel; Close(); return true; }
return base.ProcessCmdKey(ref msg, keyData);
}
}
}
// ===== FILE: Models\LeaderStyleConfig.cs =====
namespace LayerLeaderPlugin.Models
{
/// <summary>
/// Editable mirror of the AutoCAD "DB" MLeaderStyle properties shown in
/// AutoCAD's Modify Multileader Style dialog (Leader Format / Leader
/// Structure / Content tabs). The Leader tab UI binds to this object;
/// MLeaderStyleHelper reads/writes it against the real MLeaderStyle.
/// </summary>
public class LeaderStyleConfig
{
// ── Leader Format ────────────────────────────────────────────
public string LeaderType { get; set; } = "Straight"; // Straight | Spline
public string LineColor { get; set; } = "ByBlock";
public string Linetype { get; set; } = "ByBlock";
public string LineWeight { get; set; } = "ByBlock";
public string ArrowSymbol { get; set; } = "None"; // matches screenshot (no arrowhead)
public double ArrowSize { get; set; } = 0.18;
public double LeaderBreakSize { get; set; } = 0.125;
// ── Leader Structure ─────────────────────────────────────────
public bool MaxLeaderPointsEnabled = true;
public int MaxLeaderPoints { get; set; } = 2;
public bool FirstSegAngleEnabled = false;
public double FirstSegAngle { get; set; } = 0;
public bool SecondSegAngleEnabled = false;
public double SecondSegAngle { get; set; } = 0;
public bool AutoIncludeLanding { get; set; } = true;
public bool SetLandingDistance { get; set; } = true;
public double LandingDistance { get; set; } = 0.36;
public bool Annotative { get; set; } = false;
public double Scale { get; set; } = 1.0;
// ── Content ───────────────────────────────────────────────────
public string MultileaderType { get; set; } = "Mtext";
public string DefaultText { get; set; } = "Default Text";
public string TextStyle { get; set; } = "K_ISOCP";
public string TextAngle { get; set; } = "Keep horizontal";
public string TextColor { get; set; } = "ByBlock";
public double TextHeight { get; set; } = 1.0;
public bool AlwaysLeftJustify { get; set; } = false;
public bool FrameText { get; set; } = false;
public bool HorizontalAttachment { get; set; } = true; // vs Vertical
public string LeftAttachment { get; set; } = "Underline top line";
public string RightAttachment { get; set; } = "Underline top line";
public double LandingGap { get; set; } = 0.09;
public bool ExtendLeaderToText { get; set; } = false;
public LeaderStyleConfig Clone() => (LeaderStyleConfig)MemberwiseClone();
}
}
// ===== FILE: Models\LayerEntry.cs =====
using System;
namespace LayerLeaderPlugin.Models
{
/// <summary>
/// One row in the layer→label database.
/// LayerName supports * and ? wildcards for matching.
/// </summary>
public class LayerEntry
{
public string LayerName { get; set; } // may contain * or ?
public string DisplayLabel { get; set; }
public string Color { get; set; } // AutoCAD color index (e.g. "11") or name
public string Linetype { get; set; } // e.g. "Continuous"
public string Description { get; set; }
}
/// <summary>
/// One row in the block→label database.
/// BlockName is the dynamic block's effective name (Name / DynamicBlockTableRecord).
/// VisibilityState supports * and ? wildcards, so multiple states such as
/// "X_2.80_2.30_Mit_Text" and "X_2.80_2.30_Ohne_Text" can collapse to one
/// label ("X") via a pattern like "X_2.80_2.30_*".
/// </summary>
public class BlockEntry
{
public string BlockName { get; set; } // dynamic block name, may contain * or ?
public string VisibilityState { get; set; } // visibility state name, may contain * or ?
public string DisplayLabel { get; set; }
public string Color { get; set; }
public string Linetype { get; set; }
public string Description { get; set; }
}
public enum LogLevel { INFO, WARN, ERROR, DEBUG }
public class LogEntry
{
public DateTime Timestamp { get; set; } = DateTime.Now;
public LogLevel Level { get; set; }
public string Source { get; set; }
public string Message { get; set; }
public override string ToString() =>
$"{Timestamp:HH:mm:ss} [{Level}] [{Source}] {Message}";
}
}
