AutoCadServer/AutoCadTrack.cs
2025-05-16 18:17:54 +04:00

1649 lines
68 KiB
C#

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
{
/// <summary>
/// Main command class for the AutoCAD Change Tracker
/// </summary>
public class Commands
{
// Store original positions of objects
private static Dictionary<ObjectId, Point3d> originalPositions = new Dictionary<ObjectId, Point3d>();
// Store original transformation matrices for blocks
private static Dictionary<ObjectId, Matrix3d> originalBlockTransforms = new Dictionary<ObjectId, Matrix3d>();
// Store original attribute values for blocks
private static Dictionary<ObjectId, Dictionary<string, string>> originalBlockAttributes = new Dictionary<ObjectId, Dictionary<string, string>>();
// 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<object> eventQueue = new Queue<object>();
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";
/// <summary>
/// Command to start monitoring changes
/// </summary>
[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}");
}
}
/// <summary>
/// Command to stop monitoring changes
/// </summary>
[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}");
}
}
/// <summary>
/// New command to adjust scan interval
/// </summary>
[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}");
}
}
/// <summary>
/// Cleanup tracking data to free memory
/// </summary>
private static void CleanupTrackingData()
{
// Clear all tracking dictionaries
originalPositions.Clear();
originalBlockTransforms.Clear();
originalBlockAttributes.Clear();
// Force garbage collection
GC.Collect();
GC.WaitForPendingFinalizers();
}
/// <summary>
/// Reset performance counters
/// </summary>
private static void ResetPerformanceCounters()
{
lastPerformanceCheck = DateTime.Now;
operationsCount = 0;
}
/// <summary>
/// Check if operation rate is too high to perform additional operations
/// </summary>
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;
}
/// <summary>
/// Start background thread for sending events to server
/// </summary>
private static void StartSenderThread()
{
if (senderThread != null) return;
keepSenderRunning = true;
senderThread = new System.Threading.Thread(ProcessEventQueue)
{
IsBackground = true,
Name = "AutoCAD_EventSender"
};
senderThread.Start();
}
/// <summary>
/// Stop background sender thread
/// </summary>
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;
}
/// <summary>
/// Background thread method for processing event queue
/// </summary>
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);
}
}
}
/// <summary>
/// Helper method to enqueue event for background sending
/// </summary>
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}");
}
}
/// <summary>
/// Async method to actually send data to server
/// </summary>
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}");
}
}
/// <summary>
/// Event handler for command start
/// </summary>
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}");
}
}
/// <summary>
/// Event handler for command completion - used to detect attribute edits
/// </summary>
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}");
}
}
/// <summary>
/// Helper for delayed attribute scanning
/// </summary>
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}");
}
}
/// <summary>
/// Event handler for command cancellation
/// </summary>
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;
}
}
/// <summary>
/// Helper to check if a command is an attribute editing command
/// </summary>
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";
}
/// <summary>
/// Scans all blocks to check for attribute changes
/// </summary>
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<string, string> oldAttributes = originalBlockAttributes[objId];
// Get the current attributes
Dictionary<string, string> newAttributes = new Dictionary<string, string>();
Dictionary<string, object> attributeChanges = new Dictionary<string, object>();
// 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}");
}
}
/// <summary>
/// Captures the current state of all blocks in the drawing
/// </summary>
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<string, string> attributes = new Dictionary<string, string>();
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}");
}
}
/// <summary>
/// Event handler for object additions
/// </summary>
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<string, string> attributes = new Dictionary<string, string>();
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}");
}
}
/// <summary>
/// Event handler for object erasure
/// </summary>
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<string, string> attributes = new Dictionary<string, string>();
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}");
}
}
/// <summary>
/// Event handler for detecting modifications including stretch operations
/// </summary>
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}");
}
}
/// <summary>
/// Handles block modifications including attribute changes and transformations
/// </summary>
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<string, string> oldAttributes = originalBlockAttributes.ContainsKey(blockRef.ObjectId)
? originalBlockAttributes[blockRef.ObjectId]
: new Dictionary<string, string>();
Dictionary<string, string> newAttributes = new Dictionary<string, string>();
Dictionary<string, object> attributeChanges = new Dictionary<string, object>();
// 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}");
}
}
/// <summary>
/// Determines if a block has been stretched (parts moved while others remained fixed)
/// </summary>
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;
}
}
/// <summary>
/// Helper method to determine if a modification is likely a stretch
/// </summary>
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;
}
}
/// <summary>
/// Helper method to get entity position
/// </summary>
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
};
}
/// <summary>
/// Helper method to write messages to the command line
/// </summary>
private static void LogMessage(string message)
{
try
{
Editor editor = Application.DocumentManager.MdiActiveDocument?.Editor;
editor?.WriteMessage($"\n{message}");
}
catch
{
// Ignore logging errors
}
}
/// <summary>
/// Command to manually clean up tracking data and improve performance
/// </summary>
[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;
}
}
}
/// <summary>
/// Command to show tracking performance statistics
/// </summary>
[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}");
}
}
/// <summary>
/// Command to adjust max operations per minute
/// </summary>
[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}");
}
}
/// <summary>
/// Command to set server URL
/// </summary>
[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}");
}
}
/// <summary>
/// Test server connection
/// </summary>
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}");
}
}
/// <summary>
/// Command to view and modify all tracking settings in one place
/// </summary>
[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}");
}
}
/// <summary>
/// Command to show help information about tracker commands
/// </summary>
[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");
}
}
}