using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.Geometry; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Linq; using System.Timers; // Register command class with AutoCAD [assembly: CommandClass(typeof(AutoCADChangeTracker.Commands))] namespace AutoCADChangeTracker { /// /// Main command class for the AutoCAD Change Tracker /// public class Commands { // Store original positions of objects private static Dictionary originalPositions = new Dictionary(); // Store original transformation matrices for blocks private static Dictionary originalBlockTransforms = new Dictionary(); // Store original attribute values for blocks private static Dictionary> originalBlockAttributes = new Dictionary>(); // Track whether monitoring is active private static bool isTracking = false; // Timer for periodic scans private static System.Timers.Timer scanTimer = null; // Queue for network operations to avoid blocking the main thread private static Queue eventQueue = new Queue(); private static object queueLock = new object(); // Background thread for sending events private static System.Threading.Thread senderThread = null; private static bool keepSenderRunning = false; // Performance monitoring private static DateTime lastPerformanceCheck = DateTime.MinValue; private static int operationsCount = 0; private static int maxOperationsPerMinute = 300; // Can be adjusted based on performance needs // Configurable settings private static int scanIntervalMs = 5000; // 5 seconds default private static int maxQueueSize = 1000; // Maximum events to queue // Server URL to send change data to (customize this) private static string serverUrl = "http://localhost:3000/api/events"; /// /// Command to start monitoring changes /// [CommandMethod("TRACKSTART")] public static void StartTracking() { try { // Check if already tracking if (isTracking) { LogMessage("Change tracking is already active."); return; } // Get the active document Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) { LogMessage("No active document found."); return; } // Access the database Database db = doc.Database; // Subscribe to events db.ObjectAppended += OnObjectAppended; db.ObjectModified += OnObjectModified; db.ObjectErased += OnObjectErased; // Subscribe to document events to detect attribute editing doc.CommandEnded += OnCommandEnded; doc.CommandWillStart += OnCommandWillStart; doc.CommandCancelled += OnCommandCancelled; // Setup periodic scanning timer with configurable interval scanTimer = new System.Timers.Timer(scanIntervalMs); scanTimer.Elapsed += (sender, e) => { // Skip scan if we're performing too many operations if (IsOperationRateTooHigh()) { LogMessage("Skipping scan due to high operation rate."); return; } ScanForAttributeChanges(); }; scanTimer.AutoReset = true; scanTimer.Enabled = true; // Initialize current state of blocks CaptureCurrentBlockState(); // Start background sender thread StartSenderThread(); // Set tracking flag isTracking = true; // Reset performance counters ResetPerformanceCounters(); // Notify user LogMessage("AutoCAD Change Tracker started successfully."); } catch (global::System.Exception ex) { LogMessage($"Error starting change tracker: {ex.Message}"); } } /// /// Command to stop monitoring changes /// [CommandMethod("TRACKSTOP")] public static void StopTracking() { try { // Check if tracking is active if (!isTracking) { LogMessage("Change tracking is not currently active."); return; } // Get the active document Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) { LogMessage("No active document found."); return; } // Access the database Database db = doc.Database; // Unsubscribe from events - do this first to prevent new events db.ObjectAppended -= OnObjectAppended; db.ObjectModified -= OnObjectModified; db.ObjectErased -= OnObjectErased; // Unsubscribe from document events doc.CommandEnded -= OnCommandEnded; doc.CommandWillStart -= OnCommandWillStart; doc.CommandCancelled -= OnCommandCancelled; // Stop timer if (scanTimer != null) { scanTimer.Stop(); scanTimer.Dispose(); scanTimer = null; } // Stop sender thread StopSenderThread(); // Clear tracking flag isTracking = false; // Clean up tracking dictionaries to free memory CleanupTrackingData(); // Notify user LogMessage("AutoCAD Change Tracker stopped."); } catch (global::System.Exception ex) { LogMessage($"Error stopping change tracker: {ex.Message}"); } } /// /// New command to adjust scan interval /// [CommandMethod("TRACKINTERVAL")] public static void SetScanInterval() { try { Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) return; Editor ed = doc.Editor; // Prompt for new interval PromptIntegerOptions opts = new PromptIntegerOptions("\nEnter new scan interval in milliseconds (min 1000):"); opts.AllowNegative = false; opts.AllowZero = false; opts.DefaultValue = scanIntervalMs; opts.UseDefaultValue = true; PromptIntegerResult result = ed.GetInteger(opts); if (result.Status != PromptStatus.OK) return; // Validate and set new interval int newInterval = Math.Max(1000, result.Value); // Minimum 1 second scanIntervalMs = newInterval; // Update active timer if tracking is on if (isTracking && scanTimer != null) { scanTimer.Interval = scanIntervalMs; } LogMessage($"Scan interval updated to {scanIntervalMs} ms."); } catch (global::System.Exception ex) { LogMessage($"Error setting scan interval: {ex.Message}"); } } /// /// Cleanup tracking data to free memory /// private static void CleanupTrackingData() { // Clear all tracking dictionaries originalPositions.Clear(); originalBlockTransforms.Clear(); originalBlockAttributes.Clear(); // Force garbage collection GC.Collect(); GC.WaitForPendingFinalizers(); } /// /// Reset performance counters /// private static void ResetPerformanceCounters() { lastPerformanceCheck = DateTime.Now; operationsCount = 0; } /// /// Check if operation rate is too high to perform additional operations /// private static bool IsOperationRateTooHigh() { // Increment operation counter operationsCount++; // Check if a minute has passed since last check TimeSpan elapsed = DateTime.Now - lastPerformanceCheck; if (elapsed.TotalMinutes >= 1) { // Reset counters if a minute has passed ResetPerformanceCounters(); return false; } // If we've exceeded our operation rate limit, throttle return operationsCount > maxOperationsPerMinute; } /// /// Start background thread for sending events to server /// private static void StartSenderThread() { if (senderThread != null) return; keepSenderRunning = true; senderThread = new System.Threading.Thread(ProcessEventQueue) { IsBackground = true, Name = "AutoCAD_EventSender" }; senderThread.Start(); } /// /// Stop background sender thread /// private static void StopSenderThread() { if (senderThread == null) return; keepSenderRunning = false; // Wait for thread to finish, but not indefinitely if (!senderThread.Join(2000)) { try { senderThread.Abort(); // Last resort } catch { // Ignore abort errors } } senderThread = null; } /// /// Background thread method for processing event queue /// private static void ProcessEventQueue() { while (keepSenderRunning) { object eventData = null; // Safely dequeue an event lock (queueLock) { if (eventQueue.Count > 0) { eventData = eventQueue.Dequeue(); } } // Process event if we have one if (eventData != null) { try { SendEventToServer(eventData).Wait(); } catch (global::System.Exception ex) { // Log error but continue processing queue LogMessage($"Error sending event: {ex.Message}"); } } else { // No events to process, sleep to avoid spinning System.Threading.Thread.Sleep(100); } } } /// /// Helper method to enqueue event for background sending /// private static void SendToServer(object data) { try { lock (queueLock) { // Limit queue size to prevent memory issues if (eventQueue.Count > maxQueueSize) { // Remove oldest event if queue is full eventQueue.Dequeue(); } // Add the new event eventQueue.Enqueue(data); } } catch (global::System.Exception ex) { LogMessage($"Error queueing event: {ex.Message}"); } } /// /// Async method to actually send data to server /// private static async Task SendEventToServer(object data) { try { // Create HTTP client using var client = new HttpClient(); client.Timeout = TimeSpan.FromSeconds(5); // Convert data to JSON string json = JsonSerializer.Serialize(data); var content = new StringContent(json, Encoding.UTF8, "application/json"); // Send data asynchronously var response = await client.PostAsync(serverUrl, content); // We don't log success to reduce noise, only log failures if (!response.IsSuccessStatusCode) { LogMessage($"Server returned error: {response.StatusCode}"); } } catch (global::System.Exception ex) { LogMessage($"Error sending data to server: {ex.Message}"); } } /// /// Event handler for command start /// private static void OnCommandWillStart(object sender, CommandEventArgs e) { try { // Skip if operation rate is too high if (IsOperationRateTooHigh()) return; // Capture current state before command runs for comparison later if (IsAttributeEditCommand(e.GlobalCommandName)) { LogMessage($"Attribute edit command starting: {e.GlobalCommandName}"); // Take a snapshot to compare later CaptureCurrentBlockState(); } } catch (global::System.Exception ex) { LogMessage($"Error in command will start handler: {ex.Message}"); } } /// /// Event handler for command completion - used to detect attribute edits /// private static void OnCommandEnded(object sender, CommandEventArgs e) { try { // Skip if operation rate is too high if (IsOperationRateTooHigh()) return; // Check for attribute editing commands if (IsAttributeEditCommand(e.GlobalCommandName)) { LogMessage($"Attribute edit command ended: {e.GlobalCommandName}"); // Scan for attribute changes after attribute editing commands // But delay slightly to let AutoCAD complete its operations Application.Idle += DelayedAttributeScan; } } catch (global::System.Exception ex) { LogMessage($"Error in command ended handler: {ex.Message}"); } } /// /// Helper for delayed attribute scanning /// private static void DelayedAttributeScan(object sender, EventArgs e) { // Remove handler after first execution Application.Idle -= DelayedAttributeScan; try { // Wait a brief moment to let AutoCAD finish processing System.Threading.Thread.Sleep(100); // Do the actual scan ScanForAttributeChanges(); } catch (global::System.Exception ex) { LogMessage($"Error in delayed attribute scan: {ex.Message}"); } } /// /// Event handler for command cancellation /// private static void OnCommandCancelled(object sender, CommandEventArgs e) { // Skip if operation rate is too high if (IsOperationRateTooHigh()) return; // Still check for changes even if command was cancelled if (IsAttributeEditCommand(e.GlobalCommandName)) { LogMessage($"Attribute edit command cancelled: {e.GlobalCommandName}"); // Delay scan to prevent AutoCAD freezes Application.Idle += DelayedAttributeScan; } } /// /// Helper to check if a command is an attribute editing command /// private static bool IsAttributeEditCommand(string commandName) { string upperCmd = commandName.ToUpper(); return upperCmd == "ATTEDIT" || upperCmd == "EATTEDIT" || upperCmd == "BATTMAN" || upperCmd == "PROPERTIES" || upperCmd == "DDEDIT" || upperCmd == "ATTSYNC" || upperCmd == "ATTIPEDIT" || upperCmd == "ATTREDEF" || upperCmd == "REFEDIT" || upperCmd == "MLEDIT" || upperCmd == "TEXTEDIT" || upperCmd == "MODIFY" || upperCmd == "CHANGE"; } /// /// Scans all blocks to check for attribute changes /// private static void ScanForAttributeChanges() { try { // Skip if operation rate is too high if (IsOperationRateTooHigh()) return; // Make sure we're on the main thread Autodesk.AutoCAD.ApplicationServices.Core.Application.Idle += new EventHandler(delegate(object s, EventArgs e) { Autodesk.AutoCAD.ApplicationServices.Core.Application.Idle -= new EventHandler(delegate(object s2, EventArgs e2) { }); try { Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) return; using (var tr = doc.TransactionManager.StartTransaction()) { // Get the current database Database db = doc.Database; // Get the block table BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; if (bt == null) { tr.Commit(); return; } // Get the model space block table record BlockTableRecord modelSpace = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord; if (modelSpace == null) { tr.Commit(); return; } // Iterate through entities in model space - limit to 100 per scan to avoid freezes int processedEntities = 0; int maxEntitiesToProcess = 100; foreach (ObjectId objId in modelSpace) { // Limit processing to avoid freezes if (processedEntities > maxEntitiesToProcess) break; processedEntities++; // Skip if we don't have original data for this object if (!originalBlockAttributes.ContainsKey(objId)) continue; Entity ent = tr.GetObject(objId, OpenMode.ForRead) as Entity; if (ent == null) continue; // Check if this is a block reference if (ent is BlockReference blockRef) { // Get the original attributes for this block Dictionary oldAttributes = originalBlockAttributes[objId]; // Get the current attributes Dictionary newAttributes = new Dictionary(); Dictionary attributeChanges = new Dictionary(); // Collect current attribute values foreach (ObjectId attId in blockRef.AttributeCollection) { if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) { // Store new attribute value newAttributes[attRef.Tag] = attRef.TextString; // Check if attribute changed if (oldAttributes.TryGetValue(attRef.Tag, out string oldValue) && oldValue != attRef.TextString) { attributeChanges[attRef.Tag] = new { OldValue = oldValue, NewValue = attRef.TextString }; } } } // If we found changes, report them if (attributeChanges.Count > 0) { LogMessage($"Detected attribute changes in block {blockRef.Name}"); // Get current position Point3d position = GetEntityPosition(blockRef); // Create event data for attribute changes var blockData = new { EventType = "BlockAttributesChanged", ObjectId = blockRef.ObjectId.Handle.ToString(), BlockName = blockRef.Name, Position = new { X = position.X, Y = position.Y, Z = position.Z }, AttributeChanges = attributeChanges, Attributes = newAttributes, Timestamp = DateTime.UtcNow }; // Send event to server SendToServer(blockData); // Update our stored attributes originalBlockAttributes[objId] = newAttributes; } } } tr.Commit(); } } catch (global::System.Exception ex) { LogMessage($"Error during periodic attribute scan: {ex.Message}"); } }); } catch (global::System.Exception ex) { LogMessage($"Error scanning for attribute changes: {ex.Message}"); } } /// /// Captures the current state of all blocks in the drawing /// private static void CaptureCurrentBlockState() { try { Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) return; using (var tr = doc.TransactionManager.StartTransaction()) { // Get the current database Database db = doc.Database; // Get the block table BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; if (bt == null) { tr.Commit(); return; } // Get the model space block table record BlockTableRecord modelSpace = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord; if (modelSpace == null) { tr.Commit(); return; } // Count for performance monitoring int processedCount = 0; int maxToProcess = 500; // Limit to prevent freezing on huge drawings // Flag to check if this is the first capture bool isFirstCapture = originalPositions.Count == 0; // Iterate through entities in model space foreach (ObjectId objId in modelSpace) { // Limit processing for performance processedCount++; if (processedCount > maxToProcess && !isFirstCapture) break; // Skip already tracked entities on subsequent captures if (!isFirstCapture && originalPositions.ContainsKey(objId)) continue; Entity ent = tr.GetObject(objId, OpenMode.ForRead) as Entity; if (ent == null) continue; // Store position for all entities Point3d position = GetEntityPosition(ent); originalPositions[ent.ObjectId] = position; // For block references, store additional information if (ent is BlockReference blockRef) { // Store transformation matrix originalBlockTransforms[blockRef.ObjectId] = blockRef.BlockTransform; // Store attribute values Dictionary attributes = new Dictionary(); foreach (ObjectId attId in blockRef.AttributeCollection) { if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) { attributes[attRef.Tag] = attRef.TextString; } } originalBlockAttributes[blockRef.ObjectId] = attributes; } } tr.Commit(); } // If it's the first capture and we have lots of entities, suggest lower frequency if (originalPositions.Count > 1000 && scanIntervalMs < 10000) { LogMessage($"Large drawing detected ({originalPositions.Count} entities). Consider increasing scan interval with TRACKINTERVAL command."); } } catch (global::System.Exception ex) { LogMessage($"Error capturing block state: {ex.Message}"); } } /// /// Event handler for object additions /// private static void OnObjectAppended(object sender, ObjectEventArgs e) { try { // Check if object is an entity if (e.DBObject is not Entity entity) { return; } // Access the entity safely within a transaction Document doc = Application.DocumentManager.MdiActiveDocument; using (var tr = doc.TransactionManager.StartTransaction()) { // Open the entity for read entity = (Entity)tr.GetObject(entity.ObjectId, OpenMode.ForRead); // Get position Point3d position = GetEntityPosition(entity); // Store position originalPositions[entity.ObjectId] = position; // If it's a block reference, store additional information if (entity is BlockReference blockRef) { // Store transformation matrix originalBlockTransforms[blockRef.ObjectId] = blockRef.BlockTransform; // Store attribute values Dictionary attributes = new Dictionary(); foreach (ObjectId attId in blockRef.AttributeCollection) { if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) { attributes[attRef.Tag] = attRef.TextString; } } originalBlockAttributes[blockRef.ObjectId] = attributes; // Log event with block name LogMessage($"Block added: {blockRef.Name} at ({position.X:F2}, {position.Y:F2}, {position.Z:F2})"); // Send block-specific event data to server var blockData = new { EventType = "BlockAdded", ObjectId = blockRef.ObjectId.Handle.ToString(), BlockName = blockRef.Name, Position = new { X = position.X, Y = position.Y, Z = position.Z }, Scale = new { X = blockRef.ScaleFactors.X, Y = blockRef.ScaleFactors.Y, Z = blockRef.ScaleFactors.Z }, Rotation = blockRef.Rotation, Attributes = attributes, Timestamp = DateTime.UtcNow }; SendToServer(blockData); } else { // Log event for non-block entities LogMessage($"Object added: {entity.GetType().Name} at ({position.X:F2}, {position.Y:F2}, {position.Z:F2})"); // Send event data to server var data = new { EventType = "Added", ObjectId = entity.ObjectId.Handle.ToString(), ObjectType = entity.GetType().Name, Position = new { X = position.X, Y = position.Y, Z = position.Z }, Timestamp = DateTime.UtcNow }; SendToServer(data); } // Commit transaction tr.Commit(); } } catch (global::System.Exception ex) { LogMessage($"Error tracking new object: {ex.Message}"); } } /// /// Event handler for object erasure /// private static void OnObjectErased(object sender, ObjectErasedEventArgs e) { try { // Check if object is an entity if (e.DBObject is not Entity entity) { return; } // If we have been tracking this object if (originalPositions.ContainsKey(entity.ObjectId)) { // Get the last known position Point3d lastPosition = originalPositions[entity.ObjectId]; // Remove from tracking originalPositions.Remove(entity.ObjectId); // Special handling for blocks if (entity is BlockReference blockRef) { // Get block information string blockName = blockRef.Name; // Get attribute data if we have it Dictionary attributes = new Dictionary(); if (originalBlockAttributes.ContainsKey(blockRef.ObjectId)) { attributes = originalBlockAttributes[blockRef.ObjectId]; originalBlockAttributes.Remove(blockRef.ObjectId); } // Remove from block transform tracking originalBlockTransforms.Remove(blockRef.ObjectId); // Log event LogMessage($"Block erased: {blockName} from ({lastPosition.X:F2}, {lastPosition.Y:F2}, {lastPosition.Z:F2})"); // Send block-specific event data var blockData = new { EventType = "BlockErased", ObjectId = blockRef.ObjectId.Handle.ToString(), BlockName = blockName, LastPosition = new { X = lastPosition.X, Y = lastPosition.Y, Z = lastPosition.Z }, Attributes = attributes, Timestamp = DateTime.UtcNow }; SendToServer(blockData); } else { // Log event for non-block entities LogMessage($"Object erased: {entity.GetType().Name} from ({lastPosition.X:F2}, {lastPosition.Y:F2}, {lastPosition.Z:F2})"); // Send event data to server var data = new { EventType = "Erased", ObjectId = entity.ObjectId.Handle.ToString(), ObjectType = entity.GetType().Name, LastPosition = new { X = lastPosition.X, Y = lastPosition.Y, Z = lastPosition.Z }, Timestamp = DateTime.UtcNow }; SendToServer(data); } } } catch (global::System.Exception ex) { LogMessage($"Error tracking erased object: {ex.Message}"); } } /// /// Event handler for detecting modifications including stretch operations /// private static void OnObjectModified(object sender, ObjectEventArgs e) { try { // Skip if operation rate is too high if (IsOperationRateTooHigh()) { return; } // Check if object is an entity and has been tracked if (e.DBObject is not Entity entity || !originalPositions.ContainsKey(entity.ObjectId)) { return; } // Get the active document Document? doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) return; using (var tr = doc.TransactionManager.StartTransaction()) { // Open the entity for read if (tr.GetObject(entity.ObjectId, OpenMode.ForRead) is not Entity openEntity) { tr.Commit(); return; } // Get original position data Point3d oldPosition = originalPositions[entity.ObjectId]; Point3d newPosition = GetEntityPosition(openEntity); // Skip if position hasn't changed significantly (within tolerance) if (oldPosition.DistanceTo(newPosition) < 0.001) { tr.Commit(); return; } // Special handling for blocks if (openEntity is BlockReference blockRef) { HandleBlockModification(blockRef, tr, oldPosition, newPosition); } else { // Check if this is a stretch operation by examining command history and geometry changes bool isStretch = false; string lastCommand = string.Empty; try { // Try to get current command string currentCommand = string.Empty; // Try to use CommandInProgress if available if (doc.CommandInProgress != null) { currentCommand = doc.CommandInProgress; } // Option 2: Alternative - check current command through Editor else if (doc.Editor != null) { var cmdMethodInfo = doc.Editor.GetType().GetMethod("GetCurrentCommand"); if (cmdMethodInfo != null) { var result = cmdMethodInfo.Invoke(doc.Editor, null); if (result != null) { currentCommand = result.ToString(); } } } // Detect stretch-like operations if (currentCommand.Contains("STRETCH") || currentCommand.Contains("GRIPS") || IsMostLikelyStretch(openEntity, oldPosition, newPosition)) { isStretch = true; } } catch (global::System.Exception ex) { // Log the error but continue LogMessage($"Error detecting stretch: {ex.Message}"); // Fall back to geometry-based detection only isStretch = IsMostLikelyStretch(openEntity, oldPosition, newPosition); } // Log position change if (isStretch) { LogMessage($"Object STRETCHED: {openEntity.GetType().Name} from " + $"({oldPosition.X:F2}, {oldPosition.Y:F2}, {oldPosition.Z:F2}) to " + $"({newPosition.X:F2}, {newPosition.Y:F2}, {newPosition.Z:F2})"); // Send stretch event data to server var data = new { EventType = "Stretched", ObjectId = openEntity.ObjectId.Handle.ToString(), ObjectType = openEntity.GetType().Name, OldPosition = new { X = oldPosition.X, Y = oldPosition.Y, Z = oldPosition.Z }, NewPosition = new { X = newPosition.X, Y = newPosition.Y, Z = newPosition.Z }, Command = lastCommand, Timestamp = DateTime.UtcNow }; SendToServer(data); } else { // Regular modification (non-stretch) LogMessage($"Object moved: {openEntity.GetType().Name} from " + $"({oldPosition.X:F2}, {oldPosition.Y:F2}, {oldPosition.Z:F2}) to " + $"({newPosition.X:F2}, {newPosition.Y:F2}, {newPosition.Z:F2})"); // Send regular modification data var data = new { EventType = "Modified", ObjectId = openEntity.ObjectId.Handle.ToString(), ObjectType = openEntity.GetType().Name, OldPosition = new { X = oldPosition.X, Y = oldPosition.Y, Z = oldPosition.Z }, NewPosition = new { X = newPosition.X, Y = newPosition.Y, Z = newPosition.Z }, Timestamp = DateTime.UtcNow }; SendToServer(data); } // Update stored position originalPositions[entity.ObjectId] = newPosition; } // Commit transaction tr.Commit(); } } catch (global::System.Exception ex) { LogMessage($"Error tracking modified object: {ex.Message}"); } } /// /// Handles block modifications including attribute changes and transformations /// private static void HandleBlockModification(BlockReference blockRef, Transaction tr, Point3d oldPosition, Point3d newPosition) { try { // Get block name string blockName = blockRef.Name; // Check if we have transform data for this block bool hasTransformData = originalBlockTransforms.TryGetValue(blockRef.ObjectId, out Matrix3d oldTransform); Matrix3d newTransform = blockRef.BlockTransform; // Determine if block was stretched, moved, rotated, or scaled bool isStretched = false; bool isRotated = false; bool isScaled = false; bool isMoved = !oldPosition.IsEqualTo(newPosition); // Extract scale and rotation from transforms Point3d oldScale = hasTransformData ? new Point3d( Math.Sqrt(oldTransform.CoordinateSystem3d.Xaxis.DotProduct(oldTransform.CoordinateSystem3d.Xaxis)), Math.Sqrt(oldTransform.CoordinateSystem3d.Yaxis.DotProduct(oldTransform.CoordinateSystem3d.Yaxis)), Math.Sqrt(oldTransform.CoordinateSystem3d.Zaxis.DotProduct(oldTransform.CoordinateSystem3d.Zaxis)) ) : new Point3d(1, 1, 1); Point3d newScale = new Point3d( Math.Sqrt(newTransform.CoordinateSystem3d.Xaxis.DotProduct(newTransform.CoordinateSystem3d.Xaxis)), Math.Sqrt(newTransform.CoordinateSystem3d.Yaxis.DotProduct(newTransform.CoordinateSystem3d.Yaxis)), Math.Sqrt(newTransform.CoordinateSystem3d.Zaxis.DotProduct(newTransform.CoordinateSystem3d.Zaxis)) ); // Compare scale values isScaled = Math.Abs(oldScale.X - newScale.X) > 0.001 || Math.Abs(oldScale.Y - newScale.Y) > 0.001 || Math.Abs(oldScale.Z - newScale.Z) > 0.001; // Extract rotation from transforms (simplified approach) double oldRotation = hasTransformData ? Math.Atan2(oldTransform.CoordinateSystem3d.Xaxis.Y, oldTransform.CoordinateSystem3d.Xaxis.X) : blockRef.Rotation; double newRotation = blockRef.Rotation; // Compare rotation isRotated = Math.Abs(oldRotation - newRotation) > 0.001; // Check for stretch by examining if some parts moved while others remained fixed isStretched = hasTransformData && isScaled && !isRotated && IsBlockStretched(blockRef, oldTransform, newTransform); // Get attribute changes Dictionary oldAttributes = originalBlockAttributes.ContainsKey(blockRef.ObjectId) ? originalBlockAttributes[blockRef.ObjectId] : new Dictionary(); Dictionary newAttributes = new Dictionary(); Dictionary attributeChanges = new Dictionary(); // Collect attribute data foreach (ObjectId attId in blockRef.AttributeCollection) { if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) { // Store new attribute newAttributes[attRef.Tag] = attRef.TextString; // Check if attribute changed if (oldAttributes.TryGetValue(attRef.Tag, out string oldValue) && oldValue != attRef.TextString) { attributeChanges[attRef.Tag] = new { OldValue = oldValue, NewValue = attRef.TextString }; } } } // Log the appropriate event type string eventType; if (isStretched) eventType = "BlockStretched"; else if (isRotated) eventType = "BlockRotated"; else if (isScaled) eventType = "BlockScaled"; else if (isMoved) eventType = "BlockMoved"; else if (attributeChanges.Count > 0) eventType = "BlockAttributesChanged"; else eventType = "BlockModified"; // Log the event LogMessage($"{eventType}: {blockName} from ({oldPosition.X:F2}, {oldPosition.Y:F2}, {oldPosition.Z:F2}) to ({newPosition.X:F2}, {newPosition.Y:F2}, {newPosition.Z:F2})"); // Create event data object var blockData = new { EventType = eventType, ObjectId = blockRef.ObjectId.Handle.ToString(), BlockName = blockName, OldPosition = new { X = oldPosition.X, Y = oldPosition.Y, Z = oldPosition.Z }, NewPosition = new { X = newPosition.X, Y = newPosition.Y, Z = newPosition.Z }, OldRotation = oldRotation, NewRotation = newRotation, OldScale = new { X = oldScale.X, Y = oldScale.Y, Z = oldScale.Z }, NewScale = new { X = newScale.X, Y = newScale.Y, Z = newScale.Z }, AttributeChanges = attributeChanges.Count > 0 ? attributeChanges : null, Attributes = newAttributes, // Always include all current attributes Timestamp = DateTime.UtcNow }; // Send to server SendToServer(blockData); // Update stored data originalPositions[blockRef.ObjectId] = newPosition; originalBlockTransforms[blockRef.ObjectId] = newTransform; originalBlockAttributes[blockRef.ObjectId] = newAttributes; } catch (global::System.Exception ex) { LogMessage($"Error processing block modification: {ex.Message}"); } } /// /// Determines if a block has been stretched (parts moved while others remained fixed) /// private static bool IsBlockStretched(BlockReference blockRef, Matrix3d oldTransform, Matrix3d newTransform) { try { // Get the inverse of the old transform Matrix3d invOldTransform = oldTransform.Inverse(); // Compare non-uniform scaling as a sign of stretching Point3d oldScale = new Point3d( Math.Sqrt(oldTransform.CoordinateSystem3d.Xaxis.DotProduct(oldTransform.CoordinateSystem3d.Xaxis)), Math.Sqrt(oldTransform.CoordinateSystem3d.Yaxis.DotProduct(oldTransform.CoordinateSystem3d.Yaxis)), Math.Sqrt(oldTransform.CoordinateSystem3d.Zaxis.DotProduct(oldTransform.CoordinateSystem3d.Zaxis)) ); Point3d newScale = new Point3d( Math.Sqrt(newTransform.CoordinateSystem3d.Xaxis.DotProduct(newTransform.CoordinateSystem3d.Xaxis)), Math.Sqrt(newTransform.CoordinateSystem3d.Yaxis.DotProduct(newTransform.CoordinateSystem3d.Yaxis)), Math.Sqrt(newTransform.CoordinateSystem3d.Zaxis.DotProduct(newTransform.CoordinateSystem3d.Zaxis)) ); // If the scales in X/Y/Z vary differently, that's a stretch double scaleXDiff = Math.Abs(newScale.X / oldScale.X - 1.0); double scaleYDiff = Math.Abs(newScale.Y / oldScale.Y - 1.0); double scaleZDiff = Math.Abs(newScale.Z / oldScale.Z - 1.0); // If one dimension changed significantly more than others, it's likely a stretch if (Math.Abs(scaleXDiff - scaleYDiff) > 0.01 || Math.Abs(scaleXDiff - scaleZDiff) > 0.01 || Math.Abs(scaleYDiff - scaleZDiff) > 0.01) { return true; } return false; } catch { return false; } } /// /// Helper method to determine if a modification is likely a stretch /// private static bool IsMostLikelyStretch(Entity entity, Point3d oldPos, Point3d newPos) { try { // For polylines, check if some vertices moved while others stayed fixed if (entity is Polyline poly) { bool foundMovedVertex = false; bool foundFixedVertex = false; // Sample a few vertices to see if some moved and some stayed fixed for (int i = 0; i < poly.NumberOfVertices; i++) { Point3d oldVertexPos = poly.GetPoint3dAt(i); // Compare to original known positions (this would need enhancement for complete accuracy) // This is a simple approximation that detects if some vertices moved but not all double distance = oldPos.DistanceTo(oldVertexPos); double newDistance = newPos.DistanceTo(oldVertexPos); if (Math.Abs(distance - newDistance) > 0.001) { foundMovedVertex = true; } else { foundFixedVertex = true; } // If we found both moved and fixed vertices, it's likely a stretch if (foundMovedVertex && foundFixedVertex) { return true; } } } // For lines, we can check specific changes to endpoints else if (entity is Line line) { // Get the endpoints Point3d startPoint = line.StartPoint; Point3d endPoint = line.EndPoint; // Calculate distances between old position and both endpoints double oldStartDist = oldPos.DistanceTo(startPoint); double newStartDist = newPos.DistanceTo(startPoint); double oldEndDist = oldPos.DistanceTo(endPoint); double newEndDist = newPos.DistanceTo(endPoint); // If one endpoint's distance changed significantly more than the other, // it's likely one end was stretched while the other remained fixed return Math.Abs(oldStartDist - newStartDist) > 0.1 && Math.Abs(oldEndDist - newEndDist) < 0.01; } // For other entity types, we'd need specific implementation // Default to false for now return false; } catch { // If anything goes wrong in our stretch detection, default to false return false; } } /// /// Helper method to get entity position /// private static Point3d GetEntityPosition(Entity entity) { // Handle different entity types return entity switch { Line line => line.StartPoint, Circle circle => circle.Center, BlockReference blockRef => blockRef.Position, _ => entity.Bounds.Value.MinPoint }; } /// /// Helper method to write messages to the command line /// private static void LogMessage(string message) { try { Editor editor = Application.DocumentManager.MdiActiveDocument?.Editor; editor?.WriteMessage($"\n{message}"); } catch { // Ignore logging errors } } /// /// Command to manually clean up tracking data and improve performance /// [CommandMethod("TRACKCLEANUP")] public static void CleanupTracking() { try { // Only proceed if tracking is active if (!isTracking) { LogMessage("Change tracking is not active. No cleanup needed."); return; } // Get current memory usage before cleanup long beforeMemory = GC.GetTotalMemory(false); // Temporarily suspend timer if (scanTimer != null) { scanTimer.Enabled = false; } // Count entries before cleanup int positionsCount = originalPositions.Count; int transformsCount = originalBlockTransforms.Count; int attributesCount = originalBlockAttributes.Count; // Before removing, rebuild necessary tracking data Document doc = Application.DocumentManager.MdiActiveDocument; if (doc != null) { // Capture current state of all blocks as a clean baseline LogMessage("Rebuilding tracking data from current document state..."); CaptureCurrentBlockState(); } // Force garbage collection GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // Resume timer if (scanTimer != null && isTracking) { scanTimer.Enabled = true; } // Get memory after cleanup long afterMemory = GC.GetTotalMemory(false); // Report results LogMessage($"Tracking cleanup complete:"); LogMessage($"Memory before: {beforeMemory / 1024} KB, after: {afterMemory / 1024} KB"); LogMessage($"Memory freed: {(beforeMemory - afterMemory) / 1024} KB"); LogMessage($"Objects being tracked: {originalPositions.Count} (was {positionsCount})"); } catch (global::System.Exception ex) { LogMessage($"Error during tracking cleanup: {ex.Message}"); // Make sure timer is re-enabled if (scanTimer != null && isTracking) { scanTimer.Enabled = true; } } } /// /// Command to show tracking performance statistics /// [CommandMethod("TRACKSTATS")] public static void ShowTrackingStats() { try { if (!isTracking) { LogMessage("Change tracking is not active. No statistics available."); return; } // Get current memory usage long currentMemory = GC.GetTotalMemory(false); // Report statistics LogMessage("AutoCAD Change Tracker Statistics:"); LogMessage("-------------------------------"); LogMessage($"Memory usage: {currentMemory / 1024} KB"); LogMessage($"Scan interval: {scanIntervalMs} ms"); LogMessage($"Max operations per minute: {maxOperationsPerMinute}"); LogMessage($"Current operation count: {operationsCount}"); LogMessage($"Time since last reset: {(DateTime.Now - lastPerformanceCheck).TotalMinutes:F1} minutes"); LogMessage($"Event queue size: {eventQueue.Count} / {maxQueueSize}"); LogMessage($"Objects tracked:"); LogMessage($" - Positions: {originalPositions.Count}"); LogMessage($" - Block transforms: {originalBlockTransforms.Count}"); LogMessage($" - Block attributes: {originalBlockAttributes.Count}"); // If there's a significant mismatch in the dictionaries, suggest cleanup if (Math.Abs(originalPositions.Count - originalBlockAttributes.Count) > 50) { LogMessage("Note: Tracking data seems inconsistent. Consider running TRACKCLEANUP."); } } catch (global::System.Exception ex) { LogMessage($"Error showing tracking statistics: {ex.Message}"); } } /// /// Command to adjust max operations per minute /// [CommandMethod("TRACKTHROTTLE")] public static void SetOperationLimit() { try { Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) return; Editor ed = doc.Editor; // Prompt for new operation limit PromptIntegerOptions opts = new PromptIntegerOptions("\nEnter max operations per minute (50-1000):"); opts.AllowNegative = false; opts.AllowZero = false; opts.DefaultValue = maxOperationsPerMinute; opts.UseDefaultValue = true; PromptIntegerResult result = ed.GetInteger(opts); if (result.Status != PromptStatus.OK) return; // Validate and set new limit int newLimit = Math.Max(50, Math.Min(1000, result.Value)); maxOperationsPerMinute = newLimit; // Reset counters ResetPerformanceCounters(); LogMessage($"Operation throttle limit updated to {maxOperationsPerMinute} operations per minute."); } catch (global::System.Exception ex) { LogMessage($"Error setting operation limit: {ex.Message}"); } } /// /// Command to set server URL /// [CommandMethod("TRACKSERVER")] public static void SetServerUrl() { try { Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) return; Editor ed = doc.Editor; // Prompt for new server URL PromptStringOptions opts = new PromptStringOptions("\nEnter server URL:"); opts.DefaultValue = serverUrl; opts.UseDefaultValue = true; PromptResult result = ed.GetString(opts); if (result.Status != PromptStatus.OK) return; string newUrl = result.StringResult.Trim(); if (string.IsNullOrEmpty(newUrl)) { LogMessage("Server URL cannot be empty."); return; } // Update URL serverUrl = newUrl; LogMessage($"Server URL updated to: {serverUrl}"); // Test connection TestServerConnection(); } catch (global::System.Exception ex) { LogMessage($"Error setting server URL: {ex.Message}"); } } /// /// Test server connection /// private static async void TestServerConnection() { try { using var client = new HttpClient(); client.Timeout = TimeSpan.FromSeconds(5); // Ping the server with a test message var testData = new { EventType = "ConnectionTest", Timestamp = DateTime.UtcNow }; string json = JsonSerializer.Serialize(testData); var content = new StringContent(json, Encoding.UTF8, "application/json"); LogMessage("Testing server connection..."); var response = await client.PostAsync(serverUrl, content); if (response.IsSuccessStatusCode) { LogMessage("Server connection test successful."); } else { LogMessage($"Server connection test failed: {response.StatusCode}"); } } catch (global::System.Exception ex) { LogMessage($"Server connection test failed: {ex.Message}"); } } /// /// Command to view and modify all tracking settings in one place /// [CommandMethod("TRACKSETTINGS")] public static void ManageSettings() { try { Document doc = Application.DocumentManager.MdiActiveDocument; if (doc == null) return; Editor ed = doc.Editor; // Show current settings first LogMessage("--- AutoCAD Tracker Settings ---"); LogMessage($"Tracking status: {(isTracking ? "Active" : "Inactive")}"); LogMessage($"Scan interval: {scanIntervalMs} ms"); LogMessage($"Max operations per minute: {maxOperationsPerMinute}"); LogMessage($"Server URL: {serverUrl}"); LogMessage($"Event queue size: {eventQueue.Count}"); LogMessage(""); // Prompt for what to modify PromptKeywordOptions keyOpts = new PromptKeywordOptions("\nSelect setting to modify:"); keyOpts.Keywords.Add("Interval"); keyOpts.Keywords.Add("Operations"); keyOpts.Keywords.Add("Server"); keyOpts.Keywords.Add("Cleanup"); keyOpts.Keywords.Add("Status"); keyOpts.Keywords.Add("Cancel"); keyOpts.Keywords.Default = "Cancel"; PromptResult keyRes = ed.GetKeywords(keyOpts); if (keyRes.Status != PromptStatus.OK || keyRes.StringResult == "Cancel") return; switch (keyRes.StringResult) { case "Interval": SetScanInterval(); break; case "Operations": SetOperationLimit(); break; case "Server": SetServerUrl(); break; case "Cleanup": CleanupTracking(); break; case "Status": ShowTrackingStats(); break; } } catch (global::System.Exception ex) { LogMessage($"Error managing settings: {ex.Message}"); } } /// /// Command to show help information about tracker commands /// [CommandMethod("TRACKHELP")] public static void ShowHelp() { LogMessage("--- AutoCAD Change Tracker Commands ---"); LogMessage("TRACKSTART - Start change tracking"); LogMessage("TRACKSTOP - Stop change tracking"); LogMessage("TRACKSETTINGS - View and modify tracking settings"); LogMessage("TRACKINTERVAL - Set scan interval (milliseconds)"); LogMessage("TRACKTHROTTLE - Set max operations per minute"); LogMessage("TRACKSERVER - Set server URL"); LogMessage("TRACKSTATS - Show tracking statistics"); LogMessage("TRACKCLEANUP - Clean up tracking data to free memory"); LogMessage("TRACKHELP - Show this help information"); LogMessage(""); LogMessage("For performance issues:"); LogMessage("1. Increase scan interval (TRACKINTERVAL) to 10000 or higher"); LogMessage("2. Reduce operations limit (TRACKTHROTTLE) to 100-200"); LogMessage("3. Run TRACKCLEANUP periodically during long sessions"); LogMessage("4. For large drawings, stop tracking when not needed"); } } }