PlotExtra

Short description

🖨️ PlotExtra is an AutoCAD and Civil 3D .NET plotting interface that recreates and extends the plot dialog workflow with a styled WinForms UI. 📐 It includes layout preview, page setup controls, PDF project management, logging, and reusable C3D themed controls.

Image 000 5

Command:

🚀 Main command: PLOTEXTRA018
🧾 Log command: PLOTEXTRALOG
🧰 Opens a Civil 3D styled .NET plot dialog with preview, logging, PDF/project settings, and plot configuration controls.

Description:

🖨️ PlotExtra is an SEO-friendly AutoCAD .NET plugin for improving layout plotting, page setup control, PDF output preparation, and plot preview workflows.
📐 The project captures layout geometry for a lightweight preview panel and exposes a custom plot dialog modeled after AutoCAD plot settings.
🧾 A dedicated log viewer command helps diagnose plot dialog actions, preview capture issues, and workflow status during testing.
🗂️ PDF project and age-tier configuration forms support project-based output management and visual organization of recently modified PDF files.
🎯 This is useful for Civil 3D plotting, AutoCAD sheet production, PDF export workflows, custom plot automation, and C# plugin UI development.

Helper function: (if any)​

🧩 Commands.cs registers the plot dialog command and the PLOTEXTRALOG viewer command.
🧩 PlotDialogForm.cs contains the main WinForms plot interface and plot setting controls.
🧩 PreviewControl.cs renders sampled layout geometry in a reusable GDI+ preview panel.
🧩 PlotService.cs captures layout geometry and supports preview preparation.
🧩 Logger.cs records runtime messages for troubleshooting and workflow review.
🧩 PdfProjectConfig and PdfTierConfig store project output paths and file-age display tiers.
🧩 C3DTheme and ButtonIconFactory provide a consistent Civil 3D styled interface with reusable icons.

Functionalities:

✅ 🖨️ Open a custom plot dialog from AutoCAD using a .NET command.
✅ 📐 Show a paper/layout preview based on captured drawing geometry.
✅ 🧾 Open a modeless log viewer with save and clear actions.
✅ 🗂️ Manage PDF project locations and project-specific plotting folders.
✅ 🎨 Use C3D dark/light palettes, compact buttons, and generated icons for consistent UI styling.
✅ ⚙️ Centralize build naming through project properties so command and assembly names follow the build number.
✅ 🧪 Provide a foundation for wiring UI fields to AutoCAD PlotSettings and PlotEngine logic.

Result:

🎯 The result is a structured plotting workspace for AutoCAD and Civil 3D that supports preview, logging, project PDF settings, and future plot automation.
🧰 PlotExtra helps replace scattered manual plot setup steps with a single CAD-oriented interface designed for production sheet workflows.
✅ The project is especially useful as a C#/.NET template for custom plot tools, Civil 3D UI styling, and layout preview utilities.

Images, animations etc.

Image 000 5
Pixel

XAML code:

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

<src:RibbonToolTip x:Key="PLOTEXTRALOG">
<src:RibbonToolTip.ExpandedContent>
<StackPanel>
<TextBlock Background="AntiqueWhite" TextAlignment="Left">
<Bold>Function Syntax: PLOTEXTRALOG</Bold>
<LineBreak/>
<Bold>Version: 1.0 Date: 15.07.2026 11:27:53AM</Bold>
<LineBreak/>
<Hyperlink>N/A</Hyperlink>
<LineBreak/>

<Bold><Run Foreground="DodgerBlue">🚀 Command Name</Run></Bold><LineBreak/>
<Run Foreground="White">🧩 Type </Run><Bold><Run Foreground="Orange">PLOTEXTRA018</Run></Bold><Run Foreground="White"> at the AutoCAD command line.</Run><LineBreak/>
<Run Foreground="White">🧩 Type </Run><Bold><Run Foreground="Orange">PLOTEXTRALOG</Run></Bold><Run Foreground="White"> at the AutoCAD command line.</Run><LineBreak/>
<Bold><Run Foreground="DodgerBlue">🧭 SEO Optimized Description</Run></Bold><LineBreak/>
<Run Foreground="White">🖨️ PlotExtra is an SEO-friendly AutoCAD .NET plugin for improving layout plotting, page setup control, PDF output preparation, and plot preview workflows.</Run><LineBreak/>
<Run Foreground="White">📐 The project captures layout geometry for a lightweight preview panel and exposes a custom plot dialog modeled after AutoCAD plot settings.</Run><LineBreak/>
<Run Foreground="White">🧾 A dedicated log viewer command helps diagnose plot dialog actions, preview capture issues, and workflow status during testing.</Run><LineBreak/>
<Run Foreground="White">🗂️ PDF project and age-tier configuration forms support project-based output management and visual organization of recently modified PDF files.</Run><LineBreak/>
<Run Foreground="White">🎯 This is useful for Civil 3D plotting, AutoCAD sheet production, PDF export workflows, custom plot automation, and C# plugin UI development.</Run><LineBreak/>
<Bold><Run Foreground="LimeGreen">🧰 Main Workflow</Run></Bold><LineBreak/>
<Run Foreground="White">1️⃣ NETLOAD the PlotExtra build DLL in AutoCAD or Civil 3D.</Run><LineBreak/>
<Run Foreground="White">2️⃣ Run PLOTEXTRA018 to open the custom plot dialog.</Run><LineBreak/>
<Run Foreground="White">3️⃣ Review page setup, device, paper, plot area, PDF, and preview information.</Run><LineBreak/>
<Run Foreground="White">4️⃣ Use PLOTEXTRALOG to inspect runtime messages and preview diagnostics.</Run><LineBreak/>
<Run Foreground="White">5️⃣ Adjust project/PDF settings and continue the plotting workflow.</Run><LineBreak/>
<Bold><Run Foreground="LimeGreen">✅ Functionalities</Run></Bold><LineBreak/>
<Run Foreground="White">✅ 🖨️ Open a custom plot dialog from AutoCAD using a .NET command.</Run><LineBreak/>
<Run Foreground="White">✅ 📐 Show a paper/layout preview based on captured drawing geometry.</Run><LineBreak/>
<Run Foreground="White">✅ 🧾 Open a modeless log viewer with save and clear actions.</Run><LineBreak/>
<Run Foreground="White">✅ 🗂️ Manage PDF project locations and project-specific plotting folders.</Run><LineBreak/>
<Run Foreground="White">✅ 🎨 Use C3D dark/light palettes, compact buttons, and generated icons for consistent UI styling.</Run><LineBreak/>
<Run Foreground="White">✅ ⚙️ Centralize build naming through project properties so command and assembly names follow the build number.</Run><LineBreak/>
<Run Foreground="White">✅ 🧪 Provide a foundation for wiring UI fields to AutoCAD PlotSettings and PlotEngine logic.</Run><LineBreak/>
<Bold><Run Foreground="DodgerBlue">🧩 Important Commands and Helper Functions</Run></Bold><LineBreak/>
<Run Foreground="White">🧩 Commands.cs registers the plot dialog command and the PLOTEXTRALOG viewer command.</Run><LineBreak/>
<Run Foreground="White">🧩 PlotDialogForm.cs contains the main WinForms plot interface and plot setting controls.</Run><LineBreak/>
<Run Foreground="White">🧩 PreviewControl.cs renders sampled layout geometry in a reusable GDI+ preview panel.</Run><LineBreak/>
<Run Foreground="White">🧩 PlotService.cs captures layout geometry and supports preview preparation.</Run><LineBreak/>
<Run Foreground="White">🧩 Logger.cs records runtime messages for troubleshooting and workflow review.</Run><LineBreak/>
<Run Foreground="White">🧩 PdfProjectConfig and PdfTierConfig store project output paths and file-age display tiers.</Run><LineBreak/>
<Run Foreground="White">🧩 C3DTheme and ButtonIconFactory provide a consistent Civil 3D styled interface with reusable icons.</Run><LineBreak/>
<Bold><Run Foreground="DodgerBlue">🎯 Practical Result</Run></Bold><LineBreak/>
<Run Foreground="White">🎯 The result is a structured plotting workspace for AutoCAD and Civil 3D that supports preview, logging, project PDF settings, and future plot automation.</Run><LineBreak/>
<Run Foreground="White">🧰 PlotExtra helps replace scattered manual plot setup steps with a single CAD-oriented interface designed for production sheet workflows.</Run><LineBreak/>
<Run Foreground="White">✅ The project is especially useful as a C#/.NET template for custom plot tools, Civil 3D UI styling, and layout preview utilities.</Run><LineBreak/>
<Bold><Run Foreground="DarkRed">⚠️ Important Notes</Run></Bold><LineBreak/>
<Run Foreground="White">⚠️ The attached project is build 018; the generated command name is derived from the PlotExtra command prefix and build number.</Run><LineBreak/>
<Run Foreground="White">⚠️ Some plot execution logic appears structured as a UI/preview foundation and may require final PlotSettings/PlotEngine wiring for real plotting in every environment.</Run><LineBreak/>
<Run Foreground="White">⚠️ AutoCAD .NET plugins must be built against compatible x64 AutoCAD/Civil 3D assemblies before NETLOAD.</Run><LineBreak/>
<Bold><Run Foreground="Yellow">🏷️ Tags</Run></Bold><LineBreak/>
<Run Foreground="Yellow">🏷️ AutoCAD, 🏷️ Civil 3D, 🏷️ .NET, 🏷️ C#, 🏷️ AutoCAD Plugin, 🏷️ Plot Dialog, 🏷️ Plot Settings, 🏷️ PDF Plotting, 🏷️ Layout Preview, 🏷️ WinForms, 🏷️ PlotExtra, 🏷️ PLOTEXTRA018, 🏷️ PLOTEXTRALOG, 🏷️ CAD Automation, 🏷️ Sheet Production, 🏷️ C3D Theme</Run><LineBreak/>





</TextBlock>
            
<Grid>
<Image Source="Image_000.png" Stretch="Uniform"/>
</Grid>

</StackPanel>
</src:RibbonToolTip.ExpandedContent>
</src:RibbonToolTip>
</ResourceDictionary>

C# code:

// ===== FILE: Properties\BuildInfo.generated.cs =====
// <auto-generated> regenerated on every build from BQ_Build. Do not edit by hand. </auto-generated>
namespace PlotExtra
{
internal static class BuildInfo
{
public const string BuildNumber      = "018";
public const string ProjectBase      = "PlotExtra";
public const string AssemblyBaseName = "PlotExtra_018";
public const string CommandName      = "PLOTEXTRA_018";
}
}


// ===== FILE: Logging\Logger.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace PlotExtra.Logging
{
    /// <summary>
    /// Severity levels for log entries. Kept as a simple enum rather than a
    /// full logging framework dependency, so the project stays self-contained.
    /// </summary>
    public enum LogLevel
    {
        INFO,
        WARN,
        ERROR,
        DEBUG
    }

    /// <summary>
    /// Immutable record of a single log line, formatted the way it will be
    /// displayed, e.g.:
    ///   05:29:43  [INFO]   [PvSplitApi]  SplitDataLines.Add(Double rawStation, Double datum)
    /// </summary>
    public sealed class LogEntry
    {
        public DateTime Timestamp { get; }
        public LogLevel Level { get; }
        public string Source { get; }
        public string Message { get; }

        public LogEntry(DateTime timestamp, LogLevel level, string source, string message)
        {
            Timestamp = timestamp;
            Level = level;
            Source = source;
            Message = message;
        }

        public override string ToString()
        {
            return string.Format(
                "{0:HH:mm:ss}  [{1,-5}]   [{2}]  {3}",
                Timestamp, Level, Source, Message);
        }
    }

    /// <summary>
    /// Process-wide log buffer. Any part of the app (command handlers, the
    /// preview control, the plot engine, etc.) can call Logger.Info/Warn/Error
    /// and the LogViewerForm (or any other subscriber) is notified live via
    /// the EntryAdded event. The buffer itself can be persisted to disk with
    /// SaveToFile so successive NETLOAD sessions can be diffed/improved.
    /// </summary>
    public static class Logger
    {
        private static readonly object _sync = new object();
        private static readonly List<LogEntry> _entries = new List<LogEntry>();

        /// <summary>Raised on every new entry. Subscribers must marshal to their own UI thread if needed.</summary>
        public static event EventHandler<LogEntry> EntryAdded;

        public static IReadOnlyList<LogEntry> Entries
        {
            get { lock (_sync) { return _entries.ToArray(); } }
        }

        public static void Info(string source, string message) => Write(LogLevel.INFO, source, message);
        public static void Warn(string source, string message) => Write(LogLevel.WARN, source, message);
        public static void Error(string source, string message) => Write(LogLevel.ERROR, source, message);
        public static void Debug(string source, string message) => Write(LogLevel.DEBUG, source, message);

        public static void Write(LogLevel level, string source, string message)
        {
            var entry = new LogEntry(DateTime.Now, level, source, message);
            lock (_sync)
            {
                _entries.Add(entry);
            }
            EntryAdded?.Invoke(null, entry);
        }

        public static void Clear()
        {
            lock (_sync) { _entries.Clear(); }
        }

        /// <summary>
        /// Persists the current buffer to a text file (append mode), so that
        /// logs from multiple test sessions can be kept and compared to feed
        /// back into improving the plotting logic (e.g. SplitDataLines).
        /// </summary>
        public static string SaveToFile(string directory = null)
        {
            directory = directory ?? Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                "PlotExtra", "Logs");

            Directory.CreateDirectory(directory);

            string fileName = string.Format("PlotExtra_log_{0:yyyy-MM-dd}.txt", DateTime.Now);
            string fullPath = Path.Combine(directory, fileName);

            var sb = new StringBuilder();
            lock (_sync)
            {
                foreach (var e in _entries)
                    sb.AppendLine(e.ToString());
            }

            File.AppendAllText(fullPath, sb.ToString(), Encoding.UTF8);
            return fullPath;
        }
    }
}


// ===== FILE: Core\PdfTierConfig.cs =====
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using PlotExtra.Logging;

namespace PlotExtra.Core
{
    /// <summary>
    /// One age bracket: files modified within MaxAgeDays of "now" get this
    /// background/foreground color in the PDF file list. MaxAgeDays == null
    /// means "everything older than the previous tier" (the catch-all last
    /// tier). Ported field-for-field from PDF_Project.py's DEFAULT_TIERS.
    /// </summary>
    public class PdfTier : INotifyPropertyChanged
    {
        private int? _maxAgeDays;
        private string _tag;
        private string _bgColorHex;
        private string _fgColorHex;

        /// <summary>Days since last-modified; null = no upper bound (last/catch-all tier).</summary>
        public int? MaxAgeDays
        {
            get => _maxAgeDays;
            set { _maxAgeDays = value; OnChanged(nameof(MaxAgeDays)); }
        }

        public string Tag
        {
            get => _tag;
            set { _tag = value; OnChanged(nameof(Tag)); }
        }

        /// <summary>Background color, e.g. "#28a745".</summary>
        public string BgColorHex
        {
            get => _bgColorHex;
            set { _bgColorHex = value; OnChanged(nameof(BgColorHex)); }
        }

        /// <summary>Foreground/text color, e.g. "white"; empty means "use the list's default text color".</summary>
        public string FgColorHex
        {
            get => _fgColorHex;
            set { _fgColorHex = value; OnChanged(nameof(FgColorHex)); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnChanged(string prop) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));

        public PdfTier() { }
        public PdfTier(int? maxAgeDays, string tag, string bg, string fg)
        {
            _maxAgeDays = maxAgeDays; _tag = tag; _bgColorHex = bg; _fgColorHex = fg;
        }

        public PdfTier Clone() => new PdfTier(MaxAgeDays, Tag, BgColorHex, FgColorHex);

        public Color BgColor => ParseColor(BgColorHex, Color.White);
        public Color FgColor => ParseColor(FgColorHex, Color.Black);

        private static Color ParseColor(string hexOrName, Color fallback)
        {
            if (string.IsNullOrWhiteSpace(hexOrName)) return fallback;
            try
            {
                return hexOrName.StartsWith("#", StringComparison.Ordinal)
                    ? ColorTranslator.FromHtml(hexOrName)
                    : Color.FromName(hexOrName);
            }
            catch
            {
                return fallback;
            }
        }
    }

    /// <summary>Loads/saves the tier list, seeded with PDF_Project.py's exact DEFAULT_TIERS on first run.</summary>
    public static class PdfTierStore
    {
        private const string Source = "PdfTierStore";

        private static string ConfigPath => Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "PlotExtra", "pdf_tiers.json");

        private static List<PdfTier> DefaultTiers() => new List<PdfTier>
        {
            new PdfTier(1,    "tier1", "#28a745", "white"),
            new PdfTier(3,    "tier2", "#74c365", ""),
            new PdfTier(7,    "tier3", "#98fb98", ""),
            new PdfTier(30,   "tier4", "#c1fcc1", ""),
            new PdfTier(null, "tier5", "#e6ffe6", ""),
        };

        public static List<PdfTier> Load()
        {
            try
            {
                if (File.Exists(ConfigPath))
                {
                    var loaded = Deserialize(File.ReadAllText(ConfigPath, Encoding.UTF8));
                    if (loaded != null && loaded.Count > 0) return loaded;
                }
            }
            catch (Exception ex)
            {
                Logger.Warn(Source, "Could not read " + ConfigPath + ": " + ex.Message + " - using built-in defaults.");
            }

            var defaults = DefaultTiers();
            Save(defaults);
            return defaults;
        }

        public static void Save(List<PdfTier> tiers)
        {
            try
            {
                string dir = Path.GetDirectoryName(ConfigPath);
                if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
                File.WriteAllText(ConfigPath, Serialize(tiers), Encoding.UTF8);
                Logger.Info(Source, "Saved " + tiers.Count + " tier(s) to " + ConfigPath);
            }
            catch (Exception ex)
            {
                Logger.Error(Source, "Could not save " + ConfigPath + ": " + ex.Message);
            }
        }

        /// <summary>Returns the first tier whose MaxAgeDays >= ageInDays, or the last (catch-all) tier.</summary>
        public static PdfTier ForAge(List<PdfTier> tiers, double ageInDays)
        {
            foreach (var t in tiers)
                if (t.MaxAgeDays.HasValue && ageInDays <= t.MaxAgeDays.Value)
                    return t;
            return tiers.Count > 0 ? tiers[tiers.Count - 1] : null;
        }

        private static readonly Regex ObjectPattern = new Regex(@"\{(?<body>[^{}]*)\}", RegexOptions.Singleline);
        private static readonly Regex PairPattern = new Regex(
            "\"(?<key>(?:[^\"\\\\]|\\\\.)+)\"\\s*:\\s*(?:\"(?<sval>(?:[^\"\\\\]|\\\\.)*)\"|(?<nval>null|-?\\d+))",
            RegexOptions.Singleline);

        private static string Serialize(List<PdfTier> tiers)
        {
            var sb = new StringBuilder("[\n");
            for (int i = 0; i < tiers.Count; i++)
            {
                var t = tiers[i];
                sb.Append("  { \"maxAgeDays\": ").Append(t.MaxAgeDays.HasValue ? t.MaxAgeDays.Value.ToString() : "null")
                  .Append(", \"tag\": ").Append(Quote(t.Tag))
                  .Append(", \"bg\": ").Append(Quote(t.BgColorHex))
                  .Append(", \"fg\": ").Append(Quote(t.FgColorHex))
                  .Append(" }");
                if (i < tiers.Count - 1) sb.Append(",");
                sb.Append("\n");
            }
            sb.Append("]\n");
            return sb.ToString();
        }

        private static List<PdfTier> Deserialize(string json)
        {
            var result = new List<PdfTier>();
            foreach (Match objMatch in ObjectPattern.Matches(json))
            {
                string body = objMatch.Groups["body"].Value;
                int? maxAge = null;
                string tag = null, bg = "", fg = "";

                foreach (Match pair in PairPattern.Matches(body))
                {
                    string key = pair.Groups["key"].Value;
                    string sval = pair.Groups["sval"].Success ? Unescape(pair.Groups["sval"].Value) : null;
                    string nval = pair.Groups["nval"].Success ? pair.Groups["nval"].Value : null;

                    switch (key)
                    {
                        case "maxAgeDays": maxAge = (nval == null || nval == "null") ? (int?)null : int.Parse(nval); break;
                        case "tag": tag = sval; break;
                        case "bg": bg = sval ?? ""; break;
                        case "fg": fg = sval ?? ""; break;
                    }
                }

                if (tag != null) result.Add(new PdfTier(maxAge, tag, bg, fg));
            }
            return result;
        }

        private static string Quote(string s)
        {
            s = s ?? "";
            var sb = new StringBuilder("\"");
            foreach (char c in s)
            {
                if (c == '\\') sb.Append("\\\\");
                else if (c == '"') sb.Append("\\\"");
                else sb.Append(c);
            }
            sb.Append("\"");
            return sb.ToString();
        }

        private static string Unescape(string s) => s.Replace("\\\"", "\"").Replace("\\\\", "\\");
    }
}


// ===== FILE: Core\PdfProjectConfig.cs =====
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using PlotExtra.Logging;

namespace PlotExtra.Core
{
    /// <summary>
    /// One project entry: a display name plus one or more named output
    /// locations (e.g. "_pdf", "_EW"), ported from the LOCAL_DIRS dicts in
    /// PDF_Project.py's DEFAULT_SP_CONFIG - Beu and Tp in particular have
    /// exactly this shape (Tp: both "_pdf" and an "_EW" hydraulics-calc
    /// folder). Most projects only need a single location, so the simple
    /// CRUD grid (PdfProjectManagerForm) only edits <see cref="Path"/>,
    /// which reads/writes the "_pdf" entry - multi-location projects are
    /// seeded once from the data below and edited by hand in the JSON file
    /// if their extra folders ever move.
    /// </summary>
    public class PdfProject : INotifyPropertyChanged
    {
        private string _name;

        public string Name
        {
            get => _name;
            set { _name = value; OnChanged(nameof(Name)); }
        }

        /// <summary>Label (e.g. "_pdf", "_EW") -> folder path.</summary>
        [Browsable(false)]
        public Dictionary<string, string> Locations { get; set; } = new Dictionary<string, string>();

        /// <summary>The primary/default location ("_pdf" if present, else the first one). Bound by the simple CRUD grid.</summary>
        [Browsable(true)]
        public string Path
        {
            get => Locations.TryGetValue("_pdf", out var p) ? p : Locations.Values.FirstOrDefault() ?? "";
            set => Locations["_pdf"] = value;
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnChanged(string prop) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));

        public PdfProject() { }

        public PdfProject(string name, string path)
        {
            _name = name;
            Locations["_pdf"] = path;
        }

        public PdfProject(string name, Dictionary<string, string> locations)
        {
            _name = name;
            Locations = new Dictionary<string, string>(locations);
        }

        public PdfProject Clone()
        {
            var clone = new PdfProject(Name, new Dictionary<string, string>(Locations));
            return clone;
        }

        public override string ToString() => Name;
    }

    /// <summary>
    /// Loads/saves the project list as a small JSON sidecar under the
    /// user's AppData folder, seeded on first run with the same projects
    /// (and, where PDF_Project.py's DEFAULT_SP_CONFIG defined more than one
    /// LOCAL_DIRS entry, the same multiple named locations) it ships with.
    /// Also provides the "Create Today Folder" helper (a dated yyyyMMdd
    /// subfolder under whichever location is selected).
    /// </summary>
    public static class PdfProjectStore
    {
        private const string Source = "PdfProjectStore";

        private static string ConfigPath => System.IO.Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "PlotExtra", "pdf_projects.json");

        /// <summary>
        /// Ported from PDF_Project.py's DEFAULT_PROJECTS (single-location
        /// projects) merged with DEFAULT_SP_CONFIG's LOCAL_DIRS (multi-location
        /// projects: Beu, Bis, Tp_EP, Npl, Fal, La each also have their real
        /// second/third folder where DEFAULT_SP_CONFIG defined one).
        /// </summary>
        private static List<PdfProject> DefaultProjects() => new List<PdfProject>
        {
            new PdfProject("Beu", new Dictionary<string, string>
            {
                ["_pdf"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_Beu\EP\_pdf",
            }),
            new PdfProject("Tp_EP", new Dictionary<string, string>
            {
                ["_pdf"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_Tp\EP\_pdf",
                ["_EW"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_Tp\EP\EW\hydarulische_berechnung",
            }),
            new PdfProject("Tp_GP", @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_Tp\GP\_pdf"),
            new PdfProject("Npl", new Dictionary<string, string>
            {
                ["_pdf"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_Npl\EP\_pdf",
                ["_EW"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_Npl\EP\EW\hydarulische_berechnung",
            }),
            new PdfProject("Bis", new Dictionary<string, string>
            {
                ["_pdf"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_Bis\EP\_pdf",
            }),
            new PdfProject("La", new Dictionary<string, string>
            {
                ["_pdf"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_La\EP\_pdf",
                ["_EW"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P_La\EP\EW\Hydarulische_berechnung",
            }),
            new PdfProject("Fal", new Dictionary<string, string>
            {
                ["_pdf"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P\EP\_pdf",
                ["_EW"] = @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P_B001595P\EP\Entwässerung\Frankfurter_Allee",
            }),
            new PdfProject("Riedbahn", @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P-KA02545P_Riedbahn_MA_Gleisern\EP\_pdf"),
            new PdfProject("SIs",      @"C:\Users\mateid\OneDrive - DB E.C.O. Group\Lucru\P-B004278P_NVT_Nord\GUw_Sls\EP\_pdf"),
        };

        public static List<PdfProject> Load()
        {
            try
            {
                if (File.Exists(ConfigPath))
                {
                    string json = File.ReadAllText(ConfigPath, Encoding.UTF8);
                    var loaded = MiniJson.DeserializeProjects(json);
                    if (loaded != null && loaded.Count > 0)
                        return loaded;
                }
            }
            catch (Exception ex)
            {
                Logger.Warn(Source, "Could not read " + ConfigPath + ": " + ex.Message + " - using built-in defaults.");
            }

            var defaults = DefaultProjects();
            Save(defaults);
            return defaults;
        }

        public static void Save(List<PdfProject> projects)
        {
            try
            {
                string dir = System.IO.Path.GetDirectoryName(ConfigPath);
                if (!string.IsNullOrEmpty(dir))
                    Directory.CreateDirectory(dir);

                File.WriteAllText(ConfigPath, MiniJson.SerializeProjects(projects), Encoding.UTF8);
                Logger.Info(Source, "Saved " + projects.Count + " project(s) to " + ConfigPath);
            }
            catch (Exception ex)
            {
                Logger.Error(Source, "Could not save " + ConfigPath + ": " + ex.Message);
            }
        }

        /// <summary>
        /// Creates (or reuses) a dated subfolder under the given location
        /// path, e.g. "...\_pdf\20260709", and returns its full path.
        /// </summary>
        public static string CreateTodayFolder(string locationPath)
        {
            if (string.IsNullOrWhiteSpace(locationPath))
                throw new ArgumentException("This project/location has no configured path.");

            string todayFolder = System.IO.Path.Combine(locationPath, DateTime.Now.ToString("yyyyMMdd"));
            Directory.CreateDirectory(todayFolder);
            Logger.Info(Source, "Today folder ready: " + todayFolder);
            return todayFolder;
        }
    }

    /// <summary>
    /// Tiny regex-based JSON reader/writer for the one shape this project
    /// needs - a flat array of objects, each with a "name" string and any
    /// number of "loc_&lt;label&gt;" string fields (one per entry in
    /// PdfProject.Locations, e.g. "loc__pdf", "loc__EW") - so the project
    /// doesn't need a Newtonsoft.Json / System.Text.Json package reference
    /// just for this. Kept deliberately flat (no nested JSON objects) so a
    /// simple regex pass is enough, no recursive-descent parser needed.
    /// Old-format files (plain "name"/"path", no "loc_" prefix) still load
    /// correctly - "path" is treated as the "_pdf" location.
    /// </summary>
    internal static class MiniJson
    {
        // One object's worth of "key": "value" pairs, values may contain escaped quotes/backslashes.
        private static readonly Regex ObjectPattern = new Regex(@"\{(?<body>[^{}]*)\}", RegexOptions.Singleline);
        private static readonly Regex PairPattern = new Regex(
            "\"(?<key>(?:[^\"\\\\]|\\\\.)+)\"\\s*:\\s*\"(?<val>(?:[^\"\\\\]|\\\\.)*)\"", RegexOptions.Singleline);

        public static string SerializeProjects(List<PdfProject> projects)
        {
            var sb = new StringBuilder();
            sb.Append("[\n");
            for (int i = 0; i < projects.Count; i++)
            {
                var p = projects[i];
                sb.Append("  { \"name\": ").Append(Quote(p.Name));
                foreach (var kvp in p.Locations)
                    sb.Append(", \"loc_").Append(kvp.Key).Append("\": ").Append(Quote(kvp.Value));
                sb.Append(" }");
                if (i < projects.Count - 1) sb.Append(",");
                sb.Append("\n");
            }
            sb.Append("]\n");
            return sb.ToString();
        }

        public static List<PdfProject> DeserializeProjects(string json)
        {
            var result = new List<PdfProject>();

            foreach (Match objMatch in ObjectPattern.Matches(json))
            {
                string body = objMatch.Groups["body"].Value;
                string name = null;
                string legacyPath = null;
                var locations = new Dictionary<string, string>();

                foreach (Match pair in PairPattern.Matches(body))
                {
                    string key = Unescape(pair.Groups["key"].Value);
                    string val = Unescape(pair.Groups["val"].Value);

                    if (key == "name") name = val;
                    else if (key == "path") legacyPath = val;
                    else if (key.StartsWith("loc_", StringComparison.Ordinal))
                        locations[key.Substring(4)] = val;
                }

                if (string.IsNullOrEmpty(name)) continue;
                if (locations.Count == 0 && legacyPath != null) locations["_pdf"] = legacyPath;

                result.Add(new PdfProject(name, locations));
            }

            return result;
        }

        private static string Quote(string s)
        {
            if (s == null) s = "";
            var sb = new StringBuilder("\"");
            foreach (char c in s)
            {
                switch (c)
                {
                    case '\\': sb.Append("\\\\"); break;
                    case '"': sb.Append("\\\""); break;
                    case '\n': sb.Append("\\n"); break;
                    case '\r': break;
                    case '\t': sb.Append("\\t"); break;
                    default: sb.Append(c); break;
                }
            }
            sb.Append("\"");
            return sb.ToString();
        }

        private static string Unescape(string s)
        {
            return s.Replace("\\\"", "\"").Replace("\\n", "\n").Replace("\\t", "\t").Replace("\\\\", "\\");
        }
    }
}


// ===== FILE: UI\PdfTierEditorForm.cs =====
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using PlotExtra.Core;

namespace PlotExtra.UI
{
    /// <summary>
    /// Edit the age-tier brackets used to color-code the PDF file list by
    /// how recently each file was modified (ported concept from
    /// PDF_Project.py's tier editor - "Configure age tier colours").
    /// Colors are edited as hex text (e.g. "#28a745") rather than a full
    /// color-picker button-per-row, to keep this at the same scope as
    /// PdfProjectManagerForm.
    /// </summary>
    public class PdfTierEditorForm : Form
    {
        private readonly List<PdfTier> _target;
        private readonly BindingList<PdfTier> _working;
        private readonly DataGridView _grid;

        public PdfTierEditorForm(List<PdfTier> tiers)
        {
            _target = tiers;
            _working = new BindingList<PdfTier>();
            foreach (var t in tiers) _working.Add(t.Clone());

            Text = "Configure Age Tier Colours";
            FormBorderStyle = FormBorderStyle.Sizable;
            MaximizeBox = true;
            MinimizeBox = true;
            StartPosition = FormStartPosition.CenterParent;
            Size = new Size(560, 380);

            var hint = new Label
            {
                Dock = DockStyle.Top,
                Height = 40,
                Padding = new Padding(8),
                Text = "Files this many days old (or newer) get this row's colors in the PDF preview list. " +
                       "Leave the last row's \"Max age (days)\" blank for the catch-all \"everything older\" tier.",
                AutoSize = false,
            };

            var layout = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 3, ColumnCount = 1 };
            layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
            layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));

            _grid = new DataGridView
            {
                Dock = DockStyle.Fill,
                DataSource = _working,
                AutoGenerateColumns = true,
                AllowUserToAddRows = false,
                RowHeadersVisible = false,
                AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
            };
            layout.Controls.Add(_grid, 0, 0);

            var rowButtons = new FlowLayoutPanel { AutoSize = true, Padding = new Padding(4) };
            var add = Theme.CreateButton("Add tier", AppIcon.Add, 100);
            var del = Theme.CreateButton("Delete", AppIcon.Delete, 90);
            C3DTheme.Role(add, C3DTheme.RoleRefresh);
            C3DTheme.Role(del, C3DTheme.RoleDanger);
            add.Click += (s, e) => _working.Insert(Math.Max(0, _working.Count - 1), new PdfTier(7, "custom", "#cccccc", ""));
            del.Click += (s, e) => { int i = _grid.CurrentRow?.Index ?? -1; if (i >= 0) _working.RemoveAt(i); };
            rowButtons.Controls.AddRange(new Control[] { add, del });
            layout.Controls.Add(rowButtons, 0, 1);

            var okRow = new FlowLayoutPanel { AutoSize = true, Padding = new Padding(4), FlowDirection = FlowDirection.RightToLeft };
            var cancelBtn = Theme.CreateButton("Cancel", AppIcon.Cancel, 90);
            var okBtn = Theme.CreateButton("OK", AppIcon.Ok, 90);
            cancelBtn.DialogResult = DialogResult.Cancel;
            C3DTheme.Role(cancelBtn, C3DTheme.RoleDanger);
            C3DTheme.Role(okBtn, C3DTheme.RolePrimary);
            okBtn.Click += (s, e) => CommitAndClose();
            okRow.Controls.Add(cancelBtn);
            okRow.Controls.Add(okBtn);
            layout.Controls.Add(okRow, 0, 2);

            Controls.Add(layout);
            Controls.Add(hint);

            AcceptButton = okBtn;
            CancelButton = cancelBtn;

            C3DTheme.Apply(this, C3DPalette.Dark);
        }

        private void CommitAndClose()
        {
            _target.Clear();
            foreach (var t in _working) _target.Add(t);
            PdfTierStore.Save(_target);
            DialogResult = DialogResult.OK;
            Close();
        }
    }
}


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

namespace PlotExtra.UI
{
    /// <summary>
    /// Palette + fonts, ported field-for-field from ParallelPipesNet's
    /// C3DTheme.cs (itself ported from C3D_UI_Style_Template.py's
    /// C3D_DARK / C3D_LIGHT dicts and FONT dict), so PlotExtra's dialogs
    /// match the rest of the C3D toolset visually.
    /// </summary>
    public class C3DPalette
    {
        public Color Bg, Panel, Border, HeaderBg, Bg2, Bg3, Fg, Muted, Accent, TextW,
                     SelBg, SelFg, CodeBg, CodeFg, Entry, Blue, Green, Red, Orange, Purple, Grey, Reload;

        public static C3DPalette Dark { get; } = new C3DPalette
        {
            Bg = FromHex("#2b2d3e"),
            Panel = FromHex("#23253a"),
            Border = FromHex("#3e4060"),
            HeaderBg = FromHex("#1e2030"),
            Bg2 = FromHex("#23253a"),
            Bg3 = FromHex("#1e2030"),
            Fg = FromHex("#dde1f0"),
            Muted = FromHex("#7a7f9a"),
            Accent = FromHex("#2979d4"),
            TextW = FromHex("#ffffff"),
            SelBg = FromHex("#3a5fa0"),
            SelFg = FromHex("#ffffff"),
            CodeBg = FromHex("#1e2030"),
            CodeFg = FromHex("#dde1f0"),
            Entry = FromHex("#1e2030"),
            Blue = FromHex("#2979d4"),
            Green = FromHex("#2a9e52"),
            Red = FromHex("#c0392b"),
            Orange = FromHex("#e06c2a"),
            Purple = FromHex("#7c4dbd"),
            Grey = FromHex("#3e4060"),
            Reload = FromHex("#4a5070"),
        };

        public static C3DPalette Light { get; } = new C3DPalette
        {
            Bg = FromHex("#ffffff"),
            Panel = FromHex("#f6f8fa"),
            Border = FromHex("#d0d7de"),
            HeaderBg = FromHex("#f6f8fa"),
            Bg2 = FromHex("#f6f8fa"),
            Bg3 = FromHex("#eaeef2"),
            Fg = FromHex("#1f2328"),
            Muted = FromHex("#57606a"),
            Accent = FromHex("#0969da"),
            TextW = FromHex("#ffffff"),
            SelBg = FromHex("#0969da"),
            SelFg = FromHex("#ffffff"),
            CodeBg = FromHex("#f6f8fa"),
            CodeFg = FromHex("#1f2328"),
            Entry = FromHex("#ffffff"),
            Blue = FromHex("#0969da"),
            Green = FromHex("#2a9e52"),
            Red = FromHex("#c0392b"),
            Orange = FromHex("#e06c2a"),
            Purple = FromHex("#7c4dbd"),
            Grey = FromHex("#d0d7de"),
            Reload = FromHex("#afb8c1"),
        };

        private static Color FromHex(string hex) => ColorTranslator.FromHtml(hex);
    }

    public static class C3DFonts
    {
        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", 9.5f);
        public static readonly Font CodeBig = new Font("Consolas", 10.5f);
    }

    /// <summary>
    /// Recursive, role-based theming for WinForms controls (ported from
    /// ParallelPipesNet.UI.C3DTheme). A control's "role" is read from
    /// Control.Tag as a string, e.g. button.Tag = "primary".
    /// </summary>
    public static class C3DTheme
    {
        public const string RoleHeader = "header";
        public const string RoleHeaderTitle = "header_title";
        public const string RoleHeaderPath = "header_path";
        public const string RoleMuted = "muted";
        public const string RolePrimary = "primary";
        public const string RoleRefresh = "refresh";
        public const string RoleDanger = "danger";
        public const string RoleWarn = "warn";
        public const string RoleNeutral = "neutral";
        public const string RolePurple = "purple";
        public const string RoleReload = "reload";

        public static Color Lighten(Color c, int amount = 22)
        {
            int r = Math.Min(255, c.R + amount);
            int g = Math.Min(255, c.G + amount);
            int b = Math.Min(255, c.B + amount);
            return Color.FromArgb(c.A, r, g, b);
        }

        /// <summary>Attaches a role string to a control. Returns the same control for chaining.</summary>
        public static T Role<T>(T control, string roleName) where T : Control
        {
            control.Tag = roleName;
            return control;
        }

        public static string GetRole(Control c) => c.Tag as string ?? string.Empty;

        /// <summary>Recursively applies the palette to a control tree.</summary>
        public static void Apply(Control root, C3DPalette C)
        {
            string roleName = GetRole(root);

            try
            {
                switch (root)
                {
                    case TabControl tc:
                        tc.BackColor = C.Panel;
                        StyleTabControl(tc, C);
                        break;

                    case Button btn:
                        StyleButton(btn, roleName, C);
                        break;

                    case CheckBox cb:
                        cb.BackColor = C.Panel;
                        cb.ForeColor = C.Fg;
                        cb.Font = C3DFonts.Normal;
                        cb.FlatStyle = FlatStyle.Flat;
                        break;

                    case RadioButton rb:
                        rb.BackColor = C.Panel;
                        rb.ForeColor = C.Fg;
                        rb.Font = C3DFonts.Normal;
                        rb.FlatStyle = FlatStyle.Flat;
                        break;

                    case ComboBox combo:
                        combo.BackColor = C.Entry;
                        combo.ForeColor = C.Fg;
                        combo.FlatStyle = FlatStyle.Flat;
                        combo.Font = C3DFonts.Normal;
                        break;

                    case NumericUpDown num:
                        num.BackColor = C.Entry;
                        num.ForeColor = C.Fg;
                        num.Font = C3DFonts.Normal;
                        num.BorderStyle = BorderStyle.FixedSingle;
                        break;

                    case TextBox tb:
                        tb.BackColor = C.Entry;
                        tb.ForeColor = C.Fg;
                        tb.BorderStyle = BorderStyle.FixedSingle;
                        tb.Font = C3DFonts.Normal;
                        break;

                    case ListBox lb:
                        lb.BackColor = C.CodeBg;
                        lb.ForeColor = C.CodeFg;
                        lb.BorderStyle = BorderStyle.FixedSingle;
                        lb.Font = C3DFonts.Code;
                        break;

                    case DataGridView grid:
                        StyleGrid(grid, C);
                        break;

                    case GroupBox gb:
                        gb.BackColor = C.Panel;
                        gb.ForeColor = C.Accent;
                        gb.Font = C3DFonts.ButtonBold;
                        break;

                    case Label lbl:
                        StyleLabel(lbl, roleName, C);
                        break;

                    case PreviewControl _:
                        // Keeps its own white paper / hatched-margin drawing colors -
                        // it derives from Control, so exclude it here or it would get
                        // repainted with the generic panel background below.
                        break;

                    case Panel pnl:
                        pnl.BackColor = roleName == RoleHeader ? C.HeaderBg : C.Panel;
                        break;

                    case Form frm:
                        frm.BackColor = C.Bg;
                        break;

                    default:
                        if (root is TableLayoutPanel || root is FlowLayoutPanel)
                            root.BackColor = C.Panel;
                        break;
                }
            }
            catch
            {
                // Best-effort styling - a control that doesn't support one of these
                // properties shouldn't abort theming the rest of the tree.
            }

            foreach (Control child in root.Controls)
                Apply(child, C);
        }

        private static void StyleLabel(Label lbl, string roleName, C3DPalette C)
        {
            switch (roleName)
            {
                case RoleHeaderTitle:
                    lbl.BackColor = C.HeaderBg;
                    lbl.ForeColor = C.TextW;
                    lbl.Font = C3DFonts.Title;
                    break;
                case RoleHeaderPath:
                    lbl.BackColor = C.HeaderBg;
                    lbl.ForeColor = C.Muted;
                    lbl.Font = C3DFonts.Small;
                    break;
                case RoleMuted:
                case "hint":
                    lbl.BackColor = C.Panel;
                    lbl.ForeColor = C.Muted;
                    lbl.Font = C3DFonts.Small;
                    break;
                default:
                    lbl.BackColor = C.Panel;
                    lbl.ForeColor = C.Fg;
                    lbl.Font = C3DFonts.Normal;
                    break;
            }
        }

        /// <summary>One of neutral / primary / refresh / danger / warn.</summary>
        public static Button StyleButton(Button button, string roleName, C3DPalette C)
        {
            roleName = string.IsNullOrEmpty(roleName) ? RoleNeutral : roleName.ToLowerInvariant();

            button.FlatStyle = FlatStyle.Flat;
            button.FlatAppearance.BorderSize = 0;
            button.Cursor = Cursors.Hand;
            button.Padding = new Padding(6, 2, 6, 2);
            button.UseVisualStyleBackColor = false;

            switch (roleName)
            {
                case RolePrimary:
                case "ok":
                case "success":
                    button.BackColor = C.Green;
                    button.ForeColor = C.TextW;
                    button.Font = C3DFonts.ButtonBold;
                    button.FlatAppearance.MouseOverBackColor = Lighten(C.Green);
                    button.FlatAppearance.MouseDownBackColor = Lighten(C.Green, 10);
                    break;
                case RoleRefresh:
                case "info":
                case "blue":
                    button.BackColor = C.Blue;
                    button.ForeColor = C.TextW;
                    button.FlatAppearance.MouseOverBackColor = Lighten(C.Blue);
                    button.FlatAppearance.MouseDownBackColor = Lighten(C.Blue, 10);
                    break;
                case RoleDanger:
                case "delete":
                case "revert":
                    button.BackColor = C.Red;
                    button.ForeColor = C.TextW;
                    button.Font = C3DFonts.ButtonBold;
                    button.FlatAppearance.MouseOverBackColor = Lighten(C.Red);
                    button.FlatAppearance.MouseDownBackColor = Lighten(C.Red, 10);
                    break;
                case RoleWarn:
                case "warning":
                    button.BackColor = C.Orange;
                    button.ForeColor = C.TextW;
                    button.FlatAppearance.MouseOverBackColor = Lighten(C.Orange);
                    button.FlatAppearance.MouseDownBackColor = Lighten(C.Orange, 10);
                    break;
                case RolePurple:
                    button.BackColor = C.Purple;
                    button.ForeColor = C.TextW;
                    button.FlatAppearance.MouseOverBackColor = Lighten(C.Purple);
                    button.FlatAppearance.MouseDownBackColor = Lighten(C.Purple, 10);
                    break;
                case RoleReload:
                    button.BackColor = C.Reload;
                    button.ForeColor = C.TextW;
                    button.FlatAppearance.MouseOverBackColor = Lighten(C.Reload);
                    button.FlatAppearance.MouseDownBackColor = Lighten(C.Reload, 10);
                    break;
                default:
                    button.BackColor = C.Panel;
                    button.ForeColor = C.Fg;
                    button.FlatAppearance.MouseOverBackColor = C.Border;
                    button.FlatAppearance.MouseDownBackColor = Lighten(C.Border, 10);
                    break;
            }

            // Re-tint the icon (if this button was created via Theme.CreateButton)
            // to match the foreground color just chosen above, instead of leaving
            // it at ButtonIconFactory's default dark-gray - which reads poorly on
            // the green/blue/orange role backgrounds.
            if (Theme.TryGetIcon(button, out AppIcon icon))
                button.Image = ButtonIconFactory.Get(icon, 16, button.ForeColor);

            return button;
        }

        private static void StyleGrid(DataGridView grid, C3DPalette C)
        {
            grid.BackgroundColor = C.CodeBg;
            grid.ForeColor = C.Fg;
            grid.GridColor = C.Border;
            grid.BorderStyle = BorderStyle.FixedSingle;
            grid.EnableHeadersVisualStyles = false;
            grid.ColumnHeadersDefaultCellStyle.BackColor = C.Bg2;
            grid.ColumnHeadersDefaultCellStyle.ForeColor = C.Fg;
            grid.ColumnHeadersDefaultCellStyle.Font = C3DFonts.ButtonBold;
            grid.DefaultCellStyle.BackColor = C.CodeBg;
            grid.DefaultCellStyle.ForeColor = C.CodeFg;
            grid.DefaultCellStyle.SelectionBackColor = C.SelBg;
            grid.DefaultCellStyle.SelectionForeColor = C.SelFg;
            grid.DefaultCellStyle.Font = C3DFonts.Normal;
            grid.RowHeadersDefaultCellStyle.BackColor = C.Bg2;
            grid.AlternatingRowsDefaultCellStyle.BackColor = C.Panel;
        }

        /// <summary>
        /// Owner-draws the TabControl's tab strip - standard WinForms
        /// TabControl ignores BackColor for the native tab strip, so this is
        /// the only reliable way to theme it.
        /// </summary>
        public static void StyleTabControl(TabControl tc, C3DPalette C)
        {
            tc.DrawMode = TabDrawMode.OwnerDrawFixed;
            tc.Padding = new Point(14, 5);
            tc.DrawItem += (s, e) =>
            {
                var page = tc.TabPages[e.Index];
                bool selected = e.Index == tc.SelectedIndex;
                var tabRect = tc.GetTabRect(e.Index);

                using (var bg = new SolidBrush(selected ? C.Panel : C.Bg2))
                    e.Graphics.FillRectangle(bg, tabRect);

                var textColor = selected ? C.Accent : C.TextW;
                TextRenderer.DrawText(e.Graphics, page.Text, C3DFonts.Normal, tabRect, textColor,
                    TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
            };
            tc.SelectedIndexChanged += (s, e) => tc.Invalidate();
        }
    }
}


// ===== FILE: UI\ButtonIconFactory.cs =====
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace PlotExtra.UI
{
    /// <summary>Enumerates every icon the app needs on a button.</summary>
    public enum AppIcon
    {
        Save,
        Reload,
        Open,
        Add,
        Delete,
        Apply,
        Cancel,
        Settings,
        Warning,
        Export,
        Import,
        Copy,
        Move,
        Up,
        Down,
        Ok,
        Help,
        Preview,
        Properties
    }

    /// <summary>
    /// Generates small (16x16 by default) bitmap icons entirely in code with
    /// GDI+. This avoids emoji / Unicode glyphs in Button.Text (which render
    /// inconsistently across Windows font/emoji-font versions) and avoids
    /// needing an external image/resource pipeline.
    /// Icons are cached per (icon, size, color) the first time they're requested.
    /// </summary>
    public static class ButtonIconFactory
    {
        private static readonly Dictionary<string, Bitmap> _cache = new Dictionary<string, Bitmap>();

        public static Bitmap Get(AppIcon icon, int size = 16, Color? color = null)
        {
            Color c = color ?? Color.FromArgb(64, 64, 64);
            string key = icon + "_" + size + "_" + c.ToArgb();

            if (_cache.TryGetValue(key, out var cached))
                return cached;

            var bmp = new Bitmap(size, size);
            using (var g = Graphics.FromImage(bmp))
            {
                g.SmoothingMode = SmoothingMode.AntiAlias;
                g.PixelOffsetMode = PixelOffsetMode.HighQuality;
                g.Clear(Color.Transparent);

                using (var pen = new Pen(c, Math.Max(1f, size / 10f)))
                using (var brush = new SolidBrush(c))
                {
                    Draw(icon, g, pen, brush, size);
                }
            }

            _cache[key] = bmp;
            return bmp;
        }

        private static void Draw(AppIcon icon, Graphics g, Pen pen, SolidBrush brush, int s)
        {
            float m = s * 0.15f;      // margin
            float w = s - 2 * m;      // usable width

            switch (icon)
            {
                case AppIcon.Save:
                    // floppy disk outline
                    g.DrawRectangle(pen, m, m, w, w);
                    g.FillRectangle(brush, m + w * 0.2f, m, w * 0.6f, w * 0.35f);
                    g.DrawRectangle(pen, m + w * 0.15f, m + w * 0.55f, w * 0.7f, w * 0.35f);
                    break;

                case AppIcon.Reload:
                    // circular arrow
                    g.DrawArc(pen, m, m, w, w, -30, 270);
                    DrawArrowHead(g, brush, new PointF(m + w, m + w * 0.5f), 225, s * 0.22f);
                    break;

                case AppIcon.Open:
                    // folder
                    g.DrawLine(pen, m, m + w * 0.25f, m + w * 0.4f, m + w * 0.25f);
                    g.DrawLine(pen, m + w * 0.4f, m + w * 0.25f, m + w * 0.5f, m + w * 0.4f);
                    g.DrawRectangle(pen, m, m + w * 0.4f, w, w * 0.5f);
                    break;

                case AppIcon.Add:
                    g.DrawLine(pen, m + w / 2f, m, m + w / 2f, m + w);
                    g.DrawLine(pen, m, m + w / 2f, m + w, m + w / 2f);
                    break;

                case AppIcon.Delete:
                    g.DrawRectangle(pen, m + w * 0.15f, m + w * 0.2f, w * 0.7f, w * 0.75f);
                    g.DrawLine(pen, m, m + w * 0.2f, m + w, m + w * 0.2f);
                    g.DrawLine(pen, m + w * 0.35f, m, m + w * 0.65f, m);
                    break;

                case AppIcon.Apply:
                case AppIcon.Ok:
                    g.DrawLine(pen, m, m + w * 0.55f, m + w * 0.4f, m + w);
                    g.DrawLine(pen, m + w * 0.4f, m + w, m + w, m);
                    break;

                case AppIcon.Cancel:
                    g.DrawLine(pen, m, m, m + w, m + w);
                    g.DrawLine(pen, m + w, m, m, m + w);
                    break;

                case AppIcon.Settings:
                    // gear-ish: circle with ticks
                    g.DrawEllipse(pen, m + w * 0.2f, m + w * 0.2f, w * 0.6f, w * 0.6f);
                    for (int i = 0; i < 8; i++)
                    {
                        double a = i * Math.PI / 4;
                        float cx = m + w / 2f, cy = m + w / 2f;
                        float r1 = w * 0.4f, r2 = w * 0.5f;
                        g.DrawLine(pen,
                            (float)(cx + r1 * Math.Cos(a)), (float)(cy + r1 * Math.Sin(a)),
                            (float)(cx + r2 * Math.Cos(a)), (float)(cy + r2 * Math.Sin(a)));
                    }
                    break;

                case AppIcon.Warning:
                    var tri = new[]
                    {
                        new PointF(m + w / 2f, m),
                        new PointF(m, m + w),
                        new PointF(m + w, m + w)
                    };
                    using (var wp = new Pen(Color.FromArgb(200, 150, 0), pen.Width))
                        g.DrawPolygon(wp, tri);
                    using (var wb = new SolidBrush(Color.FromArgb(200, 150, 0)))
                    {
                        g.FillRectangle(wb, m + w * 0.46f, m + w * 0.35f, w * 0.08f, w * 0.3f);
                        g.FillEllipse(wb, m + w * 0.44f, m + w * 0.75f, w * 0.12f, w * 0.12f);
                    }
                    break;

                case AppIcon.Export:
                    g.DrawRectangle(pen, m, m + w * 0.35f, w * 0.65f, w * 0.65f);
                    g.DrawLine(pen, m + w * 0.55f, m, m + w, m);
                    g.DrawLine(pen, m + w, m, m + w, m + w * 0.45f);
                    g.DrawLine(pen, m + w * 0.55f, m + w * 0.45f, m + w, m);
                    break;

                case AppIcon.Import:
                    g.DrawRectangle(pen, m, m, w * 0.65f, w * 0.65f);
                    g.DrawLine(pen, m + w * 0.55f, m + w * 0.55f, m + w, m + w);
                    g.DrawLine(pen, m + w * 0.6f, m + w, m + w, m + w);
                    g.DrawLine(pen, m + w, m + w * 0.6f, m + w, m + w);
                    break;

                case AppIcon.Copy:
                    g.DrawRectangle(pen, m, m + w * 0.25f, w * 0.65f, w * 0.65f);
                    g.DrawRectangle(pen, m + w * 0.3f, m, w * 0.65f, w * 0.65f);
                    break;

                case AppIcon.Move:
                    g.DrawLine(pen, m + w / 2f, m, m + w / 2f, m + w);
                    g.DrawLine(pen, m, m + w / 2f, m + w, m + w / 2f);
                    DrawArrowHead(g, brush, new PointF(m + w / 2f, m), 270, s * 0.14f);
                    DrawArrowHead(g, brush, new PointF(m + w / 2f, m + w), 90, s * 0.14f);
                    DrawArrowHead(g, brush, new PointF(m, m + w / 2f), 180, s * 0.14f);
                    DrawArrowHead(g, brush, new PointF(m + w, m + w / 2f), 0, s * 0.14f);
                    break;

                case AppIcon.Up:
                    g.DrawLine(pen, m + w / 2f, m + w, m + w / 2f, m);
                    DrawArrowHead(g, brush, new PointF(m + w / 2f, m), 270, s * 0.22f);
                    break;

                case AppIcon.Down:
                    g.DrawLine(pen, m + w / 2f, m, m + w / 2f, m + w);
                    DrawArrowHead(g, brush, new PointF(m + w / 2f, m + w), 90, s * 0.22f);
                    break;

                case AppIcon.Help:
                    g.DrawEllipse(pen, m, m, w, w);
                    using (var f = new Font("Segoe UI", s * 0.5f, System.Drawing.FontStyle.Bold))
                        g.DrawString("?", f, brush, m + w * 0.22f, m + w * 0.05f);
                    break;

                case AppIcon.Preview:
                    g.DrawEllipse(pen, m, m + w * 0.2f, w, w * 0.6f);
                    g.DrawEllipse(pen, m + w * 0.35f, m + w * 0.35f, w * 0.3f, w * 0.3f);
                    break;

                case AppIcon.Properties:
                    for (int i = 0; i < 3; i++)
                    {
                        float y = m + i * (w / 2.2f);
                        g.DrawLine(pen, m, y, m + w, y);
                    }
                    break;
            }
        }

        private static void DrawArrowHead(Graphics g, SolidBrush brush, PointF tip, float angleDeg, float size)
        {
            double a = angleDeg * Math.PI / 180.0;
            double a1 = a + 2.5, a2 = a - 2.5;
            var p1 = new PointF(tip.X - (float)(size * Math.Cos(a1)), tip.Y - (float)(size * Math.Sin(a1)));
            var p2 = new PointF(tip.X - (float)(size * Math.Cos(a2)), tip.Y - (float)(size * Math.Sin(a2)));
            g.FillPolygon(brush, new[] { tip, p1, p2 });
        }
    }
}


// ===== FILE: UI\PlotDialogForm.cs =====
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using DrawingFont = System.Drawing.Font;
using WinFormsFlowDirection = System.Windows.Forms.FlowDirection;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.PlottingServices;
using PlotExtra.Core;
using PlotExtra.Logging;
using PlotExtra.Cad;
// Both Autodesk.AutoCAD.ApplicationServices and System.Windows.Forms declare
// a type named "Application" - alias the AutoCAD one so it's unambiguous.
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace PlotExtra.UI
{
    /// <summary>
    /// Replica of the AutoCAD / Civil 3D "Plot" dialog. Every field maps 1:1
    /// onto a PLOTSETTINGS-equivalent value so the dialog can later be wired
    /// to Autodesk.AutoCAD.DatabaseServices.PlotSettings /
    /// Autodesk.AutoCAD.PlottingServices.PlotEngine for a real plot.
    /// For now this focuses on faithfully reproducing the UI + behaviour
    /// requested (preview thumbnail, collapsible right panel, logging).
    /// </summary>
    public class PlotDialogForm : Form
    {
        // ---- Page setup -------------------------------------------------
        private ComboBox _cboPageSetup;
        private Button _btnAddPageSetup;

        // ---- Printer / plotter -------------------------------------------
        private ComboBox _cboPrinter;
        private Button _btnProperties;
        private Label _lblPlotterValue;
        private Label _lblWhereValue;
        private Label _lblDescriptionValue;
        private CheckBox _chkPlotToFile;
        private Button _btnPdfOptions;
        private PreviewControl _paperPreview;
        private string _browseFolder;
        private ListBox _lstPdfFiles;
        private Label _lblPaperPreviewCaption;

        // ---- Plot style table --------------------------------------------
        private ComboBox _cboPlotStyleTable;
        private Button _btnEditPlotStyle;

        // ---- Shaded viewport options --------------------------------------
        private ComboBox _cboShadePlot;
        private ComboBox _cboQuality;
        private TextBox _txtDpi;

        // ---- Paper size / copies ------------------------------------------
        private ComboBox _cboPaperSize;
        private NumericUpDown _numCopies;

        // ---- Plot area / Plot scale ---------------------------------------
        private ComboBox _cboWhatToPlot;
        private Button _btnWindowPick;
        private CheckBox _chkFitToPaper;
        private ComboBox _cboScale;
        private TextBox _txtScaleMm;
        private ComboBox _cboScaleUnitLabel; // "mm ="
        private TextBox _txtScaleUnits;
        private CheckBox _chkScaleLineweights;

        // ---- Plot offset ----------------------------------------------------
        private TextBox _txtOffsetX;
        private TextBox _txtOffsetY;
        private CheckBox _chkCenterPlot;

        // ---- Plot options checkboxes ----------------------------------------
        private CheckBox _chkPlotInBackground;
        private CheckBox _chkPlotObjectLineweights;
        private CheckBox _chkPlotTransparency;
        private CheckBox _chkPlotWithPlotStyles;
        private CheckBox _chkPlotPaperspaceLast;
        private CheckBox _chkHidePaperspaceObjects;
        private CheckBox _chkPlotStampOn;
        private CheckBox _chkSaveChangesToLayout;

        // ---- Drawing orientation ----------------------------------------------
        private RadioButton _radPortrait;
        private RadioButton _radLandscape;
        private CheckBox _chkPlotUpsideDown;

        // ---- Bottom buttons -------------------------------------------------
        private Button _btnPreview;
        private Button _btnApplyToLayout;
        private Button _btnOk;
        private Button _btnCancel;
        private Button _btnHelp;
        private Button _btnCollapse;

        private Panel _rightPanel;
        private TabControl _rightTabs;
        private bool _rightPanelCollapsed;

        // ---- PDF Output tab -------------------------------------------------
        private System.Collections.Generic.List<PdfProject> _projects;
        private ComboBox _cboProject;
        private ComboBox _cboLocation;
        private Label _lblProjectPath;
        private Button _btnCreateTodayFolder;
        private Button _btnOpenTodayFolder;
        private Label _lblTodayFolder;
        private CheckBox _chkSaveDirectlyToFolder;
        private string _todayFolder;
        private System.Collections.Generic.List<PdfTier> _tiers;
        private readonly System.Collections.Generic.List<string> _recentFolders = new System.Collections.Generic.List<string>();
        private readonly string _tcExePath = ResolveTcExe();
        private Button _btnOpenInTc;
        private Button _btnRecent;

        private static readonly string[] TcPaths =
        {
            @"C:\Users\mateid\OneDrive - DB E.C.O. Group\totalcmd\TOTALCMD64.EXE",
            @"C:\Program Files\Total Commander\TOTALCMD64.EXE",
            @"C:\Program Files\Total Commander\TOTALCMD.EXE",
            @"C:\TotalCMD\TOTALCMD64.EXE",
            @"C:\Program Files (x86)\Total Commander\TOTALCMD.EXE",
        };

        /// <summary>
        /// Cached after the first resolve. Previously this probed every
        /// candidate path (including a OneDrive path, which can stall if
        /// that folder isn't locally synced) fresh on *every* dialog open -
        /// one more small contributor to "the program is taking longer to
        /// load" alongside the device/style-sheet re-query above.
        /// </summary>
        private static string _cachedTcExePath;
        private static bool _tcExeResolved;

        private static string ResolveTcExe()
        {
            if (_tcExeResolved) return _cachedTcExePath;
            foreach (var p in TcPaths)
            {
                if (File.Exists(p))
                {
                    _cachedTcExePath = p;
                    break;
                }
            }
            _tcExeResolved = true;
            return _cachedTcExePath;
        }

        private readonly string _layoutName;

        private Point2d? _windowMin;
        private Point2d? _windowMax;

        private double _paperWidthMm = 1350.0;
        private double _paperHeightMm = 594.0;
        private readonly System.Collections.Generic.List<string> _paperSizeCanonicalNames = new System.Collections.Generic.List<string>();

        public PlotDialogForm(string layoutName)
        {
            _layoutName = layoutName ?? "Layout1";
            BuildLayout();
            WireDefaults();

            Logger.Info("PlotDialogForm", "Opened for layout '" + _layoutName + "'.");
        }

        // =====================================================================
        // LAYOUT
        // =====================================================================
        private void BuildLayout()
        {
            SuspendLayout();

            Text = "Plot - " + _layoutName;
            FormBorderStyle = FormBorderStyle.Sizable;
            MaximizeBox = true;
            MinimizeBox = true;
            StartPosition = FormStartPosition.CenterParent;
            ClientSize = new Size(900, 700);
            MinimumSize = new Size(950, 740);
            Font = Theme.DefaultFont;

            int leftX = 12;
            int rightX = 470;
            int colWidth = 445;

            var grpPageSetup = BuildPageSetupGroup(leftX, 8, colWidth);
            var grpPrinter = BuildPrinterGroup(leftX, grpPageSetup.Bottom + 8, colWidth);
            var grpPaperSize = BuildPaperSizeGroup(leftX, grpPrinter.Bottom + 8, 300);
            var grpCopies = BuildCopiesGroup(grpPaperSize.Right + 8, grpPrinter.Bottom + 8, colWidth - grpPaperSize.Width - 8);
            var grpPlotArea = BuildPlotAreaGroup(leftX, grpPaperSize.Bottom + 8, 215);
            var grpPlotScale = BuildPlotScaleGroup(grpPlotArea.Right + 8, grpPaperSize.Bottom + 8, colWidth - grpPlotArea.Width - 8);
            var grpOffset = BuildPlotOffsetGroup(leftX, grpPlotArea.Bottom + 8, colWidth);

            Controls.Add(grpPageSetup);
            Controls.Add(grpPrinter);
            Controls.Add(grpPaperSize);
            Controls.Add(grpCopies);
            Controls.Add(grpPlotArea);
            Controls.Add(grpPlotScale);
            Controls.Add(grpOffset);

            // Right panel wraps everything on the right so it can collapse as one unit.
            _rightPanel = new Panel
            {
                Location = new Point(rightX, 8),
                Size = new Size(colWidth, grpOffset.Bottom - 8),
                BorderStyle = BorderStyle.None,
                Anchor = AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom,
            };

            _rightTabs = new TabControl { Dock = DockStyle.Fill };

            int innerWidth = colWidth - 16;
            var tabSettings = new TabPage("Plot Settings");
            var grpStyleTable = BuildPlotStyleGroup(4, 6, innerWidth);
            var grpShaded = BuildShadedViewportGroup(4, grpStyleTable.Bottom + 8, innerWidth);
            var grpPlotOptions = BuildPlotOptionsGroup(4, grpShaded.Bottom + 8, innerWidth);
            var grpOrientation = BuildOrientationGroup(4, grpPlotOptions.Bottom + 8, innerWidth);
            tabSettings.Controls.Add(grpStyleTable);
            tabSettings.Controls.Add(grpShaded);
            tabSettings.Controls.Add(grpPlotOptions);
            tabSettings.Controls.Add(grpOrientation);

            var tabPdfOutput = new TabPage("PDF Output");
            tabPdfOutput.Controls.Add(BuildPdfOutputTab(innerWidth));

            var tabPdfPreview = new TabPage("PDF Preview");
            tabPdfPreview.Controls.Add(BuildPdfPreviewTab(innerWidth));

            _rightTabs.TabPages.Add(tabSettings);
            _rightTabs.TabPages.Add(tabPdfOutput);
            _rightTabs.TabPages.Add(tabPdfPreview);

            _rightPanel.Controls.Add(_rightTabs);
            Controls.Add(_rightPanel);

            BuildBottomBar();

            ApplyRoles();
            C3DTheme.Apply(this, C3DPalette.Dark);
            // The mini paper preview keeps a white "paper" look regardless of the
            // dark chrome everywhere else - re-assert it after theming runs.
            _paperPreview.PaperColor = Color.White;

            // The window is now user-resizable (min/max buttons, drag-to-resize) -
            // keep the bottom action bar pinned to the bottom-right as the window
            // is resized or maximized, instead of only recalculating on the
            // "collapse right panel" toggle like before.
            Resize += (s, e) =>
            {
                if (WindowState != FormWindowState.Minimized)
                    RepositionBottomBar();
            };

            ResumeLayout(false);
        }

        /// <summary>Tags every button with a C3DTheme role so C3DTheme.Apply colors it consistently with the rest of the C3D toolset.</summary>
        private void ApplyRoles()
        {
            C3DTheme.Role(_btnOk, C3DTheme.RolePrimary);
            C3DTheme.Role(_btnApplyToLayout, C3DTheme.RoleRefresh);
            C3DTheme.Role(_btnPreview, C3DTheme.RoleRefresh);
            C3DTheme.Role(_btnCancel, C3DTheme.RoleDanger);
            C3DTheme.Role(_btnHelp, C3DTheme.RoleNeutral);
            C3DTheme.Role(_btnCollapse, C3DTheme.RoleNeutral);
            C3DTheme.Role(_btnAddPageSetup, C3DTheme.RoleNeutral);
            C3DTheme.Role(_btnProperties, C3DTheme.RoleNeutral);
            C3DTheme.Role(_btnPdfOptions, C3DTheme.RoleNeutral);
            C3DTheme.Role(_btnEditPlotStyle, C3DTheme.RoleNeutral);
            C3DTheme.Role(_btnWindowPick, C3DTheme.RoleRefresh);
            C3DTheme.Role(_btnCreateTodayFolder, C3DTheme.RoleRefresh);
            C3DTheme.Role(_btnOpenTodayFolder, C3DTheme.RoleNeutral);
        }

        private GroupBox BuildPageSetupGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Page setup", width, 60);
            grp.Location = new Point(x, y);

            var lbl = new Label { Text = "Name:", Location = new Point(10, 26), AutoSize = true };
            _cboPageSetup = new ComboBox
            {
                Location = new Point(70, 22),
                Width = width - 200,
                DropDownStyle = ComboBoxStyle.DropDown
            };
            _cboPageSetup.Items.AddRange(new object[] { "<Select page setup to apply>", "1350_594" });
            _cboPageSetup.SelectedIndex = 1;

            _btnAddPageSetup = Theme.CreateButton("Add...", AppIcon.Add, 90);
            _btnAddPageSetup.Location = new Point(width - 100, 21);
            _btnAddPageSetup.Click += (s, e) => Logger.Info("PlotDialogForm", "Add page setup requested.");

            grp.Controls.Add(lbl);
            grp.Controls.Add(_cboPageSetup);
            grp.Controls.Add(_btnAddPageSetup);
            return grp;
        }

        private GroupBox BuildPrinterGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Printer/plotter", width, 250);
            grp.Location = new Point(x, y);

            var lblName = new Label { Text = "Name:", Location = new Point(10, 26), AutoSize = true };
            _cboPrinter = new ComboBox
            {
                Location = new Point(70, 22),
                Width = width - 200,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboPrinter.Items.AddRange(new object[] { "DWG To PDF.pc3", "None", "DWFx ePlot (XPS Compatible).pc3" });
            _cboPrinter.SelectedIndex = 0;
            _cboPrinter.SelectedIndexChanged += (s, e) => { UpdatePrinterInfo(); PopulateRealPaperSizeList(); };

            _btnProperties = Theme.CreateButton("Properties...", AppIcon.Properties, 100);
            _btnProperties.Location = new Point(width - 110, 21);
            _btnProperties.Click += (s, e) => OpenPlotterManager();

            var lblPlotterCap = new Label { Text = "Plotter:", Location = new Point(10, 56), AutoSize = true };
            _lblPlotterValue = new Label { Location = new Point(70, 56), AutoSize = true };

            var lblWhereCap = new Label { Text = "Where:", Location = new Point(10, 76), AutoSize = true };
            _lblWhereValue = new Label { Location = new Point(70, 76), AutoSize = true };

            var lblDescCap = new Label { Text = "Description:", Location = new Point(10, 96), AutoSize = true };
            _lblDescriptionValue = new Label { Location = new Point(90, 96), AutoSize = true, MaximumSize = new Size(width - 260, 0) };

            _chkPlotToFile = new CheckBox { Text = "Plot to file", Location = new Point(10, 122), AutoSize = true, Checked = true };

            _btnPdfOptions = Theme.CreateButton("PDF Options...", AppIcon.Settings, 110);
            _btnPdfOptions.Location = new Point(width - 260, 118);
            _btnPdfOptions.Click += (s, e) => Logger.Info("PlotDialogForm", "PDF Options dialog requested.");

            // Small paper preview thumbnail, top-right of the group (mirrors the real dialog).
            _paperPreview = new PreviewControl
            {
                Location = new Point(width - 140, 150),
                Size = new Size(128, 92)
            };
            _lblPaperPreviewCaption = new Label
            {
                Text = "1350 MM",
                Location = new Point(width - 140, 244),
                AutoSize = true
            };

            grp.Controls.Add(lblName);
            grp.Controls.Add(_cboPrinter);
            grp.Controls.Add(_btnProperties);
            grp.Controls.Add(lblPlotterCap);
            grp.Controls.Add(_lblPlotterValue);
            grp.Controls.Add(lblWhereCap);
            grp.Controls.Add(_lblWhereValue);
            grp.Controls.Add(lblDescCap);
            grp.Controls.Add(_lblDescriptionValue);
            grp.Controls.Add(_chkPlotToFile);
            grp.Controls.Add(_btnPdfOptions);
            grp.Controls.Add(_paperPreview);
            grp.Controls.Add(_lblPaperPreviewCaption);

            UpdatePrinterInfo();
            return grp;
        }

        private void UpdatePrinterInfo()
        {
            string name = _cboPrinter?.SelectedItem as string ?? "";
            _lblPlotterValue.Text = name;
            // A fully accurate "Plotter:" driver description and "Where:" port
            // would need Autodesk.AutoCAD.PlottingServices.PlotConfig.Load(name),
            // which this build doesn't call (kept out deliberately - several
            // other PlottingServices signatures guessed earlier in this project
            // turned out wrong against the real SDK, so this sticks to a simple,
            // safe heuristic instead of another unverified API call).
            _lblWhereValue.Text = LooksLikeFileBasedDevice(name) ? "File" : "Port";
            _lblDescriptionValue.Text = "";
        }

        private static bool LooksLikeFileBasedDevice(string deviceName)
        {
            if (string.IsNullOrEmpty(deviceName)) return false;
            return deviceName.EndsWith(".pc3", StringComparison.OrdinalIgnoreCase)
                || deviceName.IndexOf("PDF", StringComparison.OrdinalIgnoreCase) >= 0
                || deviceName.IndexOf("DWF", StringComparison.OrdinalIgnoreCase) >= 0
                || deviceName.IndexOf("Publish", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        /// <summary>
        /// Replaces the small hardcoded placeholder lists for Printer/plotter
        /// and Plot style table with the real ones installed on this machine
        /// (PlotSettingsValidator.GetPlotDeviceList() /
        /// GetPlotStyleSheetList()), same as the native Plot dialog shows.
        /// Falls back to the existing placeholder entries if anything about
        /// this fails - never leaves the combos empty.
        /// </summary>
        /// <summary>
        /// Real device/style-sheet lists barely ever change mid-session, but
        /// querying them (PlotSettingsValidator.GetPlotDeviceList /
        /// GetPlotStyleSheetList) re-enumerates every installed PC3/CTB file
        /// from disk each time - this was previously re-run in full on
        /// *every* dialog open, which is most of what made the dialog feel
        /// slower to open the more PC3/CTB files were installed. Cached
        /// once per NETLOAD; PopulateRealDeviceAndStyleLists(forceRefresh:
        /// true) can still force a re-query if a device/style is
        /// installed/removed mid-session.
        /// </summary>
        private static List<string> _cachedDevices;
        private static List<string> _cachedStyleSheets;

        /// <summary>
        /// Replaces the small hardcoded placeholder lists for Printer/plotter
        /// and Plot style table with the real ones installed on this machine
        /// (PlotSettingsValidator.GetPlotDeviceList() /
        /// GetPlotStyleSheetList()), same as the native Plot dialog shows.
        /// Falls back to the existing placeholder entries if anything about
        /// this fails - never leaves the combos empty.
        /// </summary>
        private void PopulateRealDeviceAndStyleLists(bool forceRefresh = false)
        {
            try
            {
                PlotSettingsValidator psv = PlotSettingsValidator.Current;

                bool devicesFromCache = !forceRefresh && _cachedDevices != null;
                if (forceRefresh || _cachedDevices == null)
                {
                    var fresh = psv.GetPlotDeviceList();
                    if (fresh != null && fresh.Count > 0)
                        _cachedDevices = fresh.Cast<string>().ToList();
                }
                var devices = _cachedDevices;
                if (devices != null && devices.Count > 0)
                {
                    string previous = _cboPrinter.SelectedItem as string;
                    _cboPrinter.Items.Clear();
                    foreach (string d in devices) _cboPrinter.Items.Add(d);

                    int idx = previous != null ? _cboPrinter.Items.IndexOf(previous) : -1;
                    if (idx < 0) idx = _cboPrinter.Items.IndexOf("DWG To PDF.pc3");
                    _cboPrinter.SelectedIndex = idx >= 0 ? idx : 0;
                    Logger.Info("PlotDialogForm", "Loaded " + devices.Count + " real plot device(s)" + (devicesFromCache ? " (cached)" : "") + ".");
                }

                if (forceRefresh || _cachedStyleSheets == null)
                {
                    var fresh = psv.GetPlotStyleSheetList();
                    if (fresh != null && fresh.Count > 0)
                        _cachedStyleSheets = fresh.Cast<string>().ToList();
                }
                var styleSheets = _cachedStyleSheets;
                if (styleSheets != null && styleSheets.Count > 0)
                {
                    _cboPlotStyleTable.Items.Clear();
                    _cboPlotStyleTable.Items.Add("None");
                    foreach (string sheet in styleSheets) _cboPlotStyleTable.Items.Add(sheet);

                    int idx = _cboPlotStyleTable.Items.IndexOf("DB300.CTB");
                    _cboPlotStyleTable.SelectedIndex = idx >= 0 ? idx : 0;
                    Logger.Info("PlotDialogForm", "Loaded " + styleSheets.Count + " real plot style table(s).");
                }
            }
            catch (Exception ex)
            {
                Logger.Warn("PlotDialogForm",
                    "Could not query the real device/plot-style-table lists (keeping the built-in placeholder entries): " + ex.Message);
            }

            PopulateRealPaperSizeList();
        }

        /// <summary>
        /// Replaces the single hardcoded "_1350.00 x 594.00mm" paper size
        /// entry with the real canonical media list for the currently
        /// selected device (PlotSettingsValidator.GetCanonicalMediaNameList),
        /// the same PlotSettingsValidator/PlotSettings calls already used
        /// (and confirmed to compile) in Cad/PlotService.cs - reused here
        /// rather than the parameter-less device/style-sheet calls above,
        /// which are new and unverified against your SDK.
        /// </summary>
        private void PopulateRealPaperSizeList()
        {
            string device = _cboPrinter.SelectedItem as string;
            if (string.IsNullOrEmpty(device)) return;

            try
            {
                PlotSettingsValidator psv = PlotSettingsValidator.Current;
                PlotSettings ps = new PlotSettings(false);
                psv.SetPlotConfigurationName(ps, device, null);
                psv.RefreshLists(ps);

                var media = psv.GetCanonicalMediaNameList(ps);
                if (media == null || media.Count == 0) return;

                _cboPaperSize.Items.Clear();
                _paperSizeCanonicalNames.Clear();

                foreach (string canonicalName in media)
                {
                    // Canonical names look like "ISO_full_bleed_A4_(210.00_x_297.00_MM)" -
                    // the native dialog displays the same string with underscores turned
                    // into spaces, e.g. "ISO full bleed A4 (210.00 x 297.00 MM)".
                    _cboPaperSize.Items.Add(canonicalName.Replace('_', ' '));
                    _paperSizeCanonicalNames.Add(canonicalName);
                }

                _cboPaperSize.SelectedIndex = 0;
                Logger.Info("PlotDialogForm", "Loaded " + media.Count + " real paper size(s) for '" + device + "'.");
            }
            catch (Exception ex)
            {
                Logger.Warn("PlotDialogForm", "Could not query real paper sizes for '" + device + "' (keeping placeholder): " + ex.Message);
            }
        }

        /// <summary>Reads the actual width/height (mm) of whichever paper size is now selected, so PaperWidthMm/PaperHeightMm and the previews reflect it.</summary>
        private void ApplySelectedPaperSizeDimensions()
        {
            int idx = _cboPaperSize.SelectedIndex;
            string device = _cboPrinter.SelectedItem as string;
            if (idx < 0 || idx >= _paperSizeCanonicalNames.Count || string.IsNullOrEmpty(device))
            {
                RefreshMiniPreview();
                return;
            }

            try
            {
                PlotSettingsValidator psv = PlotSettingsValidator.Current;
                PlotSettings ps = new PlotSettings(false);
                psv.SetPlotConfigurationName(ps, device, null);
                psv.RefreshLists(ps);
                psv.SetCanonicalMediaName(ps, _paperSizeCanonicalNames[idx]);

                Point2d size = ps.PlotPaperSize; // X = width mm, Y = height mm
                _paperWidthMm = size.X;
                _paperHeightMm = size.Y;
            }
            catch (Exception ex)
            {
                Logger.Warn("PlotDialogForm", "Could not read paper size dimensions: " + ex.Message);
            }

            RefreshMiniPreview();
        }

        /// <summary>
        /// The native AutoCAD "Properties..." button opens a Plotter
        /// Configuration Editor dialog that isn't exposed through the
        /// public ObjectARX managed API. The closest real, working
        /// equivalent is AutoCAD's own Plotter Manager (the PLOTTERMANAGER
        /// command), which opens an Explorer window of the installed PC3
        /// files - double-clicking one there launches the same editor the
        /// native Properties... button would have shown.
        /// </summary>
        private void OpenPlotterManager()
        {
            Document doc = AcadApp.DocumentManager.MdiActiveDocument;
            if (doc == null) return;

            doc.SendStringToExecute("_.PLOTTERMANAGER \n", true, false, false);
            Logger.Info("PlotDialogForm",
                "Opened Plotter Manager - double-click the selected device's PC3 there to edit its properties.");
        }

        /// <summary>
        /// The small button next to "Plot style table (pen assignments)"
        /// opens AutoCAD's Plot Style Manager (the STYLESMANAGER command),
        /// an Explorer window of the installed CTB/STB files - double-
        /// clicking the one currently selected in the combo there launches
        /// the real Plot Style Table Editor, the same one the native
        /// dialog's own edit button opens. Same reasoning as
        /// OpenPlotterManager: the real editor dialog isn't exposed through
        /// the public managed API, so this is the closest genuine working
        /// equivalent rather than a re-implementation of it.
        /// </summary>
        private void OpenPlotStyleManager()
        {
            Document doc = AcadApp.DocumentManager.MdiActiveDocument;
            if (doc == null) return;

            doc.SendStringToExecute("_.STYLESMANAGER \n", true, false, false);
            Logger.Info("PlotDialogForm",
                "Opened Plot Style Manager - double-click '" + (SelectedPlotStyleTable ?? "the selected table") + "' there to edit it.");
        }

        /// <summary>
        /// "Apply to Layout" writes the dialog's current settings onto the
        /// layout's own PlotSettings without plotting anything and without
        /// closing the dialog - matching the native button. Previously this
        /// only wrote a log line and never touched the layout at all.
        /// </summary>
        private void ApplyToLayout()
        {
            bool ok = PlotService.ApplyToLayout(this, _layoutName, out string error);
            if (ok)
            {
                Logger.Info("PlotDialogForm", "Settings applied to layout '" + _layoutName + "'.");
                MessageBox.Show(this, "Plot settings applied to layout '" + _layoutName + "'.",
                    "Apply to Layout", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            else
            {
                Logger.Error("PlotDialogForm", "Could not apply settings to layout '" + _layoutName + "': " + error);
                MessageBox.Show(this, "Could not apply settings to layout '" + _layoutName + "'.\n\n" + error,
                    "Apply to Layout", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
        }

        private GroupBox BuildPaperSizeGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Paper size", width, 60);
            grp.Location = new Point(x, y);

            _cboPaperSize = new ComboBox
            {
                Location = new Point(10, 24),
                Width = width - 20,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboPaperSize.Items.Add("_1350.00 x 594.00mm");
            _cboPaperSize.SelectedIndex = 0;
            _cboPaperSize.SelectedIndexChanged += (s, e) => ApplySelectedPaperSizeDimensions();

            grp.Controls.Add(_cboPaperSize);
            return grp;
        }

        private GroupBox BuildCopiesGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Number of copies", width, 60);
            grp.Location = new Point(x, y);

            _numCopies = new NumericUpDown
            {
                Location = new Point(10, 24),
                Width = 70,
                Minimum = 1,
                Maximum = 100,
                Value = 1
            };

            grp.Controls.Add(_numCopies);
            return grp;
        }

        private GroupBox BuildPlotAreaGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Plot area", width, 90);
            grp.Location = new Point(x, y);

            var lbl = new Label { Text = "What to plot:", Location = new Point(10, 24), AutoSize = true };
            _cboWhatToPlot = new ComboBox
            {
                Location = new Point(10, 44),
                Width = width - 20,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboWhatToPlot.Items.AddRange(new object[] { "Layout", "Extents", "Display", "View", "Window" });
            _cboWhatToPlot.SelectedItem = "Window";

            _btnWindowPick = Theme.CreateButton("Window<", AppIcon.Preview, width - 20);
            _btnWindowPick.Location = new Point(10, 70);
            _btnWindowPick.Click += (s, e) => PickWindow();

            grp.Height = 100;
            grp.Controls.Add(lbl);
            grp.Controls.Add(_cboWhatToPlot);
            grp.Controls.Add(_btnWindowPick);
            return grp;
        }

        private GroupBox BuildPlotScaleGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Plot scale", width, 100);
            grp.Location = new Point(x, y);

            _chkFitToPaper = new CheckBox { Text = "Fit to paper", Location = new Point(10, 22), AutoSize = true };
            _chkFitToPaper.CheckedChanged += (s, e) => RefreshMiniPreview();

            var lblScale = new Label { Text = "Scale:", Location = new Point(10, 46), AutoSize = true };
            _cboScale = new ComboBox
            {
                Location = new Point(60, 42),
                Width = width - 80,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboScale.Items.AddRange(new object[] { "1:1_1", "1:2", "1:5", "1:10", "1:20", "1:50", "1:100", "Custom" });
            _cboScale.SelectedIndex = 0;

            _txtScaleMm = new TextBox { Location = new Point(10, 70), Width = 80, Text = "1" };
            _cboScaleUnitLabel = new ComboBox
            {
                Location = new Point(96, 70),
                Width = 70,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboScaleUnitLabel.Items.AddRange(new object[] { "mm", "cm", "m", "in", "ft" });
            _cboScaleUnitLabel.SelectedIndex = 0;

            var lblEquals = new Label { Text = "=", Location = new Point(172, 74), AutoSize = true };

            _txtScaleUnits = new TextBox { Location = new Point(10, 96), Width = 80, Text = "1" };
            var lblUnits = new Label { Text = "units", Location = new Point(96, 100), AutoSize = true };

            _chkScaleLineweights = new CheckBox { Text = "Scale lineweights", Location = new Point(10, 124), AutoSize = true };

            grp.Height = 152;
            grp.Controls.Add(_chkFitToPaper);
            grp.Controls.Add(lblScale);
            grp.Controls.Add(_cboScale);
            grp.Controls.Add(_txtScaleMm);
            grp.Controls.Add(_cboScaleUnitLabel);
            grp.Controls.Add(lblEquals);
            grp.Controls.Add(_txtScaleUnits);
            grp.Controls.Add(lblUnits);
            grp.Controls.Add(_chkScaleLineweights);
            return grp;
        }

        private GroupBox BuildPlotOffsetGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Plot offset (origin set to printable area)", width, 90);
            grp.Location = new Point(x, y);

            var lblX = new Label { Text = "X:", Location = new Point(10, 26), AutoSize = true };
            _txtOffsetX = new TextBox { Location = new Point(30, 22), Width = 80, Text = "0.00" };
            _txtOffsetX.TextChanged += (s, e) => RefreshMiniPreview();
            var lblXUnit = new Label { Text = "mm", Location = new Point(115, 26), AutoSize = true };

            var lblY = new Label { Text = "Y:", Location = new Point(10, 54), AutoSize = true };
            _txtOffsetY = new TextBox { Location = new Point(30, 50), Width = 80, Text = "0.00" };
            _txtOffsetY.TextChanged += (s, e) => RefreshMiniPreview();
            var lblYUnit = new Label { Text = "mm", Location = new Point(115, 54), AutoSize = true };

            _chkCenterPlot = new CheckBox { Text = "Center the plot", Location = new Point(160, 36), AutoSize = true };
            _chkCenterPlot.CheckedChanged += (s, e) =>
            {
                _txtOffsetX.Enabled = _txtOffsetY.Enabled = !_chkCenterPlot.Checked;
                RefreshMiniPreview();
            };

            grp.Controls.Add(lblX);
            grp.Controls.Add(_txtOffsetX);
            grp.Controls.Add(lblXUnit);
            grp.Controls.Add(lblY);
            grp.Controls.Add(_txtOffsetY);
            grp.Controls.Add(lblYUnit);
            grp.Controls.Add(_chkCenterPlot);
            return grp;
        }

        private GroupBox BuildPlotStyleGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Plot style table (pen assignments)", width, 60);
            grp.Location = new Point(x, y);

            _cboPlotStyleTable = new ComboBox
            {
                Location = new Point(10, 24),
                Width = width - 70,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboPlotStyleTable.Items.AddRange(new object[] { "None", "DB300.CTB", "Monochrome.ctb", "acad.ctb" });
            _cboPlotStyleTable.SelectedItem = "DB300.CTB";

            var btnEdit = Theme.CreateButton("", AppIcon.Settings, 34);
            btnEdit.Location = new Point(width - 46, 22);
            btnEdit.Click += (s, e) => OpenPlotStyleManager();
            _btnEditPlotStyle = btnEdit;

            grp.Controls.Add(_cboPlotStyleTable);
            grp.Controls.Add(_btnEditPlotStyle);
            return grp;
        }

        private GroupBox BuildShadedViewportGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Shaded viewport options", width, 130);
            grp.Location = new Point(x, y);

            var lblShade = new Label { Text = "Shade plot", Location = new Point(10, 26), AutoSize = true };
            _cboShadePlot = new ComboBox
            {
                Location = new Point(120, 22),
                Width = width - 132,
                DropDownStyle = ComboBoxStyle.DropDownList,
                Enabled = false
            };
            _cboShadePlot.Items.AddRange(new object[] { "As displayed", "Wireframe", "Hidden", "Realistic", "Conceptual" });
            _cboShadePlot.SelectedIndex = 0;

            var lblQuality = new Label { Text = "Quality", Location = new Point(10, 58), AutoSize = true };
            _cboQuality = new ComboBox
            {
                Location = new Point(120, 54),
                Width = width - 132,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboQuality.Items.AddRange(new object[] { "Draft", "Low", "Medium", "High", "Presentation", "Normal" });
            _cboQuality.SelectedItem = "Normal";
            _cboQuality.SelectedIndexChanged += (s, e) =>
                _txtDpi.Enabled = string.Equals(_cboQuality.SelectedItem as string, "Custom", StringComparison.OrdinalIgnoreCase);

            var lblDpi = new Label { Text = "DPI", Location = new Point(10, 90), AutoSize = true };
            _txtDpi = new TextBox
            {
                Location = new Point(120, 86),
                Width = width - 132,
                Text = "100",
                Enabled = false
            };

            grp.Controls.Add(lblShade);
            grp.Controls.Add(_cboShadePlot);
            grp.Controls.Add(lblQuality);
            grp.Controls.Add(_cboQuality);
            grp.Controls.Add(lblDpi);
            grp.Controls.Add(_txtDpi);
            return grp;
        }

        private GroupBox BuildPlotOptionsGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Plot options", width, 190);
            grp.Location = new Point(x, y);

            _chkPlotInBackground = new CheckBox { Text = "Plot in background", Location = new Point(10, 22), AutoSize = true };
            _chkPlotObjectLineweights = new CheckBox { Text = "Plot object lineweights", Location = new Point(10, 46), AutoSize = true, Checked = true };
            _chkPlotTransparency = new CheckBox { Text = "Plot transparency", Location = new Point(10, 70), AutoSize = true, Checked = true };
            _chkPlotWithPlotStyles = new CheckBox { Text = "Plot with plot styles", Location = new Point(10, 94), AutoSize = true, Checked = true };
            _chkPlotPaperspaceLast = new CheckBox { Text = "Plot paperspace last", Location = new Point(10, 118), AutoSize = true, Checked = true };
            _chkHidePaperspaceObjects = new CheckBox { Text = "Hide paperspace objects", Location = new Point(10, 142), AutoSize = true };
            _chkPlotStampOn = new CheckBox { Text = "Plot stamp on", Location = new Point(220, 22), AutoSize = true };
            _chkSaveChangesToLayout = new CheckBox { Text = "Save changes to layout", Location = new Point(220, 46), AutoSize = true };

            grp.Controls.Add(_chkPlotInBackground);
            grp.Controls.Add(_chkPlotObjectLineweights);
            grp.Controls.Add(_chkPlotTransparency);
            grp.Controls.Add(_chkPlotWithPlotStyles);
            grp.Controls.Add(_chkPlotPaperspaceLast);
            grp.Controls.Add(_chkHidePaperspaceObjects);
            grp.Controls.Add(_chkPlotStampOn);
            grp.Controls.Add(_chkSaveChangesToLayout);
            return grp;
        }

        private GroupBox BuildOrientationGroup(int x, int y, int width)
        {
            var grp = Theme.CreateGroup("Drawing orientation", width, 120);
            grp.Location = new Point(x, y);

            _radPortrait = new RadioButton { Text = "Portrait", Location = new Point(10, 24), AutoSize = true };
            _radLandscape = new RadioButton { Text = "Landscape", Location = new Point(10, 48), AutoSize = true, Checked = true };
            _chkPlotUpsideDown = new CheckBox { Text = "Plot upside-down", Location = new Point(10, 76), AutoSize = true };
            _radPortrait.CheckedChanged += (s, e) => RefreshMiniPreview();
            _radLandscape.CheckedChanged += (s, e) => RefreshMiniPreview();
            _chkPlotUpsideDown.CheckedChanged += (s, e) => RefreshMiniPreview();

            var orientationIcon = new PictureBox
            {
                Location = new Point(width - 60, 24),
                Size = new Size(40, 40),
                BorderStyle = BorderStyle.FixedSingle,
                Image = BuildOrientationGlyph(),
                SizeMode = PictureBoxSizeMode.CenterImage
            };

            grp.Controls.Add(_radPortrait);
            grp.Controls.Add(_radLandscape);
            grp.Controls.Add(_chkPlotUpsideDown);
            grp.Controls.Add(orientationIcon);
            return grp;
        }

        private static Bitmap BuildOrientationGlyph()
        {
            var bmp = new Bitmap(24, 24);
            using (var g = Graphics.FromImage(bmp))
            {
                g.Clear(Color.Transparent);
                using (var pen = new Pen(Color.DimGray, 1.5f))
                    g.DrawRectangle(pen, 4, 2, 16, 20);
                using (var font = new DrawingFont("Segoe UI", 9f, FontStyle.Bold))
                using (var brush = new SolidBrush(Color.DimGray))
                    g.DrawString("A", font, brush, 7, 4);
            }
            return bmp;
        }

        /// <summary>
        /// "PDF Output" tab: pick one of the configured projects (ported from
        /// PDF_Project.py's DEFAULT_PROJECTS/DEFAULT_SP_CONFIG - some, like
        /// Beu and Tp_EP, have more than one named output location), create
        /// a dated (yyyyMMdd) output subfolder under the selected one with
        /// one click, preview the PDFs already there, and optionally have
        /// PlotService save straight into that folder instead of prompting
        /// with a Save As dialog every time.
        /// </summary>
        /// <summary>
        /// "PDF Preview" tab: browse the PDF file/folder structure already
        /// output for the currently selected project/location (from the
        /// "PDF Output" tab) - subfolders and PDFs, double-click to enter a
        /// folder or open a file. This is a distinct concept from the
        /// "Preview..." button at the bottom of the dialog: that one is
        /// about previewing *this plot* via AutoCAD/Civil 3D's own engine;
        /// this tab is about what's already sitting in the project's output
        /// folder. (An earlier revision conflated the two - putting a
        /// rendered sketch of the drawing here instead - that's been
        /// corrected back to browsing the actual PDF structure.)
        /// </summary>
        private Panel BuildPdfPreviewTab(int width)
        {
            var panel = new Panel { Dock = DockStyle.Fill };

            var toolbar = new FlowLayoutPanel
            {
                Dock = DockStyle.Top,
                Height = 40,
                Padding = new Padding(4, 4, 4, 4),
                FlowDirection = WinFormsFlowDirection.LeftToRight,
            };
            var btnRefreshPreview = Theme.CreateButton("Refresh", AppIcon.Reload, 100);
            C3DTheme.Role(btnRefreshPreview, C3DTheme.RoleRefresh);
            btnRefreshPreview.Click += (s, e) => RefreshPdfFileList();
            toolbar.Controls.Add(btnRefreshPreview);

            var lblFiles = new Label
            {
                Text = "PDFs for the project/location selected on the PDF Output tab (double-click to open/enter):",
                Dock = DockStyle.Top,
                Height = 24,
                Padding = new Padding(4, 4, 0, 0),
            };

            _lstPdfFiles = new ListBox
            {
                Dock = DockStyle.Fill,
                DrawMode = DrawMode.OwnerDrawFixed,
            };
            _lstPdfFiles.DrawItem += LstPdfFiles_DrawItem;
            _lstPdfFiles.DoubleClick += (s, e) => HandlePdfListActivate();

            // Dock.Fill added before the Dock.Top labels/toolbar - see the note
            // in BuildPdfOutputTab for why that order matters in WinForms.
            panel.Controls.Add(_lstPdfFiles);
            panel.Controls.Add(lblFiles);
            panel.Controls.Add(toolbar);
            return panel;
        }

        private Panel BuildPdfOutputTab(int width)
        {
            _projects = PdfProjectStore.Load();
            _tiers = PdfTierStore.Load();

            var panel = new Panel { Location = new Point(0, 0), Size = new Size(width + 20, 560), AutoScroll = true };

            // ---- Toolbar (ported from PDF_Project.py's toolbar row) -----------------
            var toolbar = new FlowLayoutPanel
            {
                Location = new Point(4, 2),
                Size = new Size(width, 66),
                FlowDirection = WinFormsFlowDirection.LeftToRight,
                WrapContents = true,
            };

            var btnTiers = Theme.CreateButton("Tiers", AppIcon.Settings, 80);
            C3DTheme.Role(btnTiers, C3DTheme.RoleNeutral);
            btnTiers.Click += (s, e) => OpenTierEditor();

            var btnRoot = Theme.CreateButton("Root", AppIcon.Open, 80);
            C3DTheme.Role(btnRoot, C3DTheme.RoleNeutral);
            // Opens in the already-running Total Commander session (the "/O"
            // switch reuses the current session instead of starting a new
            // one) when TC is available, same as "Open in TC" does for the
            // recent subfolder - falls back to Explorer otherwise.
            btnRoot.Click += (s, e) =>
            {
                string path = CurrentLocationPath();
                if (_tcExePath != null) OpenInTc(path);
                else OpenInExplorer(path);
            };

            var btnRecentFolder = Theme.CreateButton("Recent Folder", AppIcon.Open, 130);
            C3DTheme.Role(btnRecentFolder, C3DTheme.RolePrimary);
            btnRecentFolder.Click += (s, e) => OpenRecentFolder(useTc: false);

            _btnOpenInTc = Theme.CreateButton("Open in TC", AppIcon.Open, 110);
            C3DTheme.Role(_btnOpenInTc, C3DTheme.RolePurple);
            _btnOpenInTc.Enabled = _tcExePath != null;
            _btnOpenInTc.Click += (s, e) => OpenRecentFolder(useTc: true);

            _btnRecent = Theme.CreateButton("Recent \u25BE", AppIcon.Reload, 100);
            C3DTheme.Role(_btnRecent, C3DTheme.RoleReload);
            _btnRecent.Click += (s, e) => ShowRecentMenu();

            var btnCsv = Theme.CreateButton("CSV", AppIcon.Export, 80);
            C3DTheme.Role(btnCsv, C3DTheme.RoleNeutral);
            btnCsv.Click += (s, e) => ExportCsv();

            var btnNewFilesCsv = Theme.CreateButton("New Files CSV", AppIcon.Export, 150);
            C3DTheme.Role(btnNewFilesCsv, C3DTheme.RoleNeutral);
            btnNewFilesCsv.Click += (s, e) => ExportNewFilesCsv();

            var btnRefreshToolbar = Theme.CreateButton("Refresh", AppIcon.Reload, 100);
            C3DTheme.Role(btnRefreshToolbar, C3DTheme.RoleRefresh);
            btnRefreshToolbar.Click += (s, e) => RefreshPdfFileList();

            toolbar.Controls.AddRange(new Control[]
            {
                btnTiers, btnRoot, btnRecentFolder, _btnOpenInTc, _btnRecent, btnCsv, btnNewFilesCsv, btnRefreshToolbar
            });

            // ---- Project / location / today-folder group ----------------------------
            var grp = Theme.CreateGroup("Project output folder", width, 258);
            grp.Location = new Point(4, toolbar.Bottom + 4);

            var lblProject = new Label { Text = "Project:", Location = new Point(10, 28), AutoSize = true };
            _cboProject = new ComboBox
            {
                Location = new Point(80, 24),
                Width = width - 180,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            RefreshProjectCombo();
            _cboProject.SelectedIndexChanged += (s, e) => { RefreshLocationCombo(); UpdateProjectPathLabel(); };

            var btnManage = Theme.CreateButton("Manage...", AppIcon.Settings, 90);
            btnManage.Location = new Point(width - 96, 23);
            btnManage.Click += (s, e) => OpenProjectManager();

            var lblLocationCap = new Label { Text = "Location:", Location = new Point(10, 58), AutoSize = true };
            _cboLocation = new ComboBox
            {
                Location = new Point(80, 54),
                Width = width - 96,
                DropDownStyle = ComboBoxStyle.DropDownList
            };
            _cboLocation.SelectedIndexChanged += (s, e) => UpdateProjectPathLabel();

            var lblPathCap = new Label { Text = "Path:", Location = new Point(10, 86), AutoSize = true };
            _lblProjectPath = new Label
            {
                Location = new Point(80, 86),
                AutoSize = false,
                Size = new Size(width - 96, 34),
            };

            _btnCreateTodayFolder = Theme.CreateButton("Create Today Folder", AppIcon.Add, 170);
            _btnCreateTodayFolder.Location = new Point(10, 128);
            _btnCreateTodayFolder.Click += (s, e) => CreateTodayFolder();

            _btnOpenTodayFolder = Theme.CreateButton("Open Folder", AppIcon.Open, 110);
            C3DTheme.Role(_btnOpenTodayFolder, C3DTheme.RolePrimary);
            _btnOpenTodayFolder.Location = new Point(190, 128);
            _btnOpenTodayFolder.Enabled = false;
            _btnOpenTodayFolder.Click += (s, e) => OpenInExplorer(!string.IsNullOrEmpty(_todayFolder) ? _todayFolder : CurrentLocationPath());

            var lblTodayCap = new Label { Text = "Today folder:", Location = new Point(10, 164), AutoSize = true };
            _lblTodayFolder = new Label
            {
                Text = "(not created yet)",
                Location = new Point(10, 184),
                AutoSize = false,
                Size = new Size(width - 20, 34),
            };

            _chkSaveDirectlyToFolder = new CheckBox
            {
                Text = "Save PDF directly to this folder (skip Save As dialog)",
                Location = new Point(10, 224),
                AutoSize = true
            };

            grp.Controls.Add(lblProject);
            grp.Controls.Add(_cboProject);
            grp.Controls.Add(btnManage);
            grp.Controls.Add(lblLocationCap);
            grp.Controls.Add(_cboLocation);
            grp.Controls.Add(lblPathCap);
            grp.Controls.Add(_lblProjectPath);
            grp.Controls.Add(_btnCreateTodayFolder);
            grp.Controls.Add(_btnOpenTodayFolder);
            grp.Controls.Add(lblTodayCap);
            grp.Controls.Add(_lblTodayFolder);
            grp.Controls.Add(_chkSaveDirectlyToFolder);

            panel.Controls.Add(toolbar);
            panel.Controls.Add(grp);
            panel.Height = grp.Bottom + 12;

            RefreshLocationCombo();
            UpdateProjectPathLabel();
            return panel;
        }

        private void RefreshProjectCombo()
        {
            string previous = _cboProject.SelectedItem as PdfProject != null ? ((PdfProject)_cboProject.SelectedItem).Name : null;
            _cboProject.Items.Clear();
            foreach (var p in _projects)
                _cboProject.Items.Add(p);

            if (_cboProject.Items.Count > 0)
            {
                int idx = previous != null ? _projects.FindIndex(p => p.Name == previous) : -1;
                _cboProject.SelectedIndex = idx >= 0 ? idx : 0;
            }
        }

        /// <summary>Repopulates the Location combo from the selected project's Locations dictionary (e.g. Beu: just "_pdf"; Tp_EP: "_pdf" and "_EW").</summary>
        private void RefreshLocationCombo()
        {
            if (_cboLocation == null) return;

            var proj = _cboProject.SelectedItem as PdfProject;
            _cboLocation.Items.Clear();

            if (proj == null || proj.Locations.Count == 0)
            {
                _cboLocation.Enabled = false;
                return;
            }

            foreach (var label in proj.Locations.Keys)
                _cboLocation.Items.Add(label);

            _cboLocation.Enabled = proj.Locations.Count > 1;
            int pdfIdx = proj.Locations.Keys.ToList().IndexOf("_pdf");
            _cboLocation.SelectedIndex = pdfIdx >= 0 ? pdfIdx : 0;
        }

        /// <summary>The folder path for whatever project/location is currently selected.</summary>
        private string CurrentLocationPath()
        {
            var proj = _cboProject.SelectedItem as PdfProject;
            if (proj == null) return null;

            string label = _cboLocation?.SelectedItem as string;
            if (label != null && proj.Locations.TryGetValue(label, out var p)) return p;
            return proj.Path;
        }

        private void UpdateProjectPathLabel()
        {
            string path = CurrentLocationPath();
            _lblProjectPath.Text = path ?? "(no project/location selected)";
            if (_btnOpenTodayFolder != null)
                _btnOpenTodayFolder.Enabled = !string.IsNullOrEmpty(_todayFolder) || Directory.Exists(path);
            _browseFolder = null; // re-anchor browsing at the new project/location's root
            RefreshPdfFileList();
        }

        private void OpenProjectManager()
        {
            using (var mgr = new PdfProjectManagerForm(_projects))
            {
                if (mgr.ShowDialog(this) == DialogResult.OK)
                {
                    RefreshProjectCombo();
                    RefreshLocationCombo();
                    Logger.Info("PlotDialogForm", "Project list updated (" + _projects.Count + " project(s)).");
                }
            }
        }

        private void CreateTodayFolder()
        {
            string locationPath = CurrentLocationPath();
            if (string.IsNullOrWhiteSpace(locationPath))
            {
                MessageBox.Show(this, "Select a project/location with a configured path first (use Manage... to set one).",
                    "No project path", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            var proj = _cboProject.SelectedItem as PdfProject;
            try
            {
                _todayFolder = PdfProjectStore.CreateTodayFolder(locationPath);
                _lblTodayFolder.Text = _todayFolder;
                _btnOpenTodayFolder.Enabled = true;
                _browseFolder = _todayFolder;
                Logger.Info("PlotDialogForm", "Today folder ready for '" + proj?.Name + "': " + _todayFolder);
                RefreshPdfFileList();
            }
            catch (Exception ex)
            {
                _todayFolder = null;
                _lblTodayFolder.Text = "(failed - see log)";
                _btnOpenTodayFolder.Enabled = false;
                Logger.Error("PlotDialogForm", "Could not create today folder for '" + proj?.Name + "': " + ex.Message);
                MessageBox.Show(this, "Could not create the folder:\n" + ex.Message,
                    "Create Today Folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        /// <summary>Marker for the "go up one level" row.</summary>
        private const string UpMarker = ".. (up one level)";
        /// <summary>Prefix marking a subfolder row so it's visually and programmatically distinct from a PDF file row.</summary>
        private const string DirPrefix = "\u25B8 ";

        private string RootFolder() => !string.IsNullOrEmpty(_todayFolder) ? _todayFolder : CurrentLocationPath();

        private static bool SamePath(string a, string b)
        {
            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
            try
            {
                return string.Equals(
                    System.IO.Path.GetFullPath(a).TrimEnd('\\'),
                    System.IO.Path.GetFullPath(b).TrimEnd('\\'),
                    StringComparison.OrdinalIgnoreCase);
            }
            catch { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); }
        }

        /// <summary>
        /// Lists both the PDFs and the subfolders of the folder currently
        /// being browsed on the "PDF Preview" tab (root defaults to the
        /// today folder if one exists, else the raw project/location
        /// folder). Double-click a subfolder row (or the Up row) to
        /// navigate.
        /// </summary>
        private void RefreshPdfFileList()
        {
            if (_lstPdfFiles == null) return;

            if (string.IsNullOrEmpty(_browseFolder) || !Directory.Exists(_browseFolder))
                _browseFolder = RootFolder();

            _lstPdfFiles.Items.Clear();

            string folder = _browseFolder;
            if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
                return;

            try
            {
                string root = RootFolder();
                if (!string.IsNullOrEmpty(root) && !SamePath(folder, root))
                    _lstPdfFiles.Items.Add(UpMarker);

                foreach (var dir in Directory.GetDirectories(folder)
                    .OrderBy(d => System.IO.Path.GetFileName(d), StringComparer.OrdinalIgnoreCase))
                    _lstPdfFiles.Items.Add(DirPrefix + System.IO.Path.GetFileName(dir));

                foreach (var file in Directory.GetFiles(folder, "*.pdf").OrderByDescending(f => File.GetLastWriteTime(f)))
                    _lstPdfFiles.Items.Add(System.IO.Path.GetFileName(file));
            }
            catch (Exception ex)
            {
                Logger.Warn("PlotDialogForm", "Could not list " + folder + ": " + ex.Message);
            }
        }

        /// <summary>Double-click dispatcher for the PDF list: navigates for the Up row / a folder row, opens the file otherwise.</summary>
        private void HandlePdfListActivate()
        {
            string name = _lstPdfFiles.SelectedItem as string;
            if (string.IsNullOrEmpty(name)) return;

            if (name == UpMarker)
            {
                string parent = System.IO.Path.GetDirectoryName((_browseFolder ?? "").TrimEnd('\\'));
                _browseFolder = !string.IsNullOrEmpty(parent) ? parent : RootFolder();
                RefreshPdfFileList();
                return;
            }

            if (name.StartsWith(DirPrefix, StringComparison.Ordinal))
            {
                _browseFolder = System.IO.Path.Combine(_browseFolder ?? "", name.Substring(DirPrefix.Length));
                RefreshPdfFileList();
                return;
            }

            OpenSelectedPdf();
        }

        private void OpenSelectedPdf()
        {
            string name = _lstPdfFiles.SelectedItem as string;
            if (string.IsNullOrEmpty(name) || name == UpMarker || name.StartsWith(DirPrefix, StringComparison.Ordinal)) return;

            string fullPath = System.IO.Path.Combine(_browseFolder ?? "", name);
            if (!File.Exists(fullPath)) return;

            try
            {
                Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
                Logger.Info("PlotDialogForm", "Opened PDF: " + fullPath);
            }
            catch (Exception ex)
            {
                Logger.Warn("PlotDialogForm", "Could not open " + fullPath + ": " + ex.Message);
            }
        }

        /// <summary>Colors each row of the PDF file list by the file's age, per the configured tiers (ported concept from PDF_Project.py's age-tier system).</summary>
        private void LstPdfFiles_DrawItem(object sender, DrawItemEventArgs e)
        {
            if (e.Index < 0) return;

            string name = _lstPdfFiles.Items[e.Index] as string;
            bool isNav = name == UpMarker || (name != null && name.StartsWith(DirPrefix, StringComparison.Ordinal));
            string full = isNav ? null : System.IO.Path.Combine(_browseFolder ?? "", name ?? "");

            Color bg = Color.White;
            Color fg = isNav ? Color.SteelBlue : Color.Black;
            if (!isNav && _tiers != null && _tiers.Count > 0 && File.Exists(full))
            {
                double ageDays = (DateTime.Now - File.GetLastWriteTime(full)).TotalDays;
                var tier = PdfTierStore.ForAge(_tiers, ageDays);
                if (tier != null)
                {
                    bg = tier.BgColor;
                    fg = string.IsNullOrEmpty(tier.FgColorHex) ? Color.Black : tier.FgColor;
                }
            }

            bool selected = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
            Color drawBg = selected ? C3DPalette.Dark.SelBg : bg;
            Color drawFg = selected ? C3DPalette.Dark.SelFg : fg;

            using (var backBrush = new SolidBrush(drawBg))
                e.Graphics.FillRectangle(backBrush, e.Bounds);
            DrawingFont font = isNav ? C3DFonts.ButtonBold : _lstPdfFiles.Font;
            using (var textBrush = new SolidBrush(drawFg))
                e.Graphics.DrawString(name, font, textBrush, e.Bounds);

            e.DrawFocusRectangle();
        }

        // =====================================================================
        // TOOLBAR (ported from PDF_Project.py: Tiers, Root, Recent Folder,
        // Open in TC, Recent, CSV, New Files CSV, Refresh)
        // =====================================================================

        private void OpenTierEditor()
        {
            using (var editor = new PdfTierEditorForm(_tiers))
            {
                if (editor.ShowDialog(this) == DialogResult.OK)
                {
                    _lstPdfFiles?.Invalidate();
                    Logger.Info("PlotDialogForm", "Age tier colours updated (" + _tiers.Count + " tier(s)).");
                }
            }
        }

        private void OpenInExplorer(string path)
        {
            if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
            {
                Logger.Warn("PlotDialogForm", "Folder not found: " + path);
                return;
            }
            try
            {
                Process.Start("explorer.exe", "\"" + path + "\"");
                PushRecent(path);
            }
            catch (Exception ex)
            {
                Logger.Warn("PlotDialogForm", "Could not open " + path + ": " + ex.Message);
            }
        }

        private void OpenInTc(string path)
        {
            if (_tcExePath == null)
            {
                MessageBox.Show(this, "Total Commander not found. Expected:\n" + TcPaths[0] +
                    "\n\nEdit TcPaths in PlotDialogForm.cs.", "TC Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
            if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
            {
                Logger.Warn("PlotDialogForm", "Folder not found: " + path);
                return;
            }
            try
            {
                Process.Start(_tcExePath, "/O /T /L=\"" + path + "\"");
                PushRecent(path);
                Logger.Info("PlotDialogForm", "Opened in Total Commander: " + path);
            }
            catch (Exception ex)
            {
                Logger.Warn("PlotDialogForm", "Could not open Total Commander: " + ex.Message);
            }
        }

        /// <summary>Finds the subfolder with the most recent last-write time under basePath (matches PDF_Project.py's _recent_subfolder: newest by mtime, not by name).</summary>
        private static string FindMostRecentSubfolder(string basePath)
        {
            if (string.IsNullOrEmpty(basePath) || !Directory.Exists(basePath)) return null;
            try
            {
                return Directory.GetDirectories(basePath)
                    .OrderByDescending(d => Directory.GetLastWriteTime(d))
                    .FirstOrDefault();
            }
            catch
            {
                return null;
            }
        }

        private void OpenRecentFolder(bool useTc)
        {
            string basePath = CurrentLocationPath();
            string latest = FindMostRecentSubfolder(basePath);
            if (latest == null)
            {
                MessageBox.Show(this, "No subfolders found under:\n" + basePath, "Recent Folder",
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            if (useTc) OpenInTc(latest);
            else OpenInExplorer(latest);
        }

        private void PushRecent(string path)
        {
            _recentFolders.Remove(path);
            _recentFolders.Insert(0, path);
            if (_recentFolders.Count > 8)
                _recentFolders.RemoveRange(8, _recentFolders.Count - 8);
        }

        private void ShowRecentMenu()
        {
            var menu = new ContextMenuStrip();

            if (_recentFolders.Count == 0)
            {
                menu.Items.Add("(no recent folders)").Enabled = false;
            }
            else
            {
                foreach (var folder in _recentFolders)
                {
                    string captured = folder;
                    menu.Items.Add(folder, null, (s, e) => OpenInExplorer(captured));
                }
                menu.Items.Add(new ToolStripSeparator());
                menu.Items.Add("Clear history", null, (s, e) => _recentFolders.Clear());
            }

            menu.Show(_btnRecent, new Point(0, _btnRecent.Height));
        }

        private static string CsvEscape(string s)
        {
            s = s ?? "";
            return (s.IndexOf(',') >= 0 || s.IndexOf('"') >= 0 || s.IndexOf('\n') >= 0)
                ? "\"" + s.Replace("\"", "\"\"") + "\""
                : s;
        }

        /// <summary>Ported from PDF_Project.py's export_csv - same column order (Project, Filename, Modified, Created, Subfolder/Location, Full Path). Scans the folder tree directly (recursively) now that there's no browsable list to read selections from.</summary>
        private void ExportCsv()
        {
            string folder = RootFolder();
            if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
            {
                MessageBox.Show(this, "Select a project/location first.", "CSV Export", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            using (var sfd = new SaveFileDialog { Filter = "CSV Files (*.csv)|*.csv", DefaultExt = "csv" })
            {
                if (sfd.ShowDialog(this) != DialogResult.OK) return;

                try
                {
                    var proj = _cboProject.SelectedItem as PdfProject;
                    string location = _cboLocation?.SelectedItem as string ?? "";

                    var sb = new StringBuilder();
                    sb.AppendLine("Project,Filename,Modified,Created,Location,Full Path");
                    foreach (var full in Directory.GetFiles(folder, "*.pdf", SearchOption.AllDirectories)
                        .OrderByDescending(f => File.GetLastWriteTime(f)))
                    {
                        string name = System.IO.Path.GetFileName(full);
                        DateTime mod = File.GetLastWriteTime(full);
                        DateTime crea = File.GetCreationTime(full);

                        sb.AppendLine(string.Join(",",
                            CsvEscape(proj?.Name), CsvEscape(name),
                            CsvEscape(mod.ToString("yyyy-MM-dd HH:mm")), CsvEscape(crea.ToString("yyyy-MM-dd HH:mm")),
                            CsvEscape(location), CsvEscape(full)));
                    }

                    File.WriteAllText(sfd.FileName, sb.ToString(), new UTF8Encoding(true));
                    Logger.Info("PlotDialogForm", "Exported CSV: " + sfd.FileName);
                    MessageBox.Show(this, "Exported: " + sfd.FileName, "CSV Export", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch (Exception ex)
                {
                    Logger.Error("PlotDialogForm", "CSV export failed: " + ex.Message);
                    MessageBox.Show(this, "Export failed:\n" + ex.Message, "CSV Export", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        /// <summary>Ported from PDF_Project.py's export_new_files_csv - a small date-cutoff prompt, then the same CSV columns filtered to files modified on/after that date.</summary>
        private void ExportNewFilesCsv()
        {
            string folder = RootFolder();
            if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
            {
                MessageBox.Show(this, "Select a project/location with an existing folder first.", "New Files CSV", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            using (var dlg = new Form
            {
                Text = "Export New Files Since...",
                FormBorderStyle = FormBorderStyle.Sizable,
                StartPosition = FormStartPosition.CenterParent,
                ClientSize = new Size(320, 150),
                MaximizeBox = true,
                MinimizeBox = true,
            })
            {
                var lbl = new Label { Text = "Show files modified on/after:", Location = new Point(15, 15), AutoSize = true };
                var dtp = new DateTimePicker { Location = new Point(15, 40), Width = 180, Format = DateTimePickerFormat.Short, Value = DateTime.Now };

                var btnExport = Theme.CreateButton("Export", AppIcon.Export, 90);
                var btnCancel = Theme.CreateButton("Cancel", AppIcon.Cancel, 90);
                C3DTheme.Role(btnExport, C3DTheme.RolePrimary);
                C3DTheme.Role(btnCancel, C3DTheme.RoleDanger);
                btnExport.Location = new Point(15, 90);
                btnCancel.Location = new Point(115, 90);
                btnExport.DialogResult = DialogResult.OK;
                btnCancel.DialogResult = DialogResult.Cancel;

                dlg.Controls.Add(lbl);
                dlg.Controls.Add(dtp);
                dlg.Controls.Add(btnExport);
                dlg.Controls.Add(btnCancel);
                dlg.AcceptButton = btnExport;
                dlg.CancelButton = btnCancel;
                C3DTheme.Apply(dlg, C3DPalette.Dark);

                if (dlg.ShowDialog(this) != DialogResult.OK) return;
                DateTime cutoff = dtp.Value.Date;

                List<string> matches;
                try
                {
                    matches = Directory.GetFiles(folder, "*.pdf")
                        .Where(f => File.GetLastWriteTime(f).Date >= cutoff)
                        .ToList();
                }
                catch (Exception ex)
                {
                    Logger.Error("PlotDialogForm", "Could not scan " + folder + ": " + ex.Message);
                    return;
                }

                if (matches.Count == 0)
                {
                    MessageBox.Show(this, "No PDFs modified on/after " + cutoff.ToShortDateString() + ".",
                        "Nothing Found", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    return;
                }

                using (var sfd = new SaveFileDialog
                {
                    Filter = "CSV Files (*.csv)|*.csv",
                    FileName = "new_since_" + cutoff.ToString("yyyy-MM-dd") + ".csv",
                })
                {
                    if (sfd.ShowDialog(this) != DialogResult.OK) return;

                    try
                    {
                        var proj = _cboProject.SelectedItem as PdfProject;
                        string location = _cboLocation?.SelectedItem as string ?? "";

                        var sb = new StringBuilder();
                        sb.AppendLine("Project,Filename,Modified,Created,Location,Full Path");
                        foreach (var full in matches)
                        {
                            sb.AppendLine(string.Join(",",
                                CsvEscape(proj?.Name), CsvEscape(System.IO.Path.GetFileName(full)),
                                CsvEscape(File.GetLastWriteTime(full).ToString("yyyy-MM-dd HH:mm")),
                                CsvEscape(File.GetCreationTime(full).ToString("yyyy-MM-dd HH:mm")),
                                CsvEscape(location), CsvEscape(full)));
                        }

                        File.WriteAllText(sfd.FileName, sb.ToString(), new UTF8Encoding(true));
                        Logger.Info("PlotDialogForm", "Exported " + matches.Count + " new file(s) to " + sfd.FileName);
                        MessageBox.Show(this, "Exported " + matches.Count + " file(s).\n" + sfd.FileName,
                            "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
                    }
                    catch (Exception ex)
                    {
                        Logger.Error("PlotDialogForm", "CSV export failed: " + ex.Message);
                        MessageBox.Show(this, "Export failed:\n" + ex.Message, "New Files CSV", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                }
            }
        }



        private void BuildBottomBar()
        {
            int barY = ClientSize.Height - 44;

            _btnPreview = Theme.CreateButton("Preview...", AppIcon.Preview, 120);
            _btnPreview.Click += (s, e) => ShowPreview();

            _btnApplyToLayout = Theme.CreateButton("Apply to Layout", AppIcon.Apply, 170);
            _btnApplyToLayout.Click += (s, e) => ApplyToLayout();

            _btnOk = Theme.CreateButton("OK", AppIcon.Ok, 90);
            _btnOk.DialogResult = DialogResult.OK;
            _btnOk.Click += (s, e) => Logger.Info("PlotDialogForm", "Plot confirmed for layout '" + _layoutName + "'.");

            _btnCancel = Theme.CreateButton("Cancel", AppIcon.Cancel, 100);
            _btnCancel.DialogResult = DialogResult.Cancel;
            _btnCancel.Click += (s, e) => Logger.Info("PlotDialogForm", "Plot dialog cancelled.");

            _btnHelp = Theme.CreateButton("Help", AppIcon.Help, 90);
            _btnHelp.Click += (s, e) => Logger.Info("PlotDialogForm", "Help requested.");

            _btnCollapse = Theme.CreateButton("", AppIcon.Move, 34);
            _btnCollapse.Click += (s, e) => ToggleRightPanel();

            AcceptButton = _btnOk;
            CancelButton = _btnCancel;

            Controls.Add(_btnPreview);
            Controls.Add(_btnApplyToLayout);
            Controls.Add(_btnOk);
            Controls.Add(_btnCancel);
            Controls.Add(_btnHelp);
            Controls.Add(_btnCollapse);

            RepositionBottomBar();
        }

        private void ToggleRightPanel()
        {
            _rightPanelCollapsed = !_rightPanelCollapsed;
            int collapsedWidth = ClientSize.Width - _rightPanel.Left - 12;

            if (_rightPanelCollapsed)
            {
                ClientSize = new Size(_rightPanel.Left + 12, ClientSize.Height);
            }
            else
            {
                ClientSize = new Size(_rightPanel.Left + _rightPanel.Width + 12, ClientSize.Height);
            }

            RepositionBottomBar();
            Logger.Debug("PlotDialogForm", "Right panel " + (_rightPanelCollapsed ? "collapsed." : "expanded."));
        }

        /// <summary>
        /// Right-to-left stack off each button's real (now-generous) width,
        /// rather than the previous hardcoded offsets - those offsets
        /// assumed narrower buttons than "Apply to Layout"/"Cancel" actually
        /// need to show their full label without clipping, and didn't
        /// account for the window now being resizable (this recalculates
        /// from the *current* ClientSize, called again on every Resize).
        /// </summary>
        private void RepositionBottomBar()
        {
            int barY = ClientSize.Height - 44;
            const int gap = 8, rightMargin = 12;

            _btnPreview.Location = new Point(12, barY);

            int x = ClientSize.Width - rightMargin;
            x -= _btnCollapse.Width; _btnCollapse.Location = new Point(x, barY); x -= gap;
            x -= _btnHelp.Width; _btnHelp.Location = new Point(x, barY); x -= gap;
            x -= _btnCancel.Width; _btnCancel.Location = new Point(x, barY); x -= gap;
            x -= _btnOk.Width; _btnOk.Location = new Point(x, barY); x -= gap;
            x -= _btnApplyToLayout.Width; _btnApplyToLayout.Location = new Point(x, barY);
        }

        /// <summary>
        /// Same interaction as the native AutoCAD Plot dialog's "Window&lt;"
        /// button: the dialog hides itself, two points are picked directly
        /// in the drawing editor, then the dialog reappears with the picked
        /// extents recorded. This works from a modal dialog shown via
        /// Application.ShowModalDialog because that call pumps a nested
        /// message loop, which is what lets Editor.GetPoint/GetCorner run
        /// while the dialog is technically still "open".
        /// </summary>
        private void PickWindow()
        {
            Document doc = AcadApp.DocumentManager.MdiActiveDocument;
            if (doc == null)
                return;

            Hide();
            try
            {
                Editor ed = doc.Editor;

                PromptPointOptions ppo1 = new PromptPointOptions("\nSpecify first corner: ");
                PromptPointResult ppr1 = ed.GetPoint(ppo1);
                if (ppr1.Status != PromptStatus.OK)
                {
                    Logger.Info("PlotDialogForm", "Window pick cancelled.");
                    return;
                }

                PromptCornerOptions ppo2 = new PromptCornerOptions("\nSpecify opposite corner: ", ppr1.Value);
                PromptPointResult ppr2 = ed.GetCorner(ppo2);
                if (ppr2.Status != PromptStatus.OK)
                {
                    Logger.Info("PlotDialogForm", "Window pick cancelled.");
                    return;
                }

                double minX = Math.Min(ppr1.Value.X, ppr2.Value.X);
                double minY = Math.Min(ppr1.Value.Y, ppr2.Value.Y);
                double maxX = Math.Max(ppr1.Value.X, ppr2.Value.X);
                double maxY = Math.Max(ppr1.Value.Y, ppr2.Value.Y);

                _windowMin = new Point2d(minX, minY);
                _windowMax = new Point2d(maxX, maxY);

                _cboWhatToPlot.SelectedItem = "Window";
                Logger.Info("PlotDialogForm", string.Format(
                    "Window picked: ({0:0.###}, {1:0.###}) - ({2:0.###}, {3:0.###})", minX, minY, maxX, maxY));
            }
            finally
            {
                Show();
            }
        }

        // =====================================================================
        // PUBLIC SETTINGS SURFACE - read by PlotService.ExecutePlot(...)
        // =====================================================================

        public string SelectedPrinterName => _cboPrinter.SelectedItem as string;
        public string SelectedPlotStyleTable => _cboPlotStyleTable.SelectedItem as string;
        public string WhatToPlot => _cboWhatToPlot.SelectedItem as string;

        public bool PlotToFile => _chkPlotToFile.Checked;
        public bool CenterPlot => _chkCenterPlot.Checked;
        public bool FitToPaper => _chkFitToPaper.Checked;
        public bool Landscape => _radLandscape.Checked;
        public bool UpsideDown => _chkPlotUpsideDown.Checked;

        public bool PlotObjectLineweights => _chkPlotObjectLineweights.Checked;
        public bool PlotTransparency => _chkPlotTransparency.Checked;
        public bool PlotWithPlotStyles => _chkPlotWithPlotStyles.Checked;
        public bool HideOrShadedPaperspaceObjects => _chkHidePaperspaceObjects.Checked;

        public bool HasWindowSelection => _windowMin.HasValue && _windowMax.HasValue;
        public Point2d? WindowMin => _windowMin;
        public Point2d? WindowMax => _windowMax;

        public double OffsetX => ParseInvariant(_txtOffsetX.Text);
        public double OffsetY => ParseInvariant(_txtOffsetY.Text);

        /// <summary>Physical paper width in mm, parsed from the paper size combo (falls back to the 1350mm sample).</summary>
        public double PaperWidthMm => _paperWidthMm;

        /// <summary>Physical paper height in mm, parsed from the paper size combo (falls back to the 594mm sample).</summary>
        public double PaperHeightMm => _paperHeightMm;

        /// <summary>Custom scale numerator, e.g. "1" in "1 mm = 1 units". Returns 0 when a named standard scale is selected instead.</summary>
        public double ScaleNumerator => string.Equals(_cboScale.SelectedItem as string, "Custom", StringComparison.OrdinalIgnoreCase)
            ? ParseInvariant(_txtScaleMm.Text)
            : 0;

        /// <summary>Custom scale denominator (drawing units per the numerator). Returns 0 when a named standard scale is selected instead.</summary>
        public double ScaleDenominator => string.Equals(_cboScale.SelectedItem as string, "Custom", StringComparison.OrdinalIgnoreCase)
            ? ParseInvariant(_txtScaleUnits.Text)
            : 0;

        /// <summary>The folder created via "Create Today Folder" on the PDF Output tab, or null if none has been created yet this session.</summary>
        public string PdfOutputFolder => _todayFolder;

        /// <summary>Whether PlotService should save straight into PdfOutputFolder instead of showing a Save As dialog.</summary>
        public bool SaveDirectlyToProjectFolder => _chkSaveDirectlyToFolder != null && _chkSaveDirectlyToFolder.Checked;

        private static double ParseInvariant(string text)
        {
            return double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double v) ? v : 0;
        }

        private void WireDefaults()
        {
            _paperPreview.SetWorldBounds(1350, 594);
            RefreshMiniPreview();
            PopulateRealDeviceAndStyleLists();
        }

        /// <summary>
        /// Recomputes the plot-area rectangle shown in the small paper
        /// preview thumbnail from the current Fit to paper / Center / Offset
        /// / Orientation settings, and repaints it. Previously the thumbnail
        /// was seeded once with a hardcoded rectangle and never updated
        /// again, so it visually disagreed with the dialog as soon as any of
        /// those settings changed - this keeps it live.
        /// </summary>
        private void RefreshMiniPreview()
        {
            if (_paperPreview == null) return;

            double paperW = PaperWidthMm;
            double paperH = PaperHeightMm;
            if (!Landscape)
            {
                double tmp = paperW; paperW = paperH; paperH = tmp;
            }

            double margin = Math.Min(paperW, paperH) * 0.01;
            double plotW = paperW - 2 * margin;
            double plotH = paperH - 2 * margin;

            double offX = CenterPlot ? (paperW - plotW) / 2.0 : Math.Max(0, OffsetX);
            double offY = CenterPlot ? (paperH - plotH) / 2.0 : Math.Max(0, OffsetY);

            var plotArea = new RectangleF((float)offX, (float)offY, (float)plotW, (float)plotH);

            _paperPreview.SetWorldBounds((float)paperW, (float)paperH);
            _paperPreview.SetPlotArea(plotArea);
        }

        /// <summary>
        /// "Preview..." triggers AutoCAD/Civil 3D's own real plot preview -
        /// distinct from the "PDF Preview" tab, which browses the project's
        /// already-output PDF files rather than previewing this plot.
        ///
        /// History worth knowing before touching this again: driving
        /// <c>PlotFactory.CreatePreviewEngine</c> directly showed nothing;
        /// <c>Editor.Command("_.PREVIEW")</c> hung Civil 3D outright and was
        /// reverted. What's used here - staging settings via
        /// <see cref="PlotService.ApplyToLayout"/> then running the real
        /// <c>_.PREVIEW</c> command via <c>SendStringToExecute</c> - is the
        /// one approach confirmed to actually work, with one known,
        /// unavoidable quirk: <c>SendStringToExecute</c> queues the command
        /// rather than running it immediately, and this PLOTEXTRA command is
        /// still "active" while this dialog is open, so AutoCAD only
        /// processes it once this dialog is closed (OK/Cancel/Esc) - not
        /// instantly. Without warning, that delay was mistaken for "Cancel
        /// is plotting" once it fired. So: tell the person up front instead.
        /// </summary>
        private void ShowPreview()
        {
            Document doc = AcadApp.DocumentManager.MdiActiveDocument;
            if (doc == null) return;

            if (PlotFactory.ProcessPlotState != ProcessPlotState.NotPlotting)
            {
                MessageBox.Show(this,
                    "A plot/preview already appears to be in progress (ProcessPlotState = "
                    + PlotFactory.ProcessPlotState + ").\n\nIf nothing is visibly open, closing and " +
                    "reopening this drawing (or restarting Civil 3D) is the only reliable way to clear it.",
                    "Preview", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            bool applied = PlotService.ApplyToLayout(this, _layoutName, out string error);
            if (!applied)
            {
                MessageBox.Show(this, "Could not stage settings for preview:\n\n" + error,
                    "Preview", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            try
            {
                string current = LayoutManager.Current.CurrentLayout;
                if (!string.Equals(current, _layoutName, StringComparison.OrdinalIgnoreCase))
                    LayoutManager.Current.CurrentLayout = _layoutName;

                doc.SendStringToExecute("_.PREVIEW \n", true, false, false);
                Logger.Info("PlotDialogForm", "Native preview (PREVIEW command) requested for layout '" + _layoutName + "'.");

                MessageBox.Show(this,
                    "The real AutoCAD/Civil 3D preview has been requested for layout '" + _layoutName + "'.\n\n" +
                    "It will appear once this Plot dialog is closed (OK, Cancel, or Esc) - AutoCAD only " +
                    "processes it once this command finishes, not while this dialog is still open. " +
                    "That's expected, not an error - keep adjusting settings and close the dialog " +
                    "whenever you're ready to see it.",
                    "Preview requested", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                Logger.Error("PlotDialogForm", "AutoCAD API error while requesting preview: " + ex.Message);
                MessageBox.Show(this, "Could not request preview:\n\n" + ex.Message, "Preview", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            catch (Exception ex)
            {
                Logger.Error("PlotDialogForm", "Unexpected error while requesting preview: " + ex.Message);
                MessageBox.Show(this, "Could not request preview:\n\n" + ex.Message, "Preview", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        /// <summary>Feeds sampled world-space polylines (e.g. from Curve/Polyline entities) into the small paper preview thumbnail.</summary>
        public void SetPreviewGeometry(System.Collections.Generic.IEnumerable<System.Collections.Generic.IEnumerable<PointF>> polylines)
        {
            _paperPreview.SetGeometry(polylines ?? System.Linq.Enumerable.Empty<System.Collections.Generic.IEnumerable<PointF>>());
        }
    }
}


// ===== FILE: UI\LogViewerForm.cs =====
using System;
using System.Drawing;
using System.Windows.Forms;
using PlotExtra.Logging;

namespace PlotExtra.UI
{
    /// <summary>
    /// Live view of Logger entries. Demonstrates the reusable "stays open
    /// alongside AutoCAD" form pattern:
    ///   - FormBorderStyle.SizableToolWindow: small title bar, no taskbar
    ///     entry, stays above the AutoCAD main window.
    ///   - ProcessCmdKey swallows Escape so pressing Esc while focus is in
    ///     the AutoCAD viewport (or this form) doesn't close the dialog
    ///     accidentally mid-command.
    ///   - Shown via Application.ShowModelessDialog from Commands.cs, so it
    ///     can stay open while the user switches between Model Space and
    ///     Paper Space freely.
    /// </summary>
    public class LogViewerForm : Form
    {
        private readonly ListBox _list;
        private readonly Button _btnSave;
        private readonly Button _btnClear;
        private readonly Label _status;

        public LogViewerForm()
        {
            Text = "PlotExtra - Log";
            FormBorderStyle = FormBorderStyle.Sizable;
            MaximizeBox = true;
            MinimizeBox = true;
            StartPosition = FormStartPosition.Manual;
            ShowInTaskbar = false;
            TopMost = true;
            MinimumSize = new Size(420, 220);
            Size = new Size(520, 320);

            _list = new ListBox
            {
                Dock = DockStyle.Fill,
                Font = new Font("Consolas", 8.5f),
                HorizontalScrollbar = true,
                IntegralHeight = false
            };

            var buttonPanel = new FlowLayoutPanel
            {
                Dock = DockStyle.Bottom,
                Height = 34,
                FlowDirection = FlowDirection.LeftToRight,
                Padding = new Padding(4)
            };

            _btnSave = Theme.CreateButton("Save log", AppIcon.Save, 100);
            _btnSave.Click += (s, e) => SaveLog();

            _btnClear = Theme.CreateButton("Clear", AppIcon.Delete, 90);
            _btnClear.Click += (s, e) => { Logger.Clear(); _list.Items.Clear(); };

            buttonPanel.Controls.Add(_btnSave);
            buttonPanel.Controls.Add(_btnClear);

            _status = new Label
            {
                Dock = DockStyle.Bottom,
                Height = 20,
                TextAlign = ContentAlignment.MiddleLeft,
                Padding = new Padding(4, 0, 0, 0),
                ForeColor = Color.DimGray
            };

            Controls.Add(_list);
            Controls.Add(buttonPanel);
            Controls.Add(_status);

            foreach (var entry in Logger.Entries)
                _list.Items.Add(entry.ToString());
            ScrollToEnd();

            Logger.EntryAdded += Logger_EntryAdded;
            FormClosed += (s, e) => Logger.EntryAdded -= Logger_EntryAdded;

            C3DTheme.Role(_btnSave, C3DTheme.RoleRefresh);
            C3DTheme.Role(_btnClear, C3DTheme.RoleDanger);
            C3DTheme.Apply(this, C3DPalette.Dark);
        }

        private void Logger_EntryAdded(object sender, LogEntry e)
        {
            if (IsDisposed) return;

            if (InvokeRequired)
            {
                BeginInvoke(new Action(() => AppendEntry(e)));
            }
            else
            {
                AppendEntry(e);
            }
        }

        private void AppendEntry(LogEntry e)
        {
            _list.Items.Add(e.ToString());
            ScrollToEnd();
        }

        private void ScrollToEnd()
        {
            if (_list.Items.Count > 0)
                _list.TopIndex = _list.Items.Count - 1;
        }

        private void SaveLog()
        {
            string path = Logger.SaveToFile();
            _status.Text = "Saved to " + path;
            Logger.Info("LogViewer", "Log appended to " + path);
        }

        /// <summary>
        /// Swallows Escape so it doesn't propagate and close this modeless
        /// tool window while the user is mid-command in the AutoCAD editor.
        /// </summary>
        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            const int WM_KEYDOWN = 0x100;
            if (msg.Msg == WM_KEYDOWN && keyData == Keys.Escape)
                return true; // handled, do not close / do not bubble up

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


// ===== FILE: UI\PdfProjectManagerForm.cs =====
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using PlotExtra.Core;

namespace PlotExtra.UI
{
    /// <summary>
    /// Add/edit/delete the project (name -> PDF output folder) list used by
    /// the Plot dialog's "PDF Output" tab. Same directly-editable
    /// DataGridView + BindingList&lt;T&gt; pattern as ParallelPipesNet's
    /// ProfileManagerForm.
    /// </summary>
    public class PdfProjectManagerForm : Form
    {
        private readonly List<PdfProject> _target;
        private readonly BindingList<PdfProject> _working;
        private readonly DataGridView _grid;

        public PdfProjectManagerForm(List<PdfProject> projects)
        {
            _target = projects;
            _working = new BindingList<PdfProject>();
            foreach (var p in projects) _working.Add(p.Clone());

            Text = "Manage PDF Projects";
            FormBorderStyle = FormBorderStyle.Sizable;
            MaximizeBox = true;
            MinimizeBox = true;
            StartPosition = FormStartPosition.CenterParent;
            Size = new Size(720, 420);

            var layout = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 3, ColumnCount = 1 };
            layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
            layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            Controls.Add(layout);

            _grid = new DataGridView
            {
                Dock = DockStyle.Fill,
                DataSource = _working,
                AutoGenerateColumns = true,
                AllowUserToAddRows = false,
                RowHeadersVisible = false,
                AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
            };
            layout.Controls.Add(_grid, 0, 0);

            var browseRow = new FlowLayoutPanel { AutoSize = true, Padding = new Padding(4) };
            var add = Theme.CreateButton("Add", AppIcon.Add, 90);
            var browse = Theme.CreateButton("Browse path...", AppIcon.Open, 130);
            var del = Theme.CreateButton("Delete", AppIcon.Delete, 90);
            C3DTheme.Role(add, C3DTheme.RoleRefresh);
            C3DTheme.Role(browse, C3DTheme.RoleNeutral);
            C3DTheme.Role(del, C3DTheme.RoleDanger);

            add.Click += (s, e) =>
            {
                _working.Add(new PdfProject("New project", ""));
                _grid.CurrentCell = _grid.Rows[_working.Count - 1].Cells[0];
            };
            browse.Click += (s, e) =>
            {
                int i = SelectedIndex();
                if (i < 0) return;
                using (var fbd = new FolderBrowserDialog { Description = "Select the PDF output folder for this project" })
                {
                    if (fbd.ShowDialog() == DialogResult.OK)
                        _working[i].Path = fbd.SelectedPath;
                }
            };
            del.Click += (s, e) => { int i = SelectedIndex(); if (i >= 0) _working.RemoveAt(i); };
            browseRow.Controls.AddRange(new Control[] { add, browse, del });
            layout.Controls.Add(browseRow, 0, 1);

            var okRow = new FlowLayoutPanel { AutoSize = true, Padding = new Padding(4), FlowDirection = FlowDirection.RightToLeft };
            var cancelBtn = Theme.CreateButton("Cancel", AppIcon.Cancel, 90);
            var okBtn = Theme.CreateButton("OK", AppIcon.Ok, 90);
            cancelBtn.DialogResult = DialogResult.Cancel;
            C3DTheme.Role(cancelBtn, C3DTheme.RoleDanger);
            C3DTheme.Role(okBtn, C3DTheme.RolePrimary);
            okBtn.Click += (s, e) => CommitAndClose();
            okRow.Controls.Add(cancelBtn);
            okRow.Controls.Add(okBtn);
            layout.Controls.Add(okRow, 0, 2);

            AcceptButton = okBtn;
            CancelButton = cancelBtn;

            C3DTheme.Apply(this, C3DPalette.Dark);
        }

        private int SelectedIndex() => _grid.CurrentRow?.Index ?? -1;

        private void CommitAndClose()
        {
            _target.Clear();
            foreach (var p in _working) _target.Add(p);
            PdfProjectStore.Save(_target);
            DialogResult = DialogResult.OK;
            Close();
        }
    }
}


// ===== FILE: UI\PreviewControl.cs =====
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace PlotExtra.UI
{
    /// <summary>
    /// Small self-contained preview panel, the same pattern used for the
    /// tiny paper-size preview in the top-right of the real AutoCAD Plot
    /// dialog. Fully decoupled from AutoCAD drawing APIs: callers hand it
    /// world-space geometry (already sampled from Curve/Polyline entities),
    /// and it takes care of the world -> panel-pixel transform and drawing.
    ///
    /// Reuse pattern for another project:
    ///   1. During entity selection, sample points into world space, e.g.
    ///        var pts = new List&lt;PointF&gt;();
    ///        for (double p = 0; p &lt;= curve.EndParam; p += step)
    ///            pts.Add(ToPointF(curve.GetPointAtParameter(p)));
    ///        // or for a Polyline: poly.GetPoint2dAt(i) per vertex
    ///   2. previewControl.SetGeometry(new[] { pts });
    ///   3. previewControl.Invalidate();
    /// No AutoCAD API is touched after step 1 - everything below this
    /// point is plain GDI+ / WinForms and can be copy-pasted as-is.
    /// </summary>
    public class PreviewControl : Control
    {
        private RectangleF _worldBounds = new RectangleF(0, 0, 100, 100);
        private RectangleF _plotAreaWorld = RectangleF.Empty;
        private readonly List<PointF[]> _polylines = new List<PointF[]>();

        public Color PaperColor { get; set; } = Color.White;
        public Color HatchColor { get; set; } = Color.FromArgb(90, 90, 90);
        public Color GeometryColor { get; set; } = Color.FromArgb(30, 30, 30);

        public PreviewControl()
        {
            SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
                      ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
            BackColor = Color.White;
        }

        /// <summary>World-space bounds of the full paper (e.g. 1350 x 594 mm).</summary>
        public void SetWorldBounds(float width, float height)
        {
            _worldBounds = new RectangleF(0, 0, width, height);
            Invalidate();
        }

        /// <summary>World-space rectangle representing the actual plottable / printable area (drawn hatched, like the real dialog).</summary>
        public void SetPlotArea(RectangleF plotAreaWorld)
        {
            _plotAreaWorld = plotAreaWorld;
            Invalidate();
        }

        /// <summary>Replaces the collected geometry (e.g. sampled curves/polylines) shown in the preview.</summary>
        public void SetGeometry(IEnumerable<IEnumerable<PointF>> polylines)
        {
            _polylines.Clear();
            foreach (var line in polylines)
            {
                var arr = new List<PointF>(line).ToArray();
                if (arr.Length >= 2)
                    _polylines.Add(arr);
            }
            Invalidate();
        }

        private const int PreviewPadding = 4;

        /// <summary>Computes the uniform scale that fits the world bounds into the panel, preserving aspect ratio.</summary>
        private float GetScale()
        {
            if (_worldBounds.Width <= 0 || _worldBounds.Height <= 0) return 1f;
            float sx = (Width - 2 * PreviewPadding) / _worldBounds.Width;
            float sy = (Height - 2 * PreviewPadding) / _worldBounds.Height;
            return Math.Min(sx, sy);
        }

        /// <summary>
        /// World -> panel-pixel transform. Previously this only offset by a
        /// fixed preview padding from the top-left corner, so whenever the panel's
        /// aspect ratio didn't match the paper's (the normal case - most
        /// paper sizes are far wider/taller than the preview panel), the
        /// leftover space from the aspect-fit ended up entirely on the
        /// right/bottom instead of split evenly - which is exactly what
        /// produced the lopsided hatch margin (thick on one edge, invisible
        /// on the other three) in the reported screenshot. This now
        /// centers the fitted paper in the panel on both axes, matching the
        /// standard "scale = Min(panelW/worldW, panelH/worldH)" preview
        /// pattern used elsewhere in the project.
        /// </summary>
        private PointF WorldToPanel(PointF world, float scale)
        {
            float drawW = _worldBounds.Width * scale;
            float drawH = _worldBounds.Height * scale;
            float padX = PreviewPadding + (Width - 2 * PreviewPadding - drawW) / 2f;
            float padY = PreviewPadding + (Height - 2 * PreviewPadding - drawH) / 2f;

            // Flip Y: world Y-up -> panel Y-down.
            float px = padX + (world.X - _worldBounds.X) * scale;
            float py = padY + (_worldBounds.Height - (world.Y - _worldBounds.Y)) * scale;
            return new PointF(px, py);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            var g = e.Graphics;
            g.SmoothingMode = SmoothingMode.AntiAlias;

            using (var border = new Pen(Theme.PanelBorder))
                g.DrawRectangle(border, 0, 0, Width - 1, Height - 1);

            float scale = GetScale();

            // Paper rectangle (white sheet)
            var paperTL = WorldToPanel(new PointF(_worldBounds.X, _worldBounds.Y + _worldBounds.Height), scale);
            var paperBR = WorldToPanel(new PointF(_worldBounds.X + _worldBounds.Width, _worldBounds.Y), scale);
            var paperRect = RectangleF.FromLTRB(paperTL.X, paperTL.Y, paperBR.X, paperBR.Y);

            using (var paperBrush = new SolidBrush(PaperColor))
                g.FillRectangle(paperBrush, paperRect);
            using (var paperPen = new Pen(Color.Black))
                g.DrawRectangle(paperPen, paperRect.X, paperRect.Y, paperRect.Width, paperRect.Height);

            // Hatched margin outside the printable/plot area (matches the diagonal
            // hatch shown around the paper preview in the real AutoCAD dialog).
            if (!_plotAreaWorld.IsEmpty)
            {
                var pTL = WorldToPanel(new PointF(_plotAreaWorld.X, _plotAreaWorld.Y + _plotAreaWorld.Height), scale);
                var pBR = WorldToPanel(new PointF(_plotAreaWorld.X + _plotAreaWorld.Width, _plotAreaWorld.Y), scale);
                var plotRect = RectangleF.FromLTRB(pTL.X, pTL.Y, pBR.X, pBR.Y);

                using (var hatchBrush = new HatchBrush(HatchStyle.BackwardDiagonal, HatchColor, PaperColor))
                using (var region = new Region(paperRect))
                {
                    region.Exclude(plotRect);
                    g.FillRegion(hatchBrush, region);
                }
                using (var plotPen = new Pen(Color.DimGray))
                    g.DrawRectangle(plotPen, plotRect.X, plotRect.Y, plotRect.Width, plotRect.Height);
            }

            // Collected geometry (sampled curves / polylines)
            using (var geomPen = new Pen(GeometryColor, 1f))
            {
                foreach (var line in _polylines)
                {
                    var panelPts = new PointF[line.Length];
                    for (int i = 0; i < line.Length; i++)
                        panelPts[i] = WorldToPanel(line[i], scale);
                    if (panelPts.Length >= 2)
                        g.DrawLines(geomPen, panelPts);
                }
            }
        }
    }
}


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

namespace PlotExtra.UI
{
    /// <summary>
    /// Central place for visual styling. Call Theme.StyleButton(...) instead
    /// of setting Button.Image/TextImageRelation/ImageAlign on each call
    /// site individually - that way every button in the project (existing
    /// and future) automatically gets a real icon instead of an emoji glyph
    /// in Button.Text.
    /// </summary>
    public static class Theme
    {
        public static readonly Color PanelBorder = Color.FromArgb(200, 200, 200);
        public static readonly Color GroupHeader = Color.FromArgb(40, 40, 40);
        public static readonly Font DefaultFont = new Font("Segoe UI", 9f);

        // Remembers which AppIcon a button was created with, so C3DTheme.StyleButton
        // (applied afterwards, when roles/colors are known) can regenerate the icon
        // in a color that contrasts with the final button background - a dark-gray
        // icon on a green/blue/red role button would otherwise be hard to read.
        private static readonly System.Collections.Generic.Dictionary<Button, AppIcon> _iconOf =
            new System.Collections.Generic.Dictionary<Button, AppIcon>();

        public static bool TryGetIcon(Button button, out AppIcon icon) => _iconOf.TryGetValue(button, out icon);

        /// <summary>
        /// Applies a generated icon + consistent layout to a button.
        /// Text.Trim() is used verbatim (e.g. "Save", not "?? Save") -
        /// the icon communicates the affordance, the text stays clean.
        /// </summary>
        public static Button StyleButton(Button button, AppIcon icon, int minWidth = 96)
        {
            button.Image = ButtonIconFactory.Get(icon, 16);
            button.TextImageRelation = TextImageRelation.ImageBeforeText;
            button.ImageAlign = ContentAlignment.MiddleLeft;
            button.TextAlign = ContentAlignment.MiddleCenter;
            button.Padding = new Padding(8, 0, 4, 0);
            button.Font = DefaultFont;
            button.UseVisualStyleBackColor = true;

            _iconOf[button] = icon;

            if (button.Width < minWidth)
                button.Width = minWidth;

            return button;
        }

        /// <summary>Convenience overload for buttons created inline.</summary>
        public static Button CreateButton(string text, AppIcon icon, int width = 96, int height = 26)
        {
            var b = new Button
            {
                Text = text,
                Width = width,
                Height = height
            };
            StyleButton(b, icon, width);
            return b;
        }

        public static GroupBox CreateGroup(string title, int width, int height)
        {
            return new GroupBox
            {
                Text = title,
                Width = width,
                Height = height,
                Font = DefaultFont
            };
        }
    }
}


// ===== FILE: Cad\PlotService.cs =====
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Drawing;
using System.Text;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.PlottingServices;
using PlotExtra.Logging;
using PlotExtra.UI;
using AcPointF = System.Drawing.PointF;

namespace PlotExtra.Cad
{
    /// <summary>
    /// Executes an actual plot through the AutoCAD plotting pipeline
    /// (PlotSettingsValidator / PlotInfo / PlotInfoValidator / PlotEngine),
    /// using the values captured by <see cref="PlotDialogForm"/>. This is
    /// the same pipeline the native PLOT command drives - the dialog is a
    /// UI in front of it, not a separate printing mechanism.
    /// </summary>
    public static class PlotService
    {
        private const string Source = "PlotService";

        /// <summary>
        /// Runs a single-sheet plot of <paramref name="layoutName"/> using the
        /// settings currently selected on <paramref name="form"/>.
        /// Returns true on success; check Logger for details either way.
        /// </summary>
        public static bool ExecutePlot(PlotDialogForm form, string layoutName)
        {
            Document doc = Application.DocumentManager.MdiActiveDocument;
            if (doc == null)
            {
                Logger.Error(Source, "No active document - cannot plot.");
                return false;
            }

            if (PlotFactory.ProcessPlotState != ProcessPlotState.NotPlotting)
            {
                Logger.Warn(Source, "A plot is already in progress. Try again once it finishes.");
                return false;
            }

            Database db = doc.Database;

            using (doc.LockDocument())
            using (Transaction tr = db.TransactionManager.StartTransaction())
            {
                try
                {
                    DBDictionary layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead);
                    if (!layoutDict.Contains(layoutName))
                    {
                        Logger.Error(Source, "Layout '" + layoutName + "' not found.");
                        return false;
                    }

                    Layout layout = (Layout)tr.GetObject((ObjectId)layoutDict[layoutName], OpenMode.ForRead);

                    PlotInfo plotInfo = new PlotInfo { Layout = layout.ObjectId };

                    PlotSettings plotSettings = new PlotSettings(layout.ModelType);
                    plotSettings.CopyFrom(layout);

                    PlotSettingsValidator psv = PlotSettingsValidator.Current;
                    ApplyFormSettings(psv, plotSettings, form, layout);

                    plotInfo.OverrideSettings = plotSettings;

                    PlotInfoValidator piv = new PlotInfoValidator
                    {
                        MediaMatchingPolicy = MatchingPolicy.MatchEnabled
                    };
                    piv.Validate(plotInfo);

                    string outputPath = ResolveOutputPath(form, layoutName, plotSettings);
                    if (form.PlotToFile && outputPath == null)
                    {
                        Logger.Info(Source, "Plot cancelled by user (no output file chosen).");
                        return false;
                    }

                    RunEngine(doc, plotInfo, outputPath);

                    tr.Commit();
                    Logger.Info(Source, "Plot completed for layout '" + layoutName + "'."
                        + (outputPath != null ? " Output: " + outputPath : " Sent to device."));
                    return true;
                }
                catch (Autodesk.AutoCAD.Runtime.Exception ex)
                {
                    Logger.Error(Source, "AutoCAD API error while plotting: " + ex.Message);
                    return false;
                }
                catch (Exception ex)
                {
                    Logger.Error(Source, "Unexpected error while plotting: " + ex.Message);
                    return false;
                }
            }
        }

        /// <summary>
        /// "Apply to Layout" in the native dialog writes the current dialog
        /// settings onto the layout's own PlotSettings and does nothing
        /// else - no plot, no dialog close. This reuses the exact same
        /// <see cref="ApplyFormSettings"/> logic <see cref="ExecutePlot"/>
        /// already uses, just persisting the result back onto the layout
        /// instead of handing it to PlotInfo/PlotEngine.
        /// </summary>
        public static bool ApplyToLayout(PlotDialogForm form, string layoutName, out string error)
        {
            error = null;
            Document doc = Application.DocumentManager.MdiActiveDocument;
            if (doc == null)
            {
                error = "No active document - cannot apply settings to a layout.";
                Logger.Error(Source, error);
                return false;
            }

            Database db = doc.Database;
            using (doc.LockDocument())
            using (Transaction tr = db.TransactionManager.StartTransaction())
            {
                try
                {
                    DBDictionary layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead);
                    if (!layoutDict.Contains(layoutName))
                    {
                        error = "Layout '" + layoutName + "' not found.";
                        Logger.Error(Source, error);
                        return false;
                    }

                    Layout layout = (Layout)tr.GetObject((ObjectId)layoutDict[layoutName], OpenMode.ForWrite);

                    PlotSettings plotSettings = new PlotSettings(layout.ModelType);
                    plotSettings.CopyFrom(layout);

                    PlotSettingsValidator psv = PlotSettingsValidator.Current;
                    ApplyFormSettings(psv, plotSettings, form, layout);

                    layout.CopyFrom(plotSettings);

                    tr.Commit();
                    Logger.Info(Source, "Plot settings applied to layout '" + layoutName + "' (not plotted).");
                    return true;
                }
                catch (Autodesk.AutoCAD.Runtime.Exception ex)
                {
                    // ErrorStatus is the actual diagnostic here (e.g. eLockViolation
                    // if a previous queued command, like a still-settling PREVIEW,
                    // hasn't released the document yet) - surfacing it directly
                    // instead of only "see the Log Viewer" so the real cause is
                    // visible on the first failure, not the third guess.
                    error = "AutoCAD API error (" + ex.ErrorStatus + "): " + ex.Message;
                    Logger.Error(Source, "AutoCAD API error while applying settings to layout: " + ex.Message + " [" + ex.ErrorStatus + "]");
                    return false;
                }
                catch (Exception ex)
                {
                    error = "Unexpected error: " + ex.Message;
                    Logger.Error(Source, "Unexpected error while applying settings to layout: " + ex.Message);
                    return false;
                }
            }
        }

        private static void ApplyFormSettings(PlotSettingsValidator psv, PlotSettings ps, PlotDialogForm form, Layout layout)
        {
            // ---- Device -----------------------------------------------------
            string deviceName = form.SelectedPrinterName;
            if (!string.IsNullOrEmpty(deviceName))
            {
                psv.SetPlotConfigurationName(ps, deviceName, null);
                Logger.Debug(Source, "PlotSettingsValidator.SetPlotConfigurationName(\"" + deviceName + "\")");
            }
            psv.RefreshLists(ps);

            // ---- Paper size --------------------------------------------------
            string mediaName = FindClosestMediaName(psv, ps, form.PaperWidthMm, form.PaperHeightMm);
            if (mediaName != null)
            {
                psv.SetCanonicalMediaName(ps, mediaName);
                Logger.Debug(Source, "PlotSettingsValidator.SetCanonicalMediaName(\"" + mediaName + "\")");
            }

            // ---- Plot area / window ------------------------------------------
            string whatToPlot = form.WhatToPlot;
            if (string.Equals(whatToPlot, "Window", StringComparison.OrdinalIgnoreCase) && form.HasWindowSelection)
            {
                psv.SetPlotType(ps, Autodesk.AutoCAD.DatabaseServices.PlotType.Window);
                psv.SetPlotWindowArea(ps, new Extents2d(
                    form.WindowMin.Value.X, form.WindowMin.Value.Y,
                    form.WindowMax.Value.X, form.WindowMax.Value.Y));
                Logger.Debug(Source, "PlotSettingsValidator.SetPlotWindowArea(" + form.WindowMin + " , " + form.WindowMax + ")");
            }
            else if (string.Equals(whatToPlot, "Extents", StringComparison.OrdinalIgnoreCase))
            {
                psv.SetPlotType(ps, Autodesk.AutoCAD.DatabaseServices.PlotType.Extents);
            }
            else if (string.Equals(whatToPlot, "Display", StringComparison.OrdinalIgnoreCase))
            {
                psv.SetPlotType(ps, Autodesk.AutoCAD.DatabaseServices.PlotType.Display);
            }
            else
            {
                psv.SetPlotType(ps, Autodesk.AutoCAD.DatabaseServices.PlotType.Layout);
            }

            // ---- Scale --------------------------------------------------------
            if (form.FitToPaper)
            {
                psv.SetUseStandardScale(ps, true);
                psv.SetStdScaleType(ps, StdScaleType.ScaleToFit);
                Logger.Debug(Source, "PlotSettingsValidator.SetStdScaleType(ScaleToFit)");
            }
            else if (form.ScaleNumerator > 0 && form.ScaleDenominator > 0)
            {
                psv.SetUseStandardScale(ps, false);
                psv.SetCustomPrintScale(ps, new CustomScale(form.ScaleNumerator, form.ScaleDenominator));
                Logger.Debug(Source, string.Format(
                    "PlotSettingsValidator.SetCustomPrintScale({0}:{1})", form.ScaleNumerator, form.ScaleDenominator));
            }

            // ---- Offset / centering --------------------------------------------
            if (form.CenterPlot)
            {
                // AutoCAD has no direct "center" setter; approximate by leaving
                // offset at (0,0) and relying on the ScaleToFit/media match, or
                // compute the centering offset yourself against the paper size
                // if you need exact native-dialog centering behaviour.
                psv.SetPlotOrigin(ps, new Point2d(0, 0));
            }
            else
            {
                psv.SetPlotOrigin(ps, new Point2d(form.OffsetX, form.OffsetY));
            }

            // ---- Orientation ----------------------------------------------------
            // PlotRotation is relative to the *media's own natural shape*, not an
            // absolute "portrait vs landscape" flag. For media that's naturally
            // already wider than it is tall (e.g. this project's custom
            // UserDefinedMetric 1350x420mm sheet), Degrees000 already looks
            // landscape and Degrees090 rotates it INTO a portrait-looking result -
            // the opposite of what you'd want for standard portrait-natured media
            // like A-size/ISO paper. Blindly forcing Degrees090 whenever "Landscape"
            // was checked (the previous code) only worked for naturally-portrait
            // media, and produced exactly the reported bug (Landscape selected,
            // Portrait-looking output) for this naturally-landscape custom sheet.
            bool mediaIsNaturallyWide = form.PaperWidthMm >= form.PaperHeightMm;
            PlotRotation rotation;
            if (form.Landscape)
                rotation = mediaIsNaturallyWide ? PlotRotation.Degrees000 : PlotRotation.Degrees090;
            else
                rotation = mediaIsNaturallyWide ? PlotRotation.Degrees090 : PlotRotation.Degrees000;

            if (form.UpsideDown)
            {
                rotation = rotation == PlotRotation.Degrees000 ? PlotRotation.Degrees180
                         : rotation == PlotRotation.Degrees090 ? PlotRotation.Degrees270
                         : rotation == PlotRotation.Degrees180 ? PlotRotation.Degrees000
                         : PlotRotation.Degrees090;
            }

            psv.SetPlotRotation(ps, rotation);
            Logger.Debug(Source, "PlotSettingsValidator.SetPlotRotation(" + rotation + ") - media "
                + (mediaIsNaturallyWide ? "naturally wide" : "naturally tall")
                + ", form.Landscape=" + form.Landscape + ", form.UpsideDown=" + form.UpsideDown);

            // ---- Plot style table ------------------------------------------------
            string ctb = form.SelectedPlotStyleTable;
            if (!string.IsNullOrEmpty(ctb) && !string.Equals(ctb, "None", StringComparison.OrdinalIgnoreCase))
            {
                psv.SetCurrentStyleSheet(ps, ctb);
                Logger.Debug(Source, "PlotSettingsValidator.SetCurrentStyleSheet(\"" + ctb + "\")");
            }

            // ---- Misc plot options ------------------------------------------------
            ps.PlotHidden = form.HideOrShadedPaperspaceObjects;
            ps.PrintLineweights = form.PlotObjectLineweights;
            ps.PlotTransparency = form.PlotTransparency;
            ps.PlotPlotStyles = form.PlotWithPlotStyles;
            // Deliberately not setting a "PlotViewportBorders"-style flag here:
            // there's no corresponding checkbox in the dialog for it, and its
            // exact availability differs between PlotSettings (layout-level)
            // and Viewport (per-viewport) depending on SDK version - add it
            // explicitly against your installed API if/when you need it.
        }

        /// <summary>
        /// Picks the closest registered canonical media name to the requested
        /// physical size, since PlotSettingsValidator works off device-reported
        /// names (e.g. "ISO_full_bleed_A0_(841.00_x_1189.00_MM)") rather than
        /// raw width/height.
        /// </summary>
        private static string FindClosestMediaName(PlotSettingsValidator psv, PlotSettings ps, double widthMm, double heightMm)
        {
            if (widthMm <= 0 || heightMm <= 0)
                return null;

            var mediaNames = psv.GetCanonicalMediaNameList(ps);
            string best = null;
            double bestDelta = double.MaxValue;

            foreach (string name in mediaNames)
            {
                psv.SetCanonicalMediaName(ps, name);
                var locale = ps.PlotPaperSize; // Point2d: X = paper width, Y = paper height (in the paper's current units)
                double delta = Math.Abs(locale.X - widthMm) + Math.Abs(locale.Y - heightMm);
                if (delta < bestDelta)
                {
                    bestDelta = delta;
                    best = name;
                }
            }

            return best;
        }

        private static string ResolveOutputPath(PlotDialogForm form, string layoutName, PlotSettings ps)
        {
            if (!form.PlotToFile)
                return null;

            if (form.SaveDirectlyToProjectFolder && !string.IsNullOrEmpty(form.PdfOutputFolder))
            {
                string autoPath = BuildNonCollidingPath(form.PdfOutputFolder, layoutName);
                Logger.Info(Source, "Saving directly to project folder (Save As dialog skipped): " + autoPath);
                return autoPath;
            }

            using (var sfd = new System.Windows.Forms.SaveFileDialog
            {
                Title = "Specify PDF File Name",
                Filter = "PDF Files (*.pdf)|*.pdf|All Files (*.*)|*.*",
                InitialDirectory = !string.IsNullOrEmpty(form.PdfOutputFolder) ? form.PdfOutputFolder : null,
                FileName = layoutName + ".pdf"
            })
            {
                return sfd.ShowDialog() == System.Windows.Forms.DialogResult.OK ? sfd.FileName : null;
            }
        }

        /// <summary>Appends " (2)", " (3)", ... to the filename if layoutName.pdf already exists in folder, so a direct save never silently overwrites a previous plot.</summary>
        private static string BuildNonCollidingPath(string folder, string layoutName)
        {
            string candidate = System.IO.Path.Combine(folder, layoutName + ".pdf");
            int n = 2;
            while (File.Exists(candidate))
            {
                candidate = System.IO.Path.Combine(folder, layoutName + " (" + n + ").pdf");
                n++;
            }
            return candidate;
        }

        /// <summary>
        /// Samples real drawing geometry so the "Preview..." popup and the
        /// small thumbnail show actual line-work instead of an empty sheet
        /// of paper (previously <see cref="PlotDialogForm.SetPreviewGeometry"/>
        /// existed but nothing ever called it, so both previews were always
        /// blank - the bug reported as "preview is not working").
        ///
        /// For the Model layout, entities are sampled directly in WCS. For a
        /// paper-space layout, entities placed directly on the sheet (title
        /// block, borders, viewport frames) are sampled as-is, and for each
        /// enabled Viewport a best-effort transform of its model-space
        /// content is added too.
        ///
        /// Honesty note: the viewport model-space transform below only
        /// handles the common top-down/plan-view case (translate + uniform
        /// scale by <see cref="Autodesk.AutoCAD.DatabaseServices.Viewport.CustomScale"/>,
        /// plus twist). Viewports looking down a 3D view direction, or with
        /// perspective on, will come out visually wrong - this is a
        /// preview approximation for "does the sheet look about right",
        /// not the real plot engine, and every viewport is wrapped in its
        /// own try/catch so one bad viewport can't blank the rest.
        /// </summary>
        public static List<List<AcPointF>> CaptureLayoutGeometry(Document doc, string layoutName, int maxEntities = 6000)
        {
            var result = new List<List<AcPointF>>();
            if (doc == null) return result;

            Database db = doc.Database;
            try
            {
                using (Transaction tr = db.TransactionManager.StartTransaction())
                {
                    DBDictionary layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead);
                    if (!layoutDict.Contains(layoutName))
                    {
                        tr.Commit();
                        return result;
                    }

                    Layout layout = (Layout)tr.GetObject((ObjectId)layoutDict[layoutName], OpenMode.ForRead);
                    BlockTableRecord btr = (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForRead);
                    bool isModelLayout = layout.LayoutName.Equals("Model", StringComparison.OrdinalIgnoreCase);

                    BlockTableRecord modelSpace = null;
                    if (!isModelLayout)
                    {
                        BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
                        modelSpace = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);
                    }

                    foreach (ObjectId id in btr)
                    {
                        if (result.Count >= maxEntities) break;

                        Entity ent;
                        try { ent = tr.GetObject(id, OpenMode.ForRead) as Entity; }
                        catch { continue; }
                        if (ent == null || !ent.Visible) continue;

                        var viewport = ent as Autodesk.AutoCAD.DatabaseServices.Viewport;
                        if (!isModelLayout && viewport != null && viewport.Number > 1)
                        {
                            SampleViewportFrame(viewport, result);
                            if (viewport.On && modelSpace != null)
                            {
                                try { SampleViewportContent(tr, viewport, modelSpace, result, maxEntities); }
                                catch (Exception ex)
                                {
                                    Logger.Warn(Source, "Could not sample viewport #" + viewport.Number + " content for preview: " + ex.Message);
                                }
                            }
                            continue;
                        }

                        SampleEntity(tr, ent, result);
                    }

                    tr.Commit();
                }
            }
            catch (Exception ex)
            {
                Logger.Warn(Source, "Preview geometry capture failed (showing an empty sheet instead): " + ex.Message);
            }

            return result;
        }

        private static void SampleViewportFrame(Autodesk.AutoCAD.DatabaseServices.Viewport vp, List<List<AcPointF>> result)
        {
            double w = vp.Width, h = vp.Height;
            Point3d c = vp.CenterPoint;
            result.Add(new List<AcPointF>
            {
                new AcPointF((float)(c.X - w / 2), (float)(c.Y - h / 2)),
                new AcPointF((float)(c.X + w / 2), (float)(c.Y - h / 2)),
                new AcPointF((float)(c.X + w / 2), (float)(c.Y + h / 2)),
                new AcPointF((float)(c.X - w / 2), (float)(c.Y + h / 2)),
                new AcPointF((float)(c.X - w / 2), (float)(c.Y - h / 2)),
            });
        }

        private static void SampleViewportContent(Transaction tr, Autodesk.AutoCAD.DatabaseServices.Viewport vp,
            BlockTableRecord modelSpace, List<List<AcPointF>> result, int maxEntities)
        {
            double scale = vp.CustomScale > 0 ? vp.CustomScale : 1.0;
            Point2d viewCenter = vp.ViewCenter;     // WCS X/Y at the middle of what the viewport shows
            Point3d vpCenter = vp.CenterPoint;      // paper-space location of that same point
            double twist = vp.TwistAngle;
            double cosT = Math.Cos(-twist), sinT = Math.Sin(-twist);

            AcPointF ToPaper(Point3d wcs)
            {
                double dx = (wcs.X - viewCenter.X) * scale;
                double dy = (wcs.Y - viewCenter.Y) * scale;
                double px = vpCenter.X + dx * cosT - dy * sinT;
                double py = vpCenter.Y + dx * sinT + dy * cosT;
                return new AcPointF((float)px, (float)py);
            }

            foreach (ObjectId id in modelSpace)
            {
                if (result.Count >= maxEntities) break;
                Entity ent;
                try { ent = tr.GetObject(id, OpenMode.ForRead) as Entity; }
                catch { continue; }
                if (ent == null || !ent.Visible) continue;

                var raw = new List<List<AcPointF>>();
                SampleEntity(tr, ent, raw);
                foreach (var line in raw)
                    result.Add(line.Select(p => ToPaper(new Point3d(p.X, p.Y, 0))).ToList());
            }
        }

        /// <summary>
        /// Turns one entity into zero or more world-space polylines using
        /// the exact "Curve.GetPointAtParameter / Polyline.GetPoint(2/3)dAt"
        /// sampling approach already documented as the intended pattern in
        /// PreviewControl's own header comment - this just actually wires
        /// it up. Anything not explicitly handled falls back to its
        /// bounding-box rectangle so it still shows up as *something*
        /// rather than being silently dropped.
        /// </summary>
        private static void SampleEntity(Transaction tr, Entity ent, List<List<AcPointF>> result)
        {
            try
            {
                switch (ent)
                {
                    case Line line:
                        result.Add(new List<AcPointF> { ToPointF(line.StartPoint), ToPointF(line.EndPoint) });
                        return;

                    case Autodesk.AutoCAD.DatabaseServices.Polyline poly:
                        {
                            var pts = new List<AcPointF>();
                            for (int i = 0; i < poly.NumberOfVertices; i++)
                                pts.Add(ToPointF(poly.GetPoint3dAt(i)));
                            if (poly.Closed && pts.Count > 0) pts.Add(pts[0]);
                            result.Add(pts);
                            return;
                        }

                    case Polyline2d poly2d:
                        {
                            var pts = new List<AcPointF>();
                            foreach (ObjectId vId in poly2d)
                            {
                                if (tr.GetObject(vId, OpenMode.ForRead) is Vertex2d vtx)
                                    pts.Add(ToPointF(vtx.Position));
                            }
                            result.Add(pts);
                            return;
                        }

                    case Circle circle:
                        result.Add(SampleCurve(circle, 0, 2 * Math.PI, 48));
                        return;

                    case Arc arc:
                        result.Add(SampleCurve(arc, arc.StartAngle, arc.EndAngle, 32));
                        return;

                    case Curve curve:
                        try
                        {
                            result.Add(SampleCurveByParam(curve, 32));
                            return;
                        }
                        catch { /* fall through to bbox */ }
                        break;
                }

                // Fallback for everything else (text, blocks, dimensions, hatches, ...):
                // draw the bounding box so the entity is at least represented.
                Extents3d ext = ent.GeometricExtents;
                result.Add(new List<AcPointF>
                {
                    new AcPointF((float)ext.MinPoint.X, (float)ext.MinPoint.Y),
                    new AcPointF((float)ext.MaxPoint.X, (float)ext.MinPoint.Y),
                    new AcPointF((float)ext.MaxPoint.X, (float)ext.MaxPoint.Y),
                    new AcPointF((float)ext.MinPoint.X, (float)ext.MaxPoint.Y),
                    new AcPointF((float)ext.MinPoint.X, (float)ext.MinPoint.Y),
                });
            }
            catch
            {
                // No extents (e.g. empty/degenerate entity) - just skip it,
                // never let one bad entity blank the whole preview.
            }
        }

        private static List<AcPointF> SampleCurve(Curve curve, double startAngle, double endAngle, int steps)
        {
            var pts = new List<AcPointF>();
            if (endAngle < startAngle) endAngle += 2 * Math.PI;
            for (int i = 0; i <= steps; i++)
            {
                double ang = startAngle + (endAngle - startAngle) * i / steps;
                try { pts.Add(ToPointF(curve.GetPointAtParameter(ang))); }
                catch { /* skip a bad sample point rather than aborting the whole curve */ }
            }
            return pts;
        }

        private static List<AcPointF> SampleCurveByParam(Curve curve, int steps)
        {
            var pts = new List<AcPointF>();
            double startParam = curve.StartParam;
            double endParam = curve.EndParam;
            for (int i = 0; i <= steps; i++)
            {
                double p = startParam + (endParam - startParam) * i / steps;
                pts.Add(ToPointF(curve.GetPointAtParameter(p)));
            }
            return pts;
        }

        private static AcPointF ToPointF(Point3d p) => new AcPointF((float)p.X, (float)p.Y);

        private static void RunEngine(Document doc, PlotInfo plotInfo, string outputPath)
        {
            using (PlotEngine engine = PlotFactory.CreatePublishEngine())
            {
                engine.BeginPlot(null, null);

                // Note: per the standard Autodesk plotting sample pattern, the
                // 5th argument here is passed as a fixed "true" regardless of
                // whether the target is a file (PDF) or a physical/virtual
                // device - it is not a preview-vs-real-plot switch in practice.
                engine.BeginDocument(
                    plotInfo,
                    doc.Name,
                    null,
                    1,
                    true,
                    outputPath);

                PlotPageInfo pageInfo = new PlotPageInfo();
                engine.BeginPage(pageInfo, plotInfo, true, null);
                engine.BeginGenerateGraphics(null);
                engine.EndGenerateGraphics(null);
                engine.EndPage(null);

                engine.EndDocument(null);
                engine.EndPlot(null);
            }
        }
    }
}


// ===== FILE: Cad\ViewportHighlighter.cs =====
using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using PlotExtra.Logging;

namespace PlotExtra.Cad
{
    /// <summary>
    /// Highlights an entity in the current AutoCAD viewport by setting it as
    /// the implied (grip) selection and forcing a screen refresh.
    ///
    /// Reuse pattern for another project - wire this to a grid's selection:
    ///   dataGridView1.SelectionChanged += (s, e) =>
    ///   {
    ///       if (dataGridView1.CurrentRow?.Tag is ObjectId id)
    ///           ViewportHighlighter.Highlight(id);
    ///   };
    ///
    /// This must run on the AutoCAD main (document) thread. It does so
    /// naturally as long as it is invoked from a WinForms event handler on a
    /// modeless dialog shown via Application.ShowModelessDialog - no explicit
    /// marshalling is required in that case.
    /// </summary>
    public static class ViewportHighlighter
    {
        public static void Highlight(ObjectId objectId)
        {
            if (objectId.IsNull || objectId.IsErased)
                return;

            Document doc = Application.DocumentManager.MdiActiveDocument;
            if (doc == null)
                return;

            try
            {
                doc.Editor.SetImpliedSelection(new[] { objectId });
                doc.Editor.UpdateScreen();
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                Logger.Warn("ViewportHighlighter", "Could not highlight entity: " + ex.Message);
            }
        }

        public static void HighlightMultiple(ObjectId[] objectIds)
        {
            Document doc = Application.DocumentManager.MdiActiveDocument;
            if (doc == null || objectIds == null || objectIds.Length == 0)
                return;

            try
            {
                doc.Editor.SetImpliedSelection(objectIds);
                doc.Editor.UpdateScreen();
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                Logger.Warn("ViewportHighlighter", "Could not highlight entities: " + ex.Message);
            }
        }

        public static void ClearHighlight()
        {
            Document doc = Application.DocumentManager.MdiActiveDocument;
            if (doc == null)
                return;

            try
            {
                doc.Editor.SetImpliedSelection(Array.Empty<ObjectId>());
                doc.Editor.UpdateScreen();
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                Logger.Warn("ViewportHighlighter", "Could not clear highlight: " + ex.Message);
            }
        }
    }
}


// ===== FILE: Commands.cs =====
using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using PlotExtra.Cad;
using PlotExtra.Logging;
using PlotExtra.UI;
// Both Autodesk.AutoCAD.ApplicationServices and System.Windows.Forms declare
// a type named "Application" - alias the AutoCAD one so every reference
// below is unambiguous without having to drop the System.Windows.Forms using.
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: ExtensionApplication(typeof(PlotExtra.PlotExtraApplication))]
[assembly: CommandClass(typeof(PlotExtra.Commands))]

namespace PlotExtra
{
    /// <summary>
    /// Runs once per NETLOAD. Prints a version banner to the AutoCAD command
    /// line in the form:
    ///   1.0.001_09.07.2026_14.32.07
    /// where "001" is BuildInfo.BuildNumber (from BQ_Build in the .csproj)
    /// and the timestamp is the DLL's own last-write time, i.e. effectively
    /// its build/creation time - so every NETLOAD makes it obvious exactly
    /// which build is active, which matters when several ProjectName_0NN
    /// test builds are loaded side by side in the same session.
    /// </summary>
    public class PlotExtraApplication : IExtensionApplication
    {
        public void Initialize()
        {
            Document doc = AcadApp.DocumentManager.MdiActiveDocument;
            Editor ed = doc?.Editor;

            string version = BuildVersionString();
            string message = string.Format(
                "\nPlotExtra {0} loaded. Command: {1}",
                version, BuildInfo.CommandName);

            ed?.WriteMessage(message);
            Logger.Info("PlotExtraApplication", message.Trim());
        }

        public void Terminate()
        {
            Logger.Info("PlotExtraApplication", "Unloading PlotExtra " + BuildInfo.AssemblyBaseName + ".");
        }

        private static string BuildVersionString()
        {
            Assembly asm = Assembly.GetExecutingAssembly();
            Version v = asm.GetName().Version; // 1.0.1.0 -> major.minor used, build number comes from BuildInfo

            DateTime created;
            try
            {
                created = File.GetLastWriteTime(asm.Location);
            }
            catch
            {
                created = DateTime.Now;
            }

            return string.Format(
                "{0}.{1}.{2}_{3:dd.MM.yyyy}_{3:HH.mm.ss}",
                v.Major, v.Minor, BuildInfo.BuildNumber, created);
        }
    }

    public class Commands
    {
        private static LogViewerForm _logViewer;

        /// <summary>
        /// Main entry point. Command name is derived entirely from BQ_Build
        /// in the .csproj via the generated BuildInfo.CommandName constant -
        /// nothing here needs to change when the build number is bumped.
        /// </summary>
        [CommandMethod(BuildInfo.CommandName)]
        public void ShowPlotExtraDialog()
        {
            Document doc = AcadApp.DocumentManager.MdiActiveDocument;
            if (doc == null)
                return;

            string layoutName = Autodesk.AutoCAD.DatabaseServices.LayoutManager.Current.CurrentLayout;

            Logger.Info(BuildInfo.CommandName, "Command invoked on layout '" + layoutName + "'.");

            using (var form = new PlotDialogForm(layoutName))
            {
                try
                {
                    var geometry = PlotService.CaptureLayoutGeometry(doc, layoutName);
                    form.SetPreviewGeometry(geometry);
                }
                catch (System.Exception ex)
                {
                    Logger.Warn(BuildInfo.CommandName, "Could not capture preview geometry (preview will show an empty sheet): " + ex.Message);
                }

                var result = AcadApp.ShowModalDialog(form);
                Logger.Info(BuildInfo.CommandName, "Dialog closed with result: " + result);

                if (result == DialogResult.OK)
                {
                    bool ok = PlotService.ExecutePlot(form, layoutName);
                    Logger.Info(BuildInfo.CommandName, ok
                        ? "Plot finished successfully."
                        : "Plot did not complete - see preceding log entries.");
                }
            }
        }

        /// <summary>
        /// Opens (or brings to front) the modeless log window, demonstrating
        /// the SizableToolWindow / Escape-swallow reusable pattern.
        /// </summary>
        [CommandMethod("PLOTEXTRALOG")]
        public void ShowLogViewer()
        {
            if (_logViewer == null || _logViewer.IsDisposed)
            {
                _logViewer = new LogViewerForm();
                AcadApp.ShowModelessDialog(_logViewer);
            }
            else
            {
                _logViewer.Activate();
            }
        }
    }
}


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

[assembly: AssemblyTitle("PlotExtra")]
[assembly: AssemblyDescription("Custom Plot dialog replica for AutoCAD Civil3D")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("DB E.C.O. Group")]
[assembly: AssemblyProduct("PlotExtra")]
[assembly: AssemblyCopyright("Copyright 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("a1b2c3d4-e5f6-4a1b-9c2d-000000000001")]

// Major.Minor.Build.Revision
// Build (3rd part) is bumped manually here to stay in sync with BQ_Build in the .csproj.
[assembly: AssemblyVersion("1.0.18.0")]
[assembly: AssemblyFileVersion("1.0.18.0")]

Additional info:

Share this page:

Page Tag: 🏷️ PLOTEXTRA018

  • PlotExtra

    Short description🖨️ PlotExtra is an AutoCAD and Civil 3D .NET plotting interface that recreates and extends the plot dialog workflow with a styled WinForms UI. 📐 It includes layout preview, page setup controls, PDF project management, logging, and reusable C3D themed controls. Command:🚀 Main command: PLOTEXTRA018 🧾 Log command: PLOTEXTRALOG 🧰 Opens a Civil 3D…