ExtendStartEndMultiple

Short description

🧩 ExtendStartEndMultiple extends the start and end of multiple selected LINE ARC LWPOLYLINE and POLYLINE objects using independent extension values.
🎛️ The DCL interface provides separate Start and End values together with Apply Undo OK and Cancel.
↩️ Apply can be tested repeatedly and Undo reverses the most recent Apply while Cancel restores the geometry changed during the current run.

Image 000 35

Command:

Main command: ExtendStartEndMultiple

Description:

📐 The routine is based on Lee Mac DoubleExtend logic and supports straight and curved polyline geometry.
🖱️ Source entities are selected before the DCL loop starts.
➡️ Extend Start and Extend End are independent values so each end can be lengthened by a different amount.
🧬 Lightweight polyline vertex and bulge calculations preserve supported curved end geometry.
✅ OK performs one apply when no Apply or Undo interaction has yet occurred and otherwise accepts the current state.
↩️ Cancel reverses all Apply operations created during the current command session.
🧱 The complete command is wrapped in an outer AutoCAD UNDO group.

Helper function: (if any)​

LM:dex:extendpoly calculates extended lightweight polyline endpoint geometry.
LM:dex:run applies the extension logic to the current selection.
DEX:dialog returns OK Apply Undo or Cancel state to the main loop.

Functionalities:

✅ Multiple object processing – extends a selection of supported curve entities.
✅ Independent start extension – uses a separate value for the beginning of each curve.
✅ Independent end extension – uses a separate value for the end of each curve.
✅ LINE support – extends straight lines.
✅ ARC support – extends supported arc geometry.
✅ LWPOLYLINE support – extends lightweight polyline endpoints.
✅ Legacy POLYLINE support – processes supported old polyline geometry.
✅ Apply workflow – allows iterative changes without leaving the tool.
✅ Undo last Apply – reverses the latest Apply operation.
✅ Cancel rollback – restores Apply changes from the current session.
✅ Undo grouping – groups the command modifications for AutoCAD undo.

Result:

🎯 The result is the original selected curve geometry lengthened independently at its start and end.
🔄 The command supports iterative adjustment before the user accepts the final state.

Images, animations etc.

Image 000 35
Pixel

XAML code:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:src="clr-namespace:Autodesk.Windows;assembly=AdWindows" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<src:RibbonToolTip x:Key="ExtendStartEndMultiple">
<src:RibbonToolTip.ExpandedContent>
<StackPanel>
<TextBlock Background="AntiqueWhite" TextAlignment="Left">
<Bold>Function Syntax: ExtendStartEndMultiple</Bold>
<LineBreak />
<Bold>Version: 1.0 Date: 20.06.2026 6:38:59PM</Bold>
<LineBreak />
<Hyperlink>AI+ https://www.lee-mac.com/doubleextend.html</Hyperlink>
<LineBreak />
<Bold>
<Run Foreground="DodgerBlue">Description</Run>
</Bold>
<LineBreak />
<Run Foreground="White">🧩 ExtendStartEndMultiple extends the start and end of multiple selected LINE ARC LWPOLYLINE and POLYLINE objects using independent extension values.</Run>
<LineBreak />
<Run Foreground="White">🎛️ The DCL interface provides separate Start and End values together with Apply Undo OK and Cancel.</Run>
<LineBreak />
<Run Foreground="White">↩️ Apply can be tested repeatedly and Undo reverses the most recent Apply while Cancel restores the geometry changed during the current run.</Run>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="DodgerBlue">Function Syntax</Run>
</Bold>
<LineBreak />
<Run Foreground="White">Main command: </Run>
<Bold>
<Run Foreground="Orange">ExtendStartEndMultiple</Run>
</Bold>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="DodgerBlue">SEO Optimized Overview</Run>
</Bold>
<LineBreak />
<Run Foreground="White">📐 The routine is based on Lee Mac DoubleExtend logic and supports straight and curved polyline geometry.</Run>
<LineBreak />
<Run Foreground="White">🖱️ Source entities are selected before the DCL loop starts.</Run>
<LineBreak />
<Run Foreground="White">➡️ Extend Start and Extend End are independent values so each end can be lengthened by a different amount.</Run>
<LineBreak />
<Run Foreground="White">🧬 Lightweight polyline vertex and bulge calculations preserve supported curved end geometry.</Run>
<LineBreak />
<Run Foreground="White">✅ OK performs one apply when no Apply or Undo interaction has yet occurred and otherwise accepts the current state.</Run>
<LineBreak />
<Run Foreground="White">↩️ Cancel reverses all Apply operations created during the current command session.</Run>
<LineBreak />
<Run Foreground="White">🧱 The complete command is wrapped in an outer AutoCAD UNDO group.</Run>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="LimeGreen">Primary Workflow</Run>
</Bold>
<LineBreak />
<Run Foreground="White">• Run ExtendStartEndMultiple.</Run>
<LineBreak />
<Run Foreground="White">• Select LINE ARC LWPOLYLINE or POLYLINE objects.</Run>
<LineBreak />
<Run Foreground="White">• Enter independent Extend Start and Extend End values.</Run>
<LineBreak />
<Run Foreground="White">• Use Apply to modify the selected geometry while keeping the dialog workflow active.</Run>
<LineBreak />
<Run Foreground="White">• Use Undo to reverse the latest Apply.</Run>
<LineBreak />
<Run Foreground="White">• Use OK to keep the current result or Cancel to restore the changes made during this run.</Run>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="LimeGreen">Functionalities</Run>
</Bold>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Multiple object processing</Run>
</Bold>
<Run Foreground="White"> - extends a selection of supported curve entities.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Independent start extension</Run>
</Bold>
<Run Foreground="White"> - uses a separate value for the beginning of each curve.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Independent end extension</Run>
</Bold>
<Run Foreground="White"> - uses a separate value for the end of each curve.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">LINE support</Run>
</Bold>
<Run Foreground="White"> - extends straight lines.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">ARC support</Run>
</Bold>
<Run Foreground="White"> - extends supported arc geometry.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">LWPOLYLINE support</Run>
</Bold>
<Run Foreground="White"> - extends lightweight polyline endpoints.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Legacy POLYLINE support</Run>
</Bold>
<Run Foreground="White"> - processes supported old polyline geometry.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Apply workflow</Run>
</Bold>
<Run Foreground="White"> - allows iterative changes without leaving the tool.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Undo last Apply</Run>
</Bold>
<Run Foreground="White"> - reverses the latest Apply operation.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Cancel rollback</Run>
</Bold>
<Run Foreground="White"> - restores Apply changes from the current session.</Run>
<LineBreak />
<Run Foreground="White">✅ </Run>
<Bold>
<Run Foreground="LimeGreen">Undo grouping</Run>
</Bold>
<Run Foreground="White"> - groups the command modifications for AutoCAD undo.</Run>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="DodgerBlue">Important Commands and Aspects</Run>
</Bold>
<LineBreak />
<Run Foreground="White">🧩 LM:dex:extendpoly calculates extended lightweight polyline endpoint geometry.</Run>
<LineBreak />
<Run Foreground="White">🧩 LM:dex:run applies the extension logic to the current selection.</Run>
<LineBreak />
<Run Foreground="White">🧩 DEX:dialog returns OK Apply Undo or Cancel state to the main loop.</Run>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="DodgerBlue">Result</Run>
</Bold>
<LineBreak />
<Run Foreground="White">🎯 The result is the original selected curve geometry lengthened independently at its start and end.</Run>
<LineBreak />
<Run Foreground="White">🔄 The command supports iterative adjustment before the user accepts the final state.</Run>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="DarkRed">Important Notes</Run>
</Bold>
<LineBreak />
<Run Foreground="White">⚠️ </Run>
<Bold>
<Run Foreground="DarkRed">The file is based on original DoubleExtend logic by Lee Mac.</Run>
</Bold>
<LineBreak />
<Run Foreground="White">⚠️ </Run>
<Bold>
<Run Foreground="DarkRed">Cancel is designed to reverse Apply operations from the current command run.</Run>
</Bold>
<LineBreak />
<Run Foreground="White">⚠️ </Run>
<Bold>
<Run Foreground="DarkRed">The current selection filter accepts LINE ARC LWPOLYLINE and POLYLINE.</Run>
</Bold>
<LineBreak />
<LineBreak />
<Bold>
<Run Foreground="Yellow">SEO Keywords</Run>
</Bold>
<LineBreak />
<Run Foreground="Yellow">🏷️ AutoCAD, 🏷️ Civil 3D, 🏷️ AutoLISP, 🏷️ ExtendStartEndMultiple, 🏷️ DoubleExtend, 🏷️ Lengthen, 🏷️ Polyline extension, 🏷️ Start extension, 🏷️ End extension, 🏷️ Apply, 🏷️ Undo, 🏷️ DCL</Run>
<LineBreak />
</TextBlock>
<Grid>
<Image Source="Image_000.jpg" Stretch="Uniform" />
</Grid>
</StackPanel>
</src:RibbonToolTip.ExpandedContent>
</src:RibbonToolTip>
</ResourceDictionary>

C# code:

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

namespace DoubleExtendCS.Logging
{
    public enum LogLevel
    {
        INFO,
        WARN,
        ERROR,
        DEBUG
    }

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

        /// <summary>
        /// Formats exactly like the requested style:
        /// 05:29:43  [INFO]   [PvSplitApi]  SplitDataLines.Add(Double rawStation, Double datum)
        /// </summary>
        public override string ToString()
        {
            return $"{Timestamp:HH:mm:ss}  [{Level,-5}]  [{Source}]  {Message}";
        }
    }

    /// <summary>
    /// Central, static, thread-safe-ish logger for the whole plugin.
    /// - Keeps an in-memory buffer (capped) that the modeless form binds to.
    /// - Raises EntryAdded so any open DexForm can append live, on the UI thread.
    /// - Can persist the current buffer to disk ("store this log") so behaviour
    ///   / bug patterns can be reviewed later and used to improve the tool.
    /// </summary>
    public static class DexLogger
    {
        private const int MaxBufferedEntries = 5000;
        private static readonly object _lock = new object();
        private static readonly List<LogEntry> _entries = new List<LogEntry>();

        /// <summary>Raised on every new log entry. Subscribers (the form) must
        /// marshal to the UI thread themselves (DexForm does this via Invoke).</summary>
        public static event Action<LogEntry> EntryAdded;

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

        public static void Log(LogLevel level, string source, string message)
        {
            var entry = new LogEntry(DateTime.Now, level, source, message);
            lock (_lock)
            {
                _entries.Add(entry);
                if (_entries.Count > MaxBufferedEntries)
                    _entries.RemoveAt(0);
            }
            EntryAdded?.Invoke(entry);
        }

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

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

        /// <summary>
        /// Persists the current in-memory log buffer to disk so it can be
        /// reviewed later, attached to a bug report, or fed back in to improve
        /// the tool/prompting. Returns the full path written, or null on failure.
        /// </summary>
        public static string SaveToFile(string folder = null)
        {
            try
            {
                folder ??= Path.Combine(Path.GetTempPath(), "DoubleExtendCS", "logs");
                Directory.CreateDirectory(folder);

                string fileName = $"DEX_log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt";
                string fullPath = Path.Combine(folder, fileName);

                var sb = new StringBuilder();
                sb.AppendLine("============================================================");
                sb.AppendLine($"DoubleExtendCS log export - {DateTime.Now:dd/MM/yyyy HH:mm:ss}");
                sb.AppendLine($"Build: {VersionInfo.GetLoadBanner()}");
                sb.AppendLine("============================================================");
                foreach (var e in Entries)
                    sb.AppendLine(e.ToString());

                File.WriteAllText(fullPath, sb.ToString(), Encoding.UTF8);
                return fullPath;
            }
            catch (Exception ex)
            {
                // Do not throw from the logger itself - swallow and report INFO-level.
                System.Diagnostics.Debug.WriteLine("DexLogger.SaveToFile failed: " + ex);
                return null;
            }
        }
    }
}


// ===== FILE: Core\SelectionUtils.cs =====
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using DoubleExtendCS.Logging;

namespace DoubleExtendCS.Core
{
    public static class SelectionUtils
    {
        private const string Src = "SelectionUtils";

        /// <summary>
        /// Viewport highlight: the "one line" pattern requested -
        /// SetImpliedSelection + UpdateScreen - wired from DataGridView /
        /// selection-changed style events. MUST run on the AutoCAD main
        /// thread; this is guaranteed automatically when called from a
        /// WinForms event handler inside a modeless dialog opened via
        /// Application.ShowModelessDialog (see UI/DexForm.cs).
        /// </summary>
        public static void Highlight(ObjectIdCollection ids)
        {
            if (ids == null || ids.Count == 0) return;
            var doc = Application.DocumentManager.MdiActiveDocument;
            if (doc == null) return;

            var idArray = new ObjectId[ids.Count];
            ids.CopyTo(idArray, 0);

            doc.Editor.SetImpliedSelection(idArray);
            doc.Editor.UpdateScreen();
        }

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

            // Passing an empty array clears PICKFIRST / implied selection highlight.
            doc.Editor.SetImpliedSelection(Array.Empty<ObjectId>());
            doc.Editor.UpdateScreen();
        }

        /// <summary>Bounding-box midpoint of a selection, used as the fixed
        /// drag origin for the Both-Ends slot jig. Port of DEX:sel-midpoint,
        /// but simplified: AutoCAD entities expose GeometricExtents directly,
        /// so there is no need to hand-walk vertices per entity type like the
        /// LISP had to.</summary>
        public static Point3d Midpoint(Transaction tr, ObjectIdCollection ids)
        {
            bool any = false;
            double minX = 0, maxX = 0, minY = 0, maxY = 0;

            foreach (ObjectId id in ids)
            {
                if (id.IsErased) continue;
                Entity ent;
                try { ent = (Entity)tr.GetObject(id, OpenMode.ForRead); }
                catch { continue; }

                Extents3d ext;
                try { ext = ent.GeometricExtents; }
                catch (Autodesk.AutoCAD.Runtime.Exception)
                {
                    DexLogger.Warn(Src, $"Midpoint: could not read extents for {id}, skipped.");
                    continue;
                }

                if (!any)
                {
                    minX = ext.MinPoint.X; maxX = ext.MaxPoint.X;
                    minY = ext.MinPoint.Y; maxY = ext.MaxPoint.Y;
                    any = true;
                }
                else
                {
                    minX = Math.Min(minX, ext.MinPoint.X);
                    maxX = Math.Max(maxX, ext.MaxPoint.X);
                    minY = Math.Min(minY, ext.MinPoint.Y);
                    maxY = Math.Max(maxY, ext.MaxPoint.Y);
                }
            }

            return any
                ? new Point3d((minX + maxX) / 2.0, (minY + maxY) / 2.0, 0.0)
                : Point3d.Origin;
        }
    }
}


// ===== FILE: Core\PreviewGeometryCollector.cs =====
using System;
using System.Collections.Generic;
using System.Drawing;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using DoubleExtendCS.Logging;

namespace DoubleExtendCS.Core
{
    /// <summary>
    /// Samples AutoCAD entities into plain PointF chains that PreviewPanel can
    /// draw with zero further AutoCAD API calls. This is the only piece that
    /// touches the database; PreviewPanel itself is pure WinForms and can be
    /// reused as-is in any other project.
    /// </summary>
    public static class PreviewGeometryCollector
    {
        private const string Src = "PreviewGeometryCollector";
        private const int ArcSamples = 24;

        public static List<List<PointF>> Collect(Transaction tr, ObjectIdCollection ids)
        {
            var result = new List<List<PointF>>();
            foreach (ObjectId id in ids)
            {
                if (id.IsErased) continue;
                try
                {
                    var chain = CollectOne(tr, id);
                    if (chain != null && chain.Count >= 2)
                        result.Add(chain);
                }
                catch (Exception ex)
                {
                    DexLogger.Warn(Src, $"Could not sample {id} for preview: {ex.Message}");
                }
            }
            return result;
        }

        private static List<PointF> CollectOne(Transaction tr, ObjectId id)
        {
            var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;

            switch (ent)
            {
                case Line line:
                    return new List<PointF>
                    {
                        ToPF(line.StartPoint),
                        ToPF(line.EndPoint)
                    };

                case Arc arc:
                    return SampleCurve(arc, ArcSamples);

                case Polyline pl:
                    {
                        var pts = new List<PointF>();
                        int n = pl.NumberOfVertices;
                        for (int i = 0; i < n; i++)
                        {
                            // Straight segment -> just the vertex; bulged segment -> sample the arc.
                            Point2d p2d = pl.GetPoint2dAt(i);
                            pts.Add(new PointF((float)p2d.X, (float)p2d.Y));

                            if (i < n - 1 && Math.Abs(pl.GetBulgeAt(i)) > 1e-8)
                            {
                                // Insert a couple of intermediate points along the bulge arc
                                // using the segment's own parameter range (GetPointAtParameter).
                                double p1 = pl.GetParameterAtPoint(pl.GetPoint3dAt(i));
                                double p2 = pl.GetParameterAtPoint(pl.GetPoint3dAt(i + 1));
                                for (int s = 1; s < 6; s++)
                                {
                                    double t = p1 + (p2 - p1) * s / 6.0;
                                    Point3d wp = pl.GetPointAtParameter(t);
                                    pts.Add(ToPF(wp));
                                }
                            }
                        }
                        return pts;
                    }

                case Polyline2d pl2d:
                    return SampleCurve(pl2d, ArcSamples * 2);

                default:
                    return null;
            }
        }

        /// <summary>Generic sampler for any Curve using GetPointAtParameter across
        /// its own StartParam..EndParam - works for Arc, Polyline2d, Spline, etc.</summary>
        private static List<PointF> SampleCurve(Curve curve, int samples)
        {
            var pts = new List<PointF>();
            double sp = curve.StartParam;
            double ep = curve.EndParam;
            if (double.IsNaN(sp) || double.IsNaN(ep) || samples < 2)
                return pts;

            for (int i = 0; i <= samples; i++)
            {
                double t = sp + (ep - sp) * i / samples;
                try
                {
                    Point3d p = curve.GetPointAtParameter(t);
                    pts.Add(ToPF(p));
                }
                catch
                {
                    // Skip unsampleable parameter (e.g. degenerate segment)
                }
            }
            return pts;
        }

        private static PointF ToPF(Point3d p) => new PointF((float)p.X, (float)p.Y);

        /// <summary>
        /// Builds a "what it would look like" preview purely from a cached
        /// EntitySnapshot list + the current extS/extE values - no database
        /// access at all, so this can be called on every keystroke/slider tick
        /// in the form without any transaction overhead.
        /// </summary>
        public static List<List<PointF>> CollectExtendedPreview(IEnumerable<EntitySnapshot> snapshot, double extS, double extE)
        {
            var result = new List<List<PointF>>();
            if (snapshot == null) return result;

            foreach (var snap in snapshot)
            {
                switch (snap)
                {
                    case LineSnapshot ls:
                        {
                            Vector3d dir = ls.End - ls.Start;
                            double len = dir.Length;
                            if (len < 1e-8) break;
                            Vector3d u = dir / len;
                            Point3d newStart = ls.Start - u * extS;
                            Point3d newEnd = ls.End + u * extE;
                            result.Add(new List<PointF> { ToPF(newStart), ToPF(newEnd) });
                            break;
                        }
                    case ArcSnapshot arcSnap:
                        {
                            if (arcSnap.Radius < 1e-8) break;
                            double a1 = arcSnap.StartAngle - extS / arcSnap.Radius;
                            double a2 = arcSnap.EndAngle + extE / arcSnap.Radius;
                            var pts = new List<PointF>();
                            int samples = ArcSamples;
                            for (int i = 0; i <= samples; i++)
                            {
                                double t = a1 + (a2 - a1) * i / samples;
                                double x = arcSnap.Center.X + arcSnap.Radius * Math.Cos(t);
                                double y = arcSnap.Center.Y + arcSnap.Radius * Math.Sin(t);
                                pts.Add(new PointF((float)x, (float)y));
                            }
                            result.Add(pts);
                            break;
                        }
                    case LwPolylineSnapshot lwSnap:
                        {
                            var verts = new List<PolyVertex>(lwSnap.Points.Count);
                            for (int i = 0; i < lwSnap.Points.Count; i++)
                                verts.Add(new PolyVertex(lwSnap.Points[i], lwSnap.Bulges[i]));

                            var extended = DexGeometry.ExtendPolyline(verts, extS, extE);
                            var pts = new List<PointF>();
                            for (int i = 0; i < extended.Count; i++)
                            {
                                pts.Add(new PointF((float)extended[i].Pt.X, (float)extended[i].Pt.Y));
                                if (i < extended.Count - 1 && Math.Abs(extended[i].Bulge) > 1e-8)
                                {
                                    // Approximate the bulge arc with a few extra points for the preview
                                    var p1 = extended[i].Pt;
                                    var p2 = extended[i + 1].Pt;
                                    var cen = DexGeometry.BulgeCentre(p1, p2, extended[i].Bulge);
                                    double r = p1.GetDistanceTo(cen);
                                    double startAng = cen.GetVectorTo(p1).Angle;
                                    double sweep = 4.0 * Math.Atan(extended[i].Bulge);
                                    for (int s = 1; s < 6; s++)
                                    {
                                        double t = startAng + sweep * s / 6.0;
                                        pts.Add(new PointF((float)(cen.X + r * Math.Cos(t)), (float)(cen.Y + r * Math.Sin(t))));
                                    }
                                }
                            }
                            result.Add(pts);
                            break;
                        }
                    case Polyline2dSnapshot p2dSnap:
                        {
                            var verts = new List<PolyVertex>(p2dSnap.Positions.Count);
                            for (int i = 0; i < p2dSnap.Positions.Count; i++)
                                verts.Add(new PolyVertex(new Point2d(p2dSnap.Positions[i].X, p2dSnap.Positions[i].Y), p2dSnap.Bulges[i]));

                            var extended = DexGeometry.ExtendPolyline(verts, extS, extE);
                            var pts = extended.ConvertAll(v => new PointF((float)v.Pt.X, (float)v.Pt.Y));
                            result.Add(pts);
                            break;
                        }
                }
            }
            return result;
        }
    }
}


// ===== FILE: Core\SelectionSnapshot.cs =====
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using DoubleExtendCS.Logging;

namespace DoubleExtendCS.Core
{
    /// <summary>Geometry captured for a single entity so it can be restored later
    /// without relying on AutoCAD's command-level undo stack (which is awkward
    /// to drive from a modeless dialog / jig loop). Direct analogue of the
    /// LISP's DEX:snapshot / DEX:restore (which used raw entmod entity lists).</summary>
    public abstract class EntitySnapshot
    {
        public ObjectId Id;
        public abstract void Restore(Transaction tr);
    }

    public class LineSnapshot : EntitySnapshot
    {
        public Point3d Start, End;
        public override void Restore(Transaction tr)
        {
            if (Id.IsErased) return;
            var line = (Line)tr.GetObject(Id, OpenMode.ForWrite);
            line.StartPoint = Start;
            line.EndPoint = End;
        }
    }

    public class ArcSnapshot : EntitySnapshot
    {
        public double StartAngle, EndAngle;
        // Cached so consumers (preview panel, jig) can recompute geometry
        // purely in-memory without round-tripping through the database.
        public Point3d Center;
        public double Radius;
        public Vector3d Normal;

        public override void Restore(Transaction tr)
        {
            if (Id.IsErased) return;
            var arc = (Arc)tr.GetObject(Id, OpenMode.ForWrite);
            arc.StartAngle = StartAngle;
            arc.EndAngle = EndAngle;
        }
    }

    public class LwPolylineSnapshot : EntitySnapshot
    {
        public List<Point2d> Points = new List<Point2d>();
        public List<double> Bulges = new List<double>();
        public override void Restore(Transaction tr)
        {
            if (Id.IsErased) return;
            var pl = (Polyline)tr.GetObject(Id, OpenMode.ForWrite);
            for (int i = 0; i < Points.Count && i < pl.NumberOfVertices; i++)
            {
                pl.SetPointAt(i, Points[i]);
                pl.SetBulgeAt(i, Bulges[i]);
            }
        }
    }

    public class Polyline2dSnapshot : EntitySnapshot
    {
        public List<ObjectId> VertexIds = new List<ObjectId>();
        public List<Point3d> Positions = new List<Point3d>();
        public List<double> Bulges = new List<double>();
        public override void Restore(Transaction tr)
        {
            for (int i = 0; i < VertexIds.Count; i++)
            {
                if (VertexIds[i].IsErased) continue;
                var v2d = (Vertex2d)tr.GetObject(VertexIds[i], OpenMode.ForWrite);
                v2d.Position = Positions[i];
                v2d.Bulge = Bulges[i];
            }
        }
    }

    /// <summary>Captures / restores geometry for a whole ObjectIdCollection.</summary>
    public static class SelectionSnapshot
    {
        private const string Src = "SelectionSnapshot";

        public static List<EntitySnapshot> Capture(Transaction tr, ObjectIdCollection ids)
        {
            var result = new List<EntitySnapshot>();
            foreach (ObjectId id in ids)
            {
                if (id.IsErased) continue;
                var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
                switch (ent)
                {
                    case Line line:
                        result.Add(new LineSnapshot { Id = id, Start = line.StartPoint, End = line.EndPoint });
                        break;
                    case Arc arc:
                        result.Add(new ArcSnapshot
                        {
                            Id = id,
                            StartAngle = arc.StartAngle,
                            EndAngle = arc.EndAngle,
                            Center = arc.Center,
                            Radius = arc.Radius,
                            Normal = arc.Normal
                        });
                        break;
                    case Polyline pl:
                        {
                            var snap = new LwPolylineSnapshot { Id = id };
                            for (int i = 0; i < pl.NumberOfVertices; i++)
                            {
                                snap.Points.Add(pl.GetPoint2dAt(i));
                                snap.Bulges.Add(pl.GetBulgeAt(i));
                            }
                            result.Add(snap);
                            break;
                        }
                    case Polyline2d pl2d:
                        {
                            var snap = new Polyline2dSnapshot { Id = id };
                            foreach (ObjectId vid in pl2d)
                            {
                                var v2d = (Vertex2d)tr.GetObject(vid, OpenMode.ForRead);
                                snap.VertexIds.Add(vid);
                                snap.Positions.Add(v2d.Position);
                                snap.Bulges.Add(v2d.Bulge);
                            }
                            result.Add(snap);
                            break;
                        }
                    default:
                        DexLogger.Warn(Src, $"Capture: unsupported type '{ent?.GetType().Name}' for {id} - not snapshotted.");
                        break;
                }
            }
            return result;
        }

        public static void Restore(Transaction tr, IEnumerable<EntitySnapshot> snapshot)
        {
            if (snapshot == null) return;
            foreach (var s in snapshot)
            {
                try { s.Restore(tr); }
                catch (Exception ex) { DexLogger.Error(Src, $"Restore failed for {s.Id}: {ex.Message}"); }
            }
        }

        /// <summary>Convenience: capture inside its own transaction against the
        /// current document's database.</summary>
        public static List<EntitySnapshot> CaptureNow(ObjectIdCollection ids)
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            using (var tr = doc.Database.TransactionManager.StartTransaction())
            {
                var snap = Capture(tr, ids);
                tr.Commit();
                return snap;
            }
        }

        /// <summary>Convenience: restore + regen inside its own transaction.
        /// Locks the document first - required because this is typically
        /// invoked from a WinForms event handler on a modeless dialog, not
        /// from an active AutoCAD command context.</summary>
        public static void RestoreNow(List<EntitySnapshot> snapshot)
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            using (doc.LockDocument())
            using (var tr = doc.Database.TransactionManager.StartTransaction())
            {
                Restore(tr, snapshot);
                tr.Commit();
            }
            doc.Editor.Regen();
        }
    }
}


// ===== FILE: Core\DexGeometry.cs =====
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.Geometry;

namespace DoubleExtendCS.Core
{
    /// <summary>One 2D polyline vertex + its outgoing bulge (LWPOLYLINE style,
    /// bulge stored on the vertex it leaves from - matches DXF group 42 / AutoLISP's
    /// LM:lwvertices convention used by the original LISP).</summary>
    public struct PolyVertex
    {
        public Point2d Pt;
        public double Bulge;

        public PolyVertex(Point2d pt, double bulge)
        {
            Pt = pt;
            Bulge = bulge;
        }
    }

    /// <summary>
    /// Pure, side-effect-free geometry helpers, ported 1:1 from the original
    /// LISP (vxs, LM:bulgecentre, LM:dex:extendpoly). Kept independent from
    /// any AutoCAD DBObject/Transaction code so they're trivially unit-testable.
    /// </summary>
    public static class DexGeometry
    {
        private const double Tolerance = 1e-8;

        /// <summary>Centre of the arc defined by a bulge between p1 and p2
        /// (Lee Mac's LM:bulgecentre, ported verbatim).</summary>
        public static Point2d BulgeCentre(Point2d p1, Point2d p2, double bulge)
        {
            double ang = p1.GetVectorTo(p2).Angle;
            double dir = ang + (Math.PI / 2.0 - 2.0 * Math.Atan(bulge));
            double dist = (p1.GetDistanceTo(p2) * (1.0 + bulge * bulge)) / (4.0 * bulge);
            return PolarPoint(p1, dir, dist);
        }

        public static Point2d PolarPoint(Point2d basePt, double angle, double distance)
        {
            return new Point2d(basePt.X + distance * Math.Cos(angle),
                                basePt.Y + distance * Math.Sin(angle));
        }

        private static bool NearZero(double v) => Math.Abs(v) < Tolerance;

        /// <summary>
        /// Extends the START and END of an ordered vertex chain (LWPOLYLINE or
        /// old-style POLYLINE, expressed generically here) by extS / extE.
        /// Straight segments extend linearly; bulged (arc) segments extend
        /// along the arc. Direct port of LM:dex:extendpoly.
        /// </summary>
        public static List<PolyVertex> ExtendPolyline(List<PolyVertex> verts, double extS, double extE)
        {
            if (verts == null || verts.Count < 2)
                return verts;

            var lst = new List<PolyVertex>(verts);

            // ---------- Start ----------
            {
                Point2d pt1 = lst[0].Pt;
                Point2d pt2 = lst[1].Pt;
                double bul = lst[0].Bulge;
                double dis = pt1.GetDistanceTo(pt2);

                if (NearZero(bul))
                {
                    if (!NearZero(dis))
                    {
                        Vector2d u = (pt2 - pt1) / dis; // unit vector pt1->pt2
                        Point2d newPt = pt1 - u * extS;
                        lst[0] = new PolyVertex(newPt, lst[0].Bulge);
                    }
                }
                else
                {
                    Point2d cen = BulgeCentre(pt1, pt2, bul);
                    double rad = (dis * (1.0 + bul * bul)) / (4.0 * Math.Abs(bul));
                    double len = Math.Abs(4.0 * Math.Atan(bul) * rad);

                    if (len + extS < rad * 2.0 * Math.PI)
                    {
                        double baseAng = cen.GetVectorTo(pt1).Angle;
                        double sign = bul < 0 ? -1.0 : 1.0;
                        double newAng = baseAng + sign * (extS / rad);
                        Point2d pt3 = PolarPoint(cen, newAng, rad);

                        // Matches the LISP:  ang = (if (minusp b) - +) (atan b) (extS/rad/4)
                        double atanB = Math.Atan(bul);
                        double angDelta = extS / rad / 4.0;
                        double newHalfAng = bul < 0 ? (atanB - angDelta) : (atanB + angDelta);
                        double newBulge = Math.Tan(newHalfAng);

                        lst[0] = new PolyVertex(pt3, newBulge);
                    }
                }
            }

            // ---------- End ----------
            {
                int last = lst.Count - 1;
                int prev = last - 1;
                Point2d pt1 = lst[prev].Pt; // "pt2" in LISP after reverse == second-to-last original
                Point2d pt2 = lst[last].Pt;
                double bul = lst[prev].Bulge; // bulge leaving the second-to-last vertex
                double dis = pt1.GetDistanceTo(pt2);

                if (NearZero(bul))
                {
                    if (!NearZero(dis))
                    {
                        Vector2d u = (pt1 - pt2) / dis; // unit vector pt2->pt1 (i.e. direction "outward" from pt1 reversed)
                        Point2d newPt = pt1 + u * extE;
                        lst[last] = new PolyVertex(newPt, lst[last].Bulge);
                    }
                }
                else
                {
                    Point2d cen = BulgeCentre(pt2, pt1, bul);
                    double rad = (dis * (1.0 + bul * bul)) / (4.0 * Math.Abs(bul));
                    double len = Math.Abs(4.0 * Math.Atan(bul) * rad);

                    if (len + extE < rad * 2.0 * Math.PI)
                    {
                        double baseAng = cen.GetVectorTo(pt1).Angle;
                        double sign = bul < 0 ? -1.0 : 1.0;
                        double newAng = baseAng - sign * (extE / rad);
                        Point2d pt3 = PolarPoint(cen, newAng, rad);

                        // Matches the LISP:  ang = (if (minusp b) - +) (atan b) (extE/rad/4)
                        double atanB = Math.Atan(bul);
                        double angDelta = extE / rad / 4.0;
                        double newHalfAng = bul < 0 ? (atanB - angDelta) : (atanB + angDelta);
                        double newBulge = Math.Tan(newHalfAng);

                        lst[last] = new PolyVertex(pt3, lst[last].Bulge);
                        lst[prev] = new PolyVertex(lst[prev].Pt, newBulge);
                    }
                }
            }

            return lst;
        }
    }
}


// ===== FILE: Core\EntityExtender.cs =====
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using DoubleExtendCS.Logging;

namespace DoubleExtendCS.Core
{
    /// <summary>
    /// Applies the "extend start / extend end" operation to a selection of
    /// entities. Supports Line, Arc, Polyline (LWPOLYLINE) and Polyline2d
    /// (old-style POLYLINE) - the same entity set the original LISP handled.
    /// Direct port of LM:dex:run / LM:dex:extendpoly.
    /// </summary>
    public static class EntityExtender
    {
        private const string Src = "EntityExtender";

        public static void RunAll(Transaction tr, ObjectIdCollection ids, double extS, double extE)
        {
            foreach (ObjectId id in ids)
            {
                try
                {
                    RunOne(tr, id, extS, extE);
                }
                catch (Exception ex)
                {
                    DexLogger.Error(Src, $"RunOne failed for {id}: {ex.Message}");
                }
            }
        }

        public static void RunOne(Transaction tr, ObjectId id, double extS, double extE)
        {
            if (id.IsNull || id.IsErased) return;

            var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
            switch (ent)
            {
                case Line line:
                    ExtendLine(tr, line, extS, extE);
                    break;
                case Arc arc:
                    ExtendArc(tr, arc, extS, extE);
                    break;
                case Polyline pl: // LWPOLYLINE
                    ExtendLwPolyline(tr, pl, extS, extE);
                    break;
                case Polyline2d pl2d: // old-style POLYLINE
                    ExtendPolyline2d(tr, pl2d, extS, extE);
                    break;
                default:
                    DexLogger.Warn(Src, $"Unsupported entity type '{ent?.GetType().Name}' - skipped ({id}).");
                    break;
            }
        }

        private static void ExtendLine(Transaction tr, Line line, double extS, double extE)
        {
            Point3d p1 = line.StartPoint;
            Point3d p2 = line.EndPoint;
            double dis = p1.DistanceTo(p2);
            if (dis < 1e-8)
            {
                DexLogger.Warn(Src, $"Line {line.ObjectId} has zero length - skipped.");
                return;
            }

            Vector3d u = (p2 - p1) / dis; // unit vector start->end
            var w = (Line)tr.GetObject(line.ObjectId, OpenMode.ForWrite);
            w.StartPoint = p1 - u * extS;
            w.EndPoint = p2 + u * extE;

            DexLogger.Info(Src, $"Line {line.ObjectId} extended (start={extS:0.###}, end={extE:0.###}).");
        }

        private static void ExtendArc(Transaction tr, Arc arc, double extS, double extE)
        {
            double rad = arc.Radius;
            double an1 = arc.StartAngle;
            double an2 = arc.EndAngle;
            double sweep = (an2 - an1 + Math.PI + Math.PI) % (Math.PI + Math.PI);
            double dis = rad * sweep;

            if (dis + extS + extE >= 2.0 * rad * Math.PI)
            {
                DexLogger.Warn(Src, $"Arc {arc.ObjectId} extension would exceed full circle - skipped.");
                return;
            }

            var w = (Arc)tr.GetObject(arc.ObjectId, OpenMode.ForWrite);
            w.StartAngle = an1 - extS / rad;
            w.EndAngle = an2 + extE / rad;

            DexLogger.Info(Src, $"Arc {arc.ObjectId} extended (start={extS:0.###}, end={extE:0.###}).");
        }

        private static void ExtendLwPolyline(Transaction tr, Polyline pl, double extS, double extE)
        {
            int n = pl.NumberOfVertices;
            if (n < 2)
            {
                DexLogger.Warn(Src, $"Polyline {pl.ObjectId} has < 2 vertices - skipped.");
                return;
            }
            if (pl.Closed)
            {
                DexLogger.Warn(Src, $"Polyline {pl.ObjectId} is closed - start/end extension does not apply, skipped.");
                return;
            }

            var verts = new List<PolyVertex>(n);
            for (int i = 0; i < n; i++)
                verts.Add(new PolyVertex(pl.GetPoint2dAt(i), pl.GetBulgeAt(i)));

            var extended = DexGeometry.ExtendPolyline(verts, extS, extE);

            var w = (Polyline)tr.GetObject(pl.ObjectId, OpenMode.ForWrite);
            for (int i = 0; i < n; i++)
            {
                w.SetPointAt(i, extended[i].Pt);
                w.SetBulgeAt(i, extended[i].Bulge);
            }

            DexLogger.Info(Src, $"LWPolyline {pl.ObjectId} extended (start={extS:0.###}, end={extE:0.###}).");
        }

        private static void ExtendPolyline2d(Transaction tr, Polyline2d pl2d, double extS, double extE)
        {
            if (pl2d.Closed)
            {
                DexLogger.Warn(Src, $"Polyline2d {pl2d.ObjectId} is closed - skipped.");
                return;
            }

            var vertexIds = new List<ObjectId>();
            foreach (ObjectId vid in pl2d)
                vertexIds.Add(vid);

            if (vertexIds.Count < 2)
            {
                DexLogger.Warn(Src, $"Polyline2d {pl2d.ObjectId} has < 2 vertices - skipped.");
                return;
            }

            var verts = new List<PolyVertex>(vertexIds.Count);
            foreach (var vid in vertexIds)
            {
                var v2d = (Vertex2d)tr.GetObject(vid, OpenMode.ForRead);
                verts.Add(new PolyVertex(new Point2d(v2d.Position.X, v2d.Position.Y), v2d.Bulge));
            }

            var extended = DexGeometry.ExtendPolyline(verts, extS, extE);

            for (int i = 0; i < vertexIds.Count; i++)
            {
                var v2d = (Vertex2d)tr.GetObject(vertexIds[i], OpenMode.ForWrite);
                v2d.Position = new Point3d(extended[i].Pt.X, extended[i].Pt.Y, v2d.Position.Z);
                v2d.Bulge = extended[i].Bulge;
            }

            DexLogger.Info(Src, $"Polyline2d {pl2d.ObjectId} extended (start={extS:0.###}, end={extE:0.###}).");
        }
    }
}


// ===== FILE: Core\VersionInfo.cs =====
using System;
using System.IO;
using System.Reflection;

namespace DoubleExtendCS
{
    /// <summary>
    /// Builds the "vX.Y.ZZZ_dd.mm.yyyy_hh.mm.ss" banner that gets printed to
    /// the AutoCAD command line every time the DLL is loaded (NETLOAD or
    /// auto-load), so a user in a live session can immediately tell which
    /// build they are running - equivalent to the LISP file's load banner,
    /// but with an actual version + load-timestamp instead of static text.
    /// </summary>
    public static class VersionInfo
    {
        /// <summary>
        /// e.g. "1.0.001_01.07.2026_05.29.43"
        /// The version part comes from AssemblyFileVersion (bump it per release,
        /// see Properties/AssemblyInfo.cs). The timestamp is the moment this
        /// call runs (i.e. the moment the DLL is loaded into the current session).
        /// </summary>
        public static string GetLoadBanner()
        {
            var asm = Assembly.GetExecutingAssembly();
            var fileVer = asm.GetName().Version; // falls back to AssemblyVersion if FileVersion attribute missing at runtime read
            string verText;
            try
            {
                var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(asm.Location);
                verText = $"{fvi.FileMajorPart}.{fvi.FileMinorPart}.{fvi.FileBuildPart:D3}";
            }
            catch
            {
                verText = $"{fileVer.Major}.{fileVer.Minor}.{fileVer.Build:D3}";
            }

            string stamp = DateTime.Now.ToString("dd.MM.yyyy_HH.mm.ss");
            return $"{verText}_{stamp}";
        }

        /// <summary>On-disk last-write time of the DLL, useful to confirm you're
        /// really running the build you just copied (vs. a stale locked copy).</summary>
        public static DateTime? GetAssemblyBuildTime()
        {
            try
            {
                var path = Assembly.GetExecutingAssembly().Location;
                return File.GetLastWriteTime(path);
            }
            catch
            {
                return null;
            }
        }
    }
}


// ===== FILE: UI\DexForm.cs =====
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using DoubleExtendCS.Core;
using DoubleExtendCS.Jigs;
using DoubleExtendCS.Logging;
using Application = Autodesk.AutoCAD.ApplicationServices.Application;

namespace DoubleExtendCS.UI
{
    /// <summary>
    /// Modeless replacement for the LISP DCL dialog. Opened via
    /// Application.ShowModelessDialog so it stays open while the user
    /// switches between Model Space / Paper Space / other commands.
    /// All Editor calls made from event handlers here run on the AutoCAD
    /// main thread automatically, because WinForms events on a modeless
    /// dialog hosted by ShowModelessDialog are pumped on that same thread.
    /// </summary>
    public class DexForm : Form
    {
        private const string Src = "DexForm";

        // ---- selection / undo state ----
        private ObjectIdCollection _sel = new ObjectIdCollection();
        private List<EntitySnapshot> _baselineSnapshot;   // state when entities were picked / dialog opened
        private List<EntitySnapshot> _preApplySnapshot;    // state just before the last Apply (for Undo)

        private double _extStart = 1.0;
        private double _extEnd = 1.5;
        private bool _bothEnable;
        private double _bothSlot;

        // ---- controls ----
        private Label _lblVersion;
        private Button _btnPick;
        private Label _lblSelCount;
        private DataGridView _grid;
        private TextBox _txtExtStart;
        private TextBox _txtExtEnd;
        private CheckBox _chkBothEnable;
        private TextBox _txtBothSlot;
        private TrackBar _trkBothSlot;
        private Button _btnDynSlot;
        private Button _btnApply;
        private Button _btnUndo;
        private Button _btnDebug;
        private Button _btnOk;
        private Button _btnCancel;
        private PreviewPanel _preview;
        private ListBox _logList;
        private Button _btnSaveLog;

        private bool _suspendEvents;

        public DexForm()
        {
            BuildUi();
            WireEvents();
            RefreshGrid();
            RefreshPreview();
            _lblVersion.Text = "DoubleExtendCS " + VersionInfo.GetLoadBanner();

            DexLogger.EntryAdded += OnLogEntryAdded;
            FormClosed += (s, e) => DexLogger.EntryAdded -= OnLogEntryAdded;

            // seed the visible log with whatever is already buffered (e.g. the
            // load banner printed by Commands.Initialize before this form existed)
            foreach (var entry in DexLogger.Entries)
                AppendLogLine(entry.ToString());
        }

        // ==============================================================
        // UI construction (code-only, no designer file, kept compact)
        // ==============================================================
        private void BuildUi()
        {
            Text = "Double Extend";
            FormBorderStyle = FormBorderStyle.SizableToolWindow; // small title bar, stays above AutoCAD
            ShowInTaskbar = false;
            StartPosition = FormStartPosition.CenterScreen;
            MinimumSize = new Size(560, 640);
            Size = new Size(620, 760);
            KeyPreview = true;

            var root = new TableLayoutPanel
            {
                Dock = DockStyle.Fill,
                ColumnCount = 1,
                Padding = new Padding(8)
            };
            root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // version
            root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // pick row
            root.RowStyles.Add(new RowStyle(SizeType.Absolute, 130)); // grid
            root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // extension group
            root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // both-ends group
            root.RowStyles.Add(new RowStyle(SizeType.Percent, 45)); // preview
            root.RowStyles.Add(new RowStyle(SizeType.Percent, 30)); // log
            root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // action buttons

            _lblVersion = new Label { AutoSize = true, ForeColor = Color.DimGray, Text = "DoubleExtendCS" };
            root.Controls.Add(_lblVersion);

            // ---- Pick row ----
            var pickRow = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
            _btnPick = new Button { Text = "Pick Entities (Window / Crossing)", AutoSize = true };
            _lblSelCount = new Label { Text = "No selection", AutoSize = true, Padding = new Padding(10, 6, 0, 0) };
            pickRow.Controls.Add(_btnPick);
            pickRow.Controls.Add(_lblSelCount);
            root.Controls.Add(pickRow);

            // ---- Grid ----
            _grid = new DataGridView
            {
                Dock = DockStyle.Fill,
                ReadOnly = true,
                AllowUserToAddRows = false,
                AllowUserToDeleteRows = false,
                AllowUserToResizeRows = false,
                SelectionMode = DataGridViewSelectionMode.FullRowSelect,
                MultiSelect = true,
                AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
                RowHeadersVisible = false
            };
            _grid.Columns.Add("idx", "#");
            _grid.Columns.Add("type", "Type");
            _grid.Columns.Add("handle", "Handle");
            _grid.Columns.Add("layer", "Layer");
            root.Controls.Add(_grid);

            // ---- Extension group ----
            var extGroup = new GroupBox { Text = "Extension", AutoSize = true, Dock = DockStyle.Fill, Padding = new Padding(6) };
            var extLayout = new TableLayoutPanel { ColumnCount = 4, AutoSize = true };
            extLayout.Controls.Add(new Label { Text = "Extend Start", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 0);
            _txtExtStart = new TextBox { Width = 80, Text = "1.0" };
            extLayout.Controls.Add(_txtExtStart, 1, 0);
            extLayout.Controls.Add(new Label { Text = "Extend End", AutoSize = true, Anchor = AnchorStyles.Left }, 2, 0);
            _txtExtEnd = new TextBox { Width = 80, Text = "1.5" };
            extLayout.Controls.Add(_txtExtEnd, 3, 0);
            extGroup.Controls.Add(extLayout);
            root.Controls.Add(extGroup);

            // ---- Both-ends group ----
            var bothGroup = new GroupBox { Text = "Both Ends - Horizontal Slot", AutoSize = true, Dock = DockStyle.Fill, Padding = new Padding(6) };
            var bothLayout = new TableLayoutPanel { ColumnCount = 1, AutoSize = true };
            _chkBothEnable = new CheckBox { Text = "Enable Both-Ends Slot", AutoSize = true };
            bothLayout.Controls.Add(_chkBothEnable);

            var slotRow = new FlowLayoutPanel { AutoSize = true };
            slotRow.Controls.Add(new Label { Text = "Slot Length (0 = All Slot)", AutoSize = true, Padding = new Padding(0, 6, 6, 0) });
            _txtBothSlot = new TextBox { Width = 80, Text = "0.0" };
            slotRow.Controls.Add(_txtBothSlot);
            bothLayout.Controls.Add(slotRow);

            _trkBothSlot = new TrackBar { Minimum = 0, Maximum = 1000, TickFrequency = 100, Width = 480 };
            bothLayout.Controls.Add(_trkBothSlot);

            _btnDynSlot = new Button { Text = "Dynamic Drag Preview", AutoSize = true };
            bothLayout.Controls.Add(_btnDynSlot);

            bothGroup.Controls.Add(bothLayout);
            root.Controls.Add(bothGroup);

            // ---- Preview ----
            var previewGroup = new GroupBox { Text = "Preview", Dock = DockStyle.Fill };
            _preview = new PreviewPanel { Dock = DockStyle.Fill, Margin = new Padding(4) };
            previewGroup.Controls.Add(_preview);
            root.Controls.Add(previewGroup);

            // ---- Log ----
            var logGroup = new GroupBox { Text = "Log", Dock = DockStyle.Fill };
            var logLayout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 2 };
            logLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
            logLayout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            _logList = new ListBox { Dock = DockStyle.Fill, Font = new Font(FontFamily.GenericMonospace, 8f), HorizontalScrollbar = true };
            logLayout.Controls.Add(_logList, 0, 0);
            _btnSaveLog = new Button { Text = "Save Log...", AutoSize = true, Anchor = AnchorStyles.Right };
            var saveRow = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Fill };
            saveRow.Controls.Add(_btnSaveLog);
            logLayout.Controls.Add(saveRow, 0, 1);
            logGroup.Controls.Add(logLayout);
            root.Controls.Add(logGroup);

            // ---- Action buttons ----
            var actionRow = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight, Dock = DockStyle.Fill };
            _btnApply = new Button { Text = "Apply", AutoSize = true };
            _btnUndo = new Button { Text = "Undo", AutoSize = true };
            _btnDebug = new Button { Text = "Debug", AutoSize = true };
            _btnOk = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.OK };
            _btnCancel = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
            actionRow.Controls.AddRange(new Control[] { _btnApply, _btnUndo, _btnDebug, _btnOk, _btnCancel });
            root.Controls.Add(actionRow);

            Controls.Add(root);
            AcceptButton = null; // Enter must not silently trigger OK while editing text fields
        }

        private void WireEvents()
        {
            _btnPick.Click += (s, e) => PickEntities();
            _btnApply.Click += (s, e) => ApplyExtension();
            _btnUndo.Click += (s, e) => UndoLastApply();
            _btnDebug.Click += (s, e) => ShowDebugReport();
            _btnDynSlot.Click += (s, e) => RunDynamicSlot();
            _btnSaveLog.Click += (s, e) => SaveLog();

            _txtExtStart.TextChanged += (s, e) => { ReadNumericFields(); RefreshPreview(); };
            _txtExtEnd.TextChanged += (s, e) => { ReadNumericFields(); RefreshPreview(); };
            _chkBothEnable.CheckedChanged += (s, e) => { _bothEnable = _chkBothEnable.Checked; };

            _txtBothSlot.TextChanged += (s, e) =>
            {
                if (_suspendEvents) return;
                if (double.TryParse(_txtBothSlot.Text, out double v))
                {
                    _bothSlot = Math.Max(0, v);
                    _suspendEvents = true;
                    _trkBothSlot.Value = (int)Math.Min(_trkBothSlot.Maximum, _bothSlot * 10);
                    _suspendEvents = false;
                }
            };
            _trkBothSlot.Scroll += (s, e) =>
            {
                if (_suspendEvents) return;
                _bothSlot = _trkBothSlot.Value / 10.0;
                _suspendEvents = true;
                _txtBothSlot.Text = _bothSlot.ToString("0.0");
                _suspendEvents = false;
            };

            // ---- viewport highlight wiring (the "one line" pattern) ----
            _grid.SelectionChanged += Grid_SelectionChanged;

            _btnOk.Click += (s, e) => { ApplyExtension(); Close(); };
            _btnCancel.Click += (s, e) => { RestoreBaseline(); Close(); };
        }

        // Esc in the AutoCAD viewport must not close this floating dialog.
        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            const int WM_KEYDOWN = 0x100;
            if (msg.Msg == WM_KEYDOWN && keyData == Keys.Escape)
                return true; // swallow
            return base.ProcessCmdKey(ref msg, keyData);
        }

        private void Grid_SelectionChanged(object sender, EventArgs e)
        {
            if (_grid.SelectedRows.Count == 0) return;

            var idx = new List<int>();
            foreach (DataGridViewRow row in _grid.SelectedRows)
                idx.Add(row.Index);

            var ids = new ObjectIdCollection();
            var flatIds = _sel.Cast<ObjectId>().ToList();
            foreach (var i in idx)
                if (i >= 0 && i < flatIds.Count)
                    ids.Add(flatIds[i]);

            SelectionUtils.Highlight(ids);
            _preview.SetHighlightIndex(idx.Count == 1 ? idx[0] : -1);
        }

        // ==============================================================
        // Commands
        // ==============================================================

        private void PickEntities()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            var ed = doc.Editor;

            SelectionUtils.Unhighlight();

            var filter = new SelectionFilter(new[]
            {
                new TypedValue((int)DxfCode.Start, "LINE,ARC,LWPOLYLINE,POLYLINE2D")
            });

            ed.WriteMessage("\nSelect objects (Window / Crossing / Pick / Fence / ALL):");
            PromptSelectionResult res = ed.GetSelection(filter);

            if (res.Status != PromptStatus.OK || res.Value.Count == 0)
            {
                DexLogger.Info(Src, "Pick Entities: nothing selected.");
                _sel = new ObjectIdCollection();
                _lblSelCount.Text = "No selection";
                RefreshGrid();
                RefreshPreview();
                return;
            }

            _sel = new ObjectIdCollection(res.Value.GetObjectIds());
            _lblSelCount.Text = $"{_sel.Count} object(s) selected";
            DexLogger.Info(Src, $"Pick Entities: {_sel.Count} object(s) selected.");

            _baselineSnapshot = SelectionSnapshot.CaptureNow(_sel);
            _preApplySnapshot = null;

            SelectionUtils.Highlight(_sel);
            RefreshGrid();
            RefreshPreview();
        }

        private void ApplyExtension()
        {
            if (_sel.Count == 0)
            {
                DexLogger.Warn(Src, "Apply: no entities selected - use Pick Entities first.");
                return;
            }

            ReadNumericFields();

            var doc = Application.DocumentManager.MdiActiveDocument;
            using (doc.LockDocument())
            using (var tr = doc.Database.TransactionManager.StartTransaction())
            {
                _preApplySnapshot = SelectionSnapshot.Capture(tr, _sel);

                EntityExtender.RunAll(tr, _sel, _extStart, _extEnd);

                if (_bothEnable && _bothSlot > 0.0)
                    EntityExtender.RunAll(tr, _sel, _bothSlot, _bothSlot);

                tr.Commit();
            }
            doc.Editor.Regen();
            SelectionUtils.Highlight(_sel);
            RefreshPreview();
            DexLogger.Info(Src, $"Apply: extStart={_extStart:0.###}, extEnd={_extEnd:0.###}, bothEnable={_bothEnable}, bothSlot={_bothSlot:0.###}.");
        }

        private void UndoLastApply()
        {
            if (_preApplySnapshot == null)
            {
                DexLogger.Info(Src, "Undo: nothing to undo.");
                return;
            }

            SelectionSnapshot.RestoreNow(_preApplySnapshot);
            SelectionUtils.Highlight(_sel);
            RefreshPreview();
            DexLogger.Info(Src, "Undo: last Apply reverted.");
            _preApplySnapshot = null;
        }

        private void RestoreBaseline()
        {
            if (_baselineSnapshot != null)
            {
                SelectionSnapshot.RestoreNow(_baselineSnapshot);
                DexLogger.Info(Src, "Cancel: restored geometry to state at Pick Entities.");
            }
            SelectionUtils.Unhighlight();
        }

        private void RunDynamicSlot()
        {
            if (_sel.Count == 0)
            {
                DexLogger.Warn(Src, "Dynamic Drag Preview: no entities selected - use Pick Entities first.");
                return;
            }

            var doc = Application.DocumentManager.MdiActiveDocument;
            var ed = doc.Editor;

            List<EntitySnapshot> preJigSnapshot;
            Point3d basePt;
            using (var tr = doc.Database.TransactionManager.StartTransaction())
            {
                preJigSnapshot = SelectionSnapshot.Capture(tr, _sel);
                basePt = SelectionUtils.Midpoint(tr, _sel);
                tr.Commit();
            }

            var jig = new BothEndsSlotJig(_sel, basePt, preJigSnapshot);
            bool confirmed;
            using (doc.LockDocument())
            {
                confirmed = jig.Run(ed);
            }

            if (confirmed)
            {
                using (doc.LockDocument())
                using (var tr = doc.Database.TransactionManager.StartTransaction())
                {
                    _preApplySnapshot = SelectionSnapshot.Capture(tr, _sel);
                    EntityExtender.RunAll(tr, _sel, jig.ConfirmedSlot, jig.ConfirmedSlot);
                    tr.Commit();
                }
                doc.Editor.Regen();
                _bothSlot = jig.ConfirmedSlot;
                _suspendEvents = true;
                _txtBothSlot.Text = _bothSlot.ToString("0.0");
                _trkBothSlot.Value = (int)Math.Min(_trkBothSlot.Maximum, _bothSlot * 10);
                _suspendEvents = false;
            }
            else
            {
                SelectionSnapshot.RestoreNow(preJigSnapshot);
            }

            SelectionUtils.Highlight(_sel);
            RefreshPreview();
        }

        private void ShowDebugReport()
        {
            var sb = new System.Text.StringBuilder();
            sb.AppendLine("============================================================");
            sb.AppendLine($"DEX DEBUG REPORT  {DateTime.Now:dd/MM/yyyy HH:mm:ss}");
            sb.AppendLine("============================================================");
            sb.AppendLine($"extStart   = {_extStart:0.000000}");
            sb.AppendLine($"extEnd     = {_extEnd:0.000000}");
            sb.AppendLine($"bothSlot   = {_bothSlot:0.000000}");
            sb.AppendLine($"bothEnable = {_bothEnable}");
            sb.AppendLine($"Selection  : {_sel.Count} object(s)");

            var doc = Application.DocumentManager.MdiActiveDocument;
            using (var tr = doc.Database.TransactionManager.StartTransaction())
            {
                int i = 0;
                foreach (ObjectId id in _sel)
                {
                    if (id.IsErased) continue;
                    var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
                    sb.AppendLine($"  [{i}] {ent?.GetType().Name}  handle={ent?.Handle}  layer={ent?.Layer}");
                    i++;
                }
                tr.Commit();
            }
            sb.AppendLine("============================================================");

            DexLogger.Info(Src, "Debug report generated (see log for full detail).");
            foreach (var line in sb.ToString().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries))
                DexLogger.Debug(Src, line.TrimEnd('\r'));

            var path = DexLogger.SaveToFile();
            if (path != null)
                DexLogger.Info(Src, $"Debug log written to: {path}");
        }

        private void SaveLog()
        {
            using (var dlg = new SaveFileDialog
            {
                Filter = "Text log (*.txt)|*.txt",
                FileName = $"DEX_log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt"
            })
            {
                if (dlg.ShowDialog(this) == DialogResult.OK)
                {
                    var folder = Path.GetDirectoryName(dlg.FileName);
                    var path = DexLogger.SaveToFile(folder);
                    if (path != null && !string.Equals(path, dlg.FileName, StringComparison.OrdinalIgnoreCase))
                    {
                        File.Copy(path, dlg.FileName, true);
                    }
                    DexLogger.Info(Src, $"Log saved to {dlg.FileName}");
                }
            }
        }

        // ==============================================================
        // Helpers
        // ==============================================================

        private void ReadNumericFields()
        {
            if (double.TryParse(_txtExtStart.Text, out double s)) _extStart = s;
            if (double.TryParse(_txtExtEnd.Text, out double eVal)) _extEnd = eVal;
        }

        private void RefreshGrid()
        {
            _grid.Rows.Clear();
            if (_sel.Count == 0) return;

            var doc = Application.DocumentManager.MdiActiveDocument;
            using (var tr = doc.Database.TransactionManager.StartTransaction())
            {
                int i = 0;
                foreach (ObjectId id in _sel)
                {
                    if (id.IsErased) continue;
                    var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
                    _grid.Rows.Add(i, ent?.GetType().Name, ent?.Handle.ToString(), ent?.Layer);
                    i++;
                }
                tr.Commit();
            }
        }

        private void RefreshPreview()
        {
            if (_sel.Count == 0)
            {
                _preview.SetGeometry(Enumerable.Empty<List<PointF>>());
                return;
            }

            ReadNumericFields();

            // Prefer the pure in-memory extended preview (fast, no DB writes);
            // fall back to sampling live DB geometry if we have no baseline yet.
            List<List<PointF>> geometry;
            if (_baselineSnapshot != null)
            {
                geometry = PreviewGeometryCollector.CollectExtendedPreview(_baselineSnapshot, _extStart, _extEnd);
            }
            else
            {
                var doc = Application.DocumentManager.MdiActiveDocument;
                using (var tr = doc.Database.TransactionManager.StartTransaction())
                {
                    geometry = PreviewGeometryCollector.Collect(tr, _sel);
                    tr.Commit();
                }
            }

            _preview.SetGeometry(geometry);
        }

        private void OnLogEntryAdded(LogEntry entry)
        {
            if (IsDisposed) return;
            if (InvokeRequired)
            {
                try { BeginInvoke(new Action(() => AppendLogLine(entry.ToString()))); }
                catch (ObjectDisposedException) { /* form closing race - ignore */ }
                return;
            }
            AppendLogLine(entry.ToString());
        }

        private void AppendLogLine(string line)
        {
            _logList.Items.Add(line);
            if (_logList.Items.Count > 2000)
                _logList.Items.RemoveAt(0);
            _logList.TopIndex = _logList.Items.Count - 1;
        }
    }
}


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

namespace DoubleExtendCS.UI
{
    /// <summary>
    /// Self-contained 2D preview panel. Entirely decoupled from AutoCAD - once
    /// the caller has collected world-space polylines (a List of PointF chains,
    /// one per entity), this control only ever does WinForms drawing. Copy this
    /// class + PreviewGeometryCollector as a pair into any other project.
    ///
    /// Transform: scale = Min(panelW / worldW, panelH / worldH), then flip Y
    /// (CAD is Y-up, GDI+ is Y-down) and centre the drawing in the panel.
    /// </summary>
    public class PreviewPanel : Panel
    {
        private List<List<PointF>> _worldPolylines = new List<List<PointF>>();
        private int _highlightIndex = -1;

        public Color CurveColor { get; set; } = Color.DeepSkyBlue;
        public Color HighlightColor { get; set; } = Color.OrangeRed;
        public Color BackgroundGridColor { get; set; } = Color.FromArgb(235, 235, 235);

        public PreviewPanel()
        {
            DoubleBuffered = true;
            BackColor = Color.White;
            BorderStyle = BorderStyle.FixedSingle;
        }

        /// <summary>Replace the geometry to preview. Each inner list is one
        /// entity's sampled point chain, already in world (drawing) units.</summary>
        public void SetGeometry(IEnumerable<List<PointF>> worldPolylines)
        {
            _worldPolylines = worldPolylines?.Select(p => p.ToList()).ToList() ?? new List<List<PointF>>();
            _highlightIndex = -1;
            Invalidate();
        }

        public void SetHighlightIndex(int index)
        {
            _highlightIndex = index;
            Invalidate();
        }

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

            if (_worldPolylines.Count == 0 || Width < 4 || Height < 4)
            {
                using (var f = new Font(Font.FontFamily, 9f))
                using (var b = new SolidBrush(Color.Gray))
                {
                    var msg = "No selection to preview";
                    var size = g.MeasureString(msg, f);
                    g.DrawString(msg, f, b, (Width - size.Width) / 2f, (Height - size.Height) / 2f);
                }
                return;
            }

            var bounds = ComputeWorldBounds();
            float worldW = Math.Max(bounds.Width, 1e-6f);
            float worldH = Math.Max(bounds.Height, 1e-6f);

            const int margin = 12;
            float availW = Math.Max(Width - 2 * margin, 1);
            float availH = Math.Max(Height - 2 * margin, 1);

            float scale = Math.Min(availW / worldW, availH / worldH);

            // Centre the drawing
            float drawnW = worldW * scale;
            float drawnH = worldH * scale;
            float offsetX = margin + (availW - drawnW) / 2f;
            float offsetY = margin + (availH - drawnH) / 2f;

            PointF ToPanel(PointF wp)
            {
                float px = offsetX + (wp.X - bounds.Left) * scale;
                // Flip Y: world Y-up -> panel Y-down
                float py = offsetY + (bounds.Bottom - wp.Y) * scale;
                return new PointF(px, py);
            }

            for (int i = 0; i < _worldPolylines.Count; i++)
            {
                var chain = _worldPolylines[i];
                if (chain.Count < 2) continue;

                var panelPts = chain.Select(ToPanel).ToArray();
                Color c = (i == _highlightIndex) ? HighlightColor : CurveColor;
                using (var pen = new Pen(c, (i == _highlightIndex) ? 2.4f : 1.6f))
                {
                    g.DrawLines(pen, panelPts);
                }
            }
        }

        private RectangleF ComputeWorldBounds()
        {
            float minX = float.MaxValue, minY = float.MaxValue;
            float maxX = float.MinValue, maxY = float.MinValue;

            foreach (var chain in _worldPolylines)
            {
                foreach (var p in chain)
                {
                    if (p.X < minX) minX = p.X;
                    if (p.X > maxX) maxX = p.X;
                    if (p.Y < minY) minY = p.Y;
                    if (p.Y > maxY) maxY = p.Y;
                }
            }

            if (minX > maxX) return new RectangleF(0, 0, 1, 1);

            // Small padding so geometry never touches the panel edge exactly.
            float padX = Math.Max((maxX - minX) * 0.05f, 0.01f);
            float padY = Math.Max((maxY - minY) * 0.05f, 0.01f);
            return RectangleF.FromLTRB(minX - padX, minY - padY, maxX + padX, maxY + padY);
        }
    }
}


// ===== FILE: Commands.cs =====
using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using DoubleExtendCS.Core;
using DoubleExtendCS.Logging;
using DoubleExtendCS.UI;
using Application = Autodesk.AutoCAD.ApplicationServices.Application;
using Exception = System.Exception;

namespace DoubleExtendCS
{
    /// <summary>
    /// Entry point. IExtensionApplication.Initialize runs once per DLL load
    /// (NETLOAD or auto-load via a .bundle / acad.rx path) - this is where we
    /// print the version+timestamp banner requested ("Display after loading
    /// the dll each time a new version is loaded").
    /// </summary>
    public class Commands : IExtensionApplication
    {
        private const string Src = "Commands";
        private static DexForm _form; // single modeless instance, reused across DEX invocations

        public void Initialize()
        {
            string banner = VersionInfo.GetLoadBanner();
            DexLogger.Info(Src, $"DoubleExtendCS loaded - build {banner}");

            var doc = Application.DocumentManager.MdiActiveDocument;
            doc?.Editor.WriteMessage(
                "\n:: DoubleExtendCS | modeless dialog | live preview | viewport highlight | dynamic slot jig ::" +
                $"\n:: Build {banner} ::" +
                "\n:: Commands: DEX  /  EXTENDSTARTENDMULTIPLE  /  DEXDEBUG ::\n");
        }

        public void Terminate()
        {
            DexLogger.Info(Src, "DoubleExtendCS unloading.");
            _form?.Close();
            _form = null;
        }

        [CommandMethod("EXTENDSTARTENDMULTIPLE", CommandFlags.Modal | CommandFlags.Redraw)]
        public void ExtendStartEndMultiple()
        {
            ShowForm();
        }

        [CommandMethod("DEX")]
        public void Dex()
        {
            ShowForm();
        }

        [CommandMethod("DEXDEBUG")]
        public void DexDebug()
        {
            var path = DexLogger.SaveToFile();
            var doc = Application.DocumentManager.MdiActiveDocument;
            doc.Editor.WriteMessage(path != null
                ? $"\nDEX debug log written to: {path}\n"
                : "\nDEX debug log could not be written.\n");
        }

        private void ShowForm()
        {
            try
            {
                if (_form == null || _form.IsDisposed)
                {
                    _form = new DexForm();
                    Application.ShowModelessDialog(_form);
                }
                else
                {
                    _form.Activate();
                }
            }
            catch (Exception ex)
            {
                DexLogger.Error(Src, $"Failed to open DexForm: {ex}");
                Application.DocumentManager.MdiActiveDocument?.Editor.WriteMessage($"\nDEX error: {ex.Message}\n");
            }
        }
    }
}


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

// ==============================================================
// Bump AssemblyVersion / AssemblyFileVersion here for every release.
// VersionInfo.cs reads AssemblyFileVersion + the DLL's own on-disk
// timestamp to build the banner printed to the command line each
// time the DLL is (re)loaded into AutoCAD / Civil 3D.
// ==============================================================
[assembly: AssemblyTitle("DoubleExtendCS")]
[assembly: AssemblyDescription("Double Extend (Lengthen Start/End) - Civil 3D managed .NET plugin")]
[assembly: AssemblyCompany("DB E.C.O. Group")]
[assembly: AssemblyProduct("DoubleExtendCS")]
[assembly: AssemblyCopyright("Copyright (c) 2026")]
[assembly: ComVisible(false)]

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.1.0")]

// ObjectARX / AutoCAD .NET plugin entry points
[assembly: ExtensionApplication(typeof(Commands))]
[assembly: CommandClass(typeof(Commands))]


// ===== FILE: Jigs\BothEndsSlotJig.cs =====
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using DoubleExtendCS.Core;
using DoubleExtendCS.Logging;

namespace DoubleExtendCS.Jigs
{
    /// <summary>
    /// Improvement over the original LISP: instead of grread/grdraw drawing a
    /// flat screen-space slider bar (DEX:draw-slider) and re-running entmod on
    /// every mouse-move sample (DEX:dynamic-both), this Jig lets AutoCAD do
    /// what it's built for - WorldDraw is called by the graphics system itself
    /// on every regen, and Sampler only needs to report the current slot
    /// value. The actual entity geometry is only committed once, when the
    /// user confirms, which is both faster and safer (no repeated entmod/undo
    /// churn while dragging).
    /// </summary>
    public class BothEndsSlotJig : DrawJig
    {
        private readonly ObjectIdCollection _ids;
        private readonly Point3d _basePt;
        private readonly List<EntitySnapshot> _originalSnapshot;

        private double _slotVal;
        private Point3d _lastMouse;

        public double ConfirmedSlot { get; private set; }

        public BothEndsSlotJig(ObjectIdCollection ids, Point3d basePt, List<EntitySnapshot> originalSnapshot)
        {
            _ids = ids;
            _basePt = basePt;
            _originalSnapshot = originalSnapshot;
            _slotVal = 0.0;
        }

        protected override SamplerStatus Sampler(JigPrompts prompts)
        {
            var opts = new JigPromptPointOptions("\nDrag RIGHT for Both-Ends slot amount: ")
            {
                UserInputControls = UserInputControls.NullResponseAccepted
                                    | UserInputControls.NoNegativeResponseAccepted
                                    | UserInputControls.GovernedByOrthoMode
                                    | UserInputControls.Accept3dCoordinatesAlways
            };

            PromptPointResult res = prompts.AcquirePoint(opts);
            if (res.Status != PromptStatus.OK)
                return SamplerStatus.Cancel;

            if (res.Value.IsEqualTo(_lastMouse))
                return SamplerStatus.NoChange;

            _lastMouse = res.Value;
            _slotVal = Math.Max(0.0, res.Value.X - _basePt.X);
            return SamplerStatus.OK;
        }

        public override bool WorldDraw(WorldDraw draw)
        {
            draw.Geometry.PushModelTransform(Matrix3d.Identity);
            try
            {
                // Preview: show the base point, drag direction, and a live
                // numeric readout so the user gets exact geometric feedback
                // rather than an abstract screen-space bar.
                draw.SubEntityTraits.Color = 4; // cyan
                draw.Geometry.WorldLine(_basePt, new Point3d(_basePt.X + _slotVal, _basePt.Y, _basePt.Z));

                draw.SubEntityTraits.Color = 2; // yellow marker at base
                draw.Geometry.Circle(_basePt, ComputeMarkerRadius(draw), Vector3d.ZAxis);

                // Live-extend a lightweight *preview only* copy of each entity's
                // end geometry so the operator sees the true result while
                // dragging - the real DB entities are left untouched until Commit().
                DrawExtendedPreview(draw);
            }
            finally
            {
                draw.Geometry.PopModelTransform();
            }
            return true;
        }

        private double ComputeMarkerRadius(WorldDraw draw) => 0.15; // simple fixed-size marker; swap for a viewport-scaled size if desired

        private void DrawExtendedPreview(WorldDraw draw)
        {
            // Recomputed entirely from the cached snapshot (no database access
            // inside WorldDraw - it can be called many times per second while
            // dragging, so keeping this in-memory-only matters for smoothness).
            // NOTE: Polyline/Polyline2d previews are intentionally left out of
            // the jig's WorldDraw for performance; the modeless form's
            // PreviewPanel (PictureBox-based) already renders every supported
            // entity type cheaply from cached points - see UI/PreviewPanel.cs.
            foreach (var snap in _originalSnapshot)
            {
                if (snap is LineSnapshot ls)
                {
                    Vector3d dir = ls.End - ls.Start;
                    double len = dir.Length;
                    if (len < 1e-8) continue;
                    Vector3d u = dir / len;
                    Point3d newStart = ls.Start - u * _slotVal;
                    Point3d newEnd = ls.End + u * _slotVal;
                    draw.SubEntityTraits.Color = 3; // green preview
                    draw.Geometry.WorldLine(newStart, newEnd);
                }
                else if (snap is ArcSnapshot arcSnap)
                {
                    if (arcSnap.Radius < 1e-8) continue;
                    double a1 = arcSnap.StartAngle - _slotVal / arcSnap.Radius;
                    double a2 = arcSnap.EndAngle + _slotVal / arcSnap.Radius;
                    Vector3d startVector = new Vector3d(Math.Cos(a1), Math.Sin(a1), 0.0);
                    draw.SubEntityTraits.Color = 3;
                    draw.Geometry.CircularArc(arcSnap.Center, arcSnap.Radius, arcSnap.Normal, startVector, a2 - a1);
                }
            }
        }

        /// <summary>Runs the jig; returns true if the user confirmed (left
        /// click / Enter), false on cancel (Esc / right-click). On confirm,
        /// ConfirmedSlot holds the final value to persist via EntityExtender.</summary>
        public bool Run(Editor ed)
        {
            PromptResult pr = ed.Drag(this);
            if (pr.Status == PromptStatus.OK)
            {
                ConfirmedSlot = _slotVal;
                DexLogger.Info("BothEndsSlotJig", $"Confirmed slot = {_slotVal:0.###}");
                return true;
            }

            DexLogger.Info("BothEndsSlotJig", "Cancelled by user.");
            return false;
        }
    }
}

Additional info:

Share this page:

Page Tag: 🏷️ Apply

  • ExtendStartEndMultiple

    Short description🧩 ExtendStartEndMultiple extends the start and end of multiple selected LINE ARC LWPOLYLINE and POLYLINE objects using independent extension values. 🎛️ The DCL interface provides separate Start and End values together with Apply Undo OK and Cancel. ↩️ Apply can be tested repeatedly and Undo reverses the most recent Apply while Cancel restores the…

  • MultiOffsetBothSidesWithLayer

    Short description🧩 MultiOffsetBothSidesWithLayer is the M_07 C# AutoCAD and Civil 3D offset project for generating structured left and right parallel geometry from selected centerline objects. 🎛️ The current project uses a modeless WinForms interface with profiles pipe counts left and right offsets extra ducts target layers closing centerline source handling live preview Apply and Undo…