1649 lines
68 KiB
C#
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");
|
|
}
|
|
}
|
|
} |