first commit

This commit is contained in:
ilia gurielidze 2025-05-16 18:17:54 +04:00
commit 99ed39df07
16 changed files with 6269 additions and 0 deletions

1
.cursorignore Normal file
View File

@ -0,0 +1 @@
node_modules/

3
.env Normal file
View File

@ -0,0 +1,3 @@
PORT=3000
# Uncomment and set this if you want to use MongoDB
# MONGODB_URI=mongodb://localhost:27017/autocad_tracker

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

1649
AutoCadTrack.cs Normal file

File diff suppressed because it is too large Load Diff

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# AutoCAD Server
A simple Node.js server to receive and store events from an AutoCAD plugin.
## Features
- Receives object creation and modification events from AutoCAD
- Stores events in memory (with option to use MongoDB)
- Provides API endpoints to retrieve stored events
- Simple setup and configuration
## Setup
1. Clone this repository
2. Install dependencies:
```
npm install
```
3. Configure environment variables in `.env` file (optional)
4. Start the server:
```
node server.js
```
## Usage
The server provides the following endpoints:
- `POST /api/autocad-events`: Endpoint for receiving events from AutoCAD
- `GET /api/autocad-events`: Retrieve all stored events
## AutoCAD Plugin Configuration
Update the `ServerEndpoint` variable in your AutoCAD plugin to point to this server:
```csharp
private static readonly string ServerEndpoint = "http://localhost:3000/api/autocad-events";
```
## Using with MongoDB (Optional)
To store events in MongoDB instead of memory:
1. Uncomment the MongoDB connection code in `server.js`
2. Set the `MONGODB_URI` in the `.env` file
3. Uncomment the MongoDB code in the controller
## License
ISC

View File

@ -0,0 +1,33 @@
// Optional: If you want to use MongoDB later
// const AutocadEvent = require('../models/AutocadEvent');
// Handle incoming AutoCAD events
exports.receiveEvent = (req, res) => {
try {
const eventData = req.body;
// Log received data
console.log('Received AutoCAD event:', JSON.stringify(eventData, null, 2));
// Store in our simple in-memory storage
global.autocadEvents.push({
...eventData,
receivedAt: new Date()
});
// Optional: Store in MongoDB
// const event = new AutocadEvent(eventData);
// await event.save();
// Send success response
res.status(200).json({ message: 'Event received successfully' });
} catch (error) {
console.error('Error processing AutoCAD event:', error);
res.status(500).json({ message: 'Error processing event', error: error.message });
}
};
// Get all stored events (for demo/debugging)
exports.getAllEvents = (req, res) => {
res.json(global.autocadEvents);
};

File diff suppressed because it is too large Load Diff

39
models/AutocadEvent.js Normal file
View File

@ -0,0 +1,39 @@
const mongoose = require('mongoose');
// Define schema for position data
const positionSchema = new mongoose.Schema({
X: Number,
Y: Number,
Z: Number
}, { _id: false });
// Define schema for AutoCAD events
const autocadEventSchema = new mongoose.Schema({
EventType: {
type: String,
required: true,
enum: ['Added', 'Modified', 'Deleted', 'Stretched']
},
ObjectId: {
type: String,
required: true
},
ObjectType: {
type: String,
required: true
},
Position: positionSchema,
OldPosition: positionSchema,
NewPosition: positionSchema,
Command: String, // Store command information
Timestamp: {
type: Date,
required: true
},
receivedAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('AutocadEvent', autocadEventSchema);

1594
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "autocadserver",
"version": "1.0.0",
"description": "Server for receiving AutoCAD change events",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"autocad",
"server",
"event-tracking"
],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^7.4.3",
"node-fetch": "^2.7.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

225
public/css/style.css Normal file
View File

@ -0,0 +1,225 @@
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background: #f9f9f9;
}
h1 {
color: #333;
}
.container {
display: flex;
flex-direction: row;
}
.block-list {
width: 250px;
margin-right: 20px;
}
.block-item {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
border-left: 4px solid #2196F3;
transition: all 0.3s ease;
}
.block-item:hover {
background: #e0e0e0;
}
.block-item.active {
background: #e3f2fd;
}
.block-item.recently-updated {
animation: pulse 1.5s;
border-left-color: #e91e63;
}
@keyframes pulse {
0% { background-color: #f5f5f5; }
50% { background-color: #ffcdd2; }
100% { background-color: #f5f5f5; }
}
.timeline-view {
flex: 1;
}
.timeline-item {
border-left: 2px solid #2196F3;
padding: 10px;
margin: 10px 0 10px 20px;
position: relative;
}
.timeline-item:before {
content: '';
position: absolute;
width: 12px;
height: 12px;
background: #2196F3;
border-radius: 50%;
left: -7px;
top: 10px;
}
.timeline-item.attribute-edit {
border-left-color: #e91e63;
}
.timeline-item.attribute-edit:before {
background: #e91e63;
}
.timeline-item.moved {
border-left-color: #4caf50;
}
.timeline-item.moved:before {
background: #4caf50;
}
.timeline-item.scaled {
border-left-color: #ff9800;
}
.timeline-item.scaled:before {
background: #ff9800;
}
.timeline-item.stretched {
border-left-color: #9c27b0;
}
.timeline-item.stretched:before {
background: #9c27b0;
}
.timeline-item.rotated {
border-left-color: #795548;
}
.timeline-item.rotated:before {
background: #795548;
}
.timeline-time {
color: #666;
font-size: 0.8em;
}
.action-type {
font-weight: bold;
margin-right: 10px;
}
.block-action-details {
margin-top: 5px;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.timeline-container {
max-height: 600px;
overflow-y: auto;
padding-right: 10px;
}
.block-info {
margin-bottom: 15px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.changes {
color: #e91e63;
font-weight: bold;
}
.current {
color: #4caf50;
font-weight: bold;
}
.initial {
color: #2196F3;
font-weight: bold;
}
.attribute {
color: #333;
}
.attribute-section {
border-top: 1px solid #ddd;
margin-top: 8px;
padding-top: 8px;
}
.attribute-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
}
@media (min-width: 768px) {
.attribute-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.collapsible {
cursor: pointer;
padding: 5px 0;
width: 100%;
border: none;
text-align: left;
outline: none;
background-color: transparent;
font-weight: bold;
position: relative;
}
.collapsible:after {
content: '+';
font-weight: bold;
float: right;
margin-left: 5px;
}
.active-collapse:after {
content: "-";
}
.collapse-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
.notification {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
background-color: #4caf50;
color: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.3s;
z-index: 1000;
}
.notification.show {
opacity: 1;
}

103
public/index.html Normal file
View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AutoCAD Block Timeline</title>
<link rel="stylesheet" href="css/style.css">
<style>
.connection-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #f5f5f5;
padding: 8px 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #ddd;
z-index: 100;
}
.status-indicator {
display: inline-block;
padding: 4px 8px;
margin-right: 10px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
}
.status-indicator.connected {
background-color: #4CAF50;
color: white;
}
.status-indicator.connecting {
background-color: #2196F3;
color: white;
}
.status-indicator.disconnected {
background-color: #F44336;
color: white;
}
.status-indicator.waiting {
background-color: #FF9800;
color: white;
}
.status-indicator.failed {
background-color: #9E9E9E;
color: white;
}
#reconnect-button {
border: none;
background-color: #2196F3;
color: white;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
display: none;
}
#reconnect-button:hover {
background-color: #0b7dda;
}
/* Add padding to body to make room for the connection bar */
body {
padding-top: 40px;
}
</style>
</head>
<body>
<div class="connection-bar">
<div id="connection-status" class="status-indicator">Not connected</div>
<button id="reconnect-button">Reconnect</button>
</div>
<h1>AutoCAD Block Timeline</h1>
<div class="container">
<div class="block-list">
<h2>Blocks</h2>
<div id="blocks"></div>
</div>
<div class="timeline-view">
<div id="blockInfo" class="block-info"></div>
<div id="blockTimeline" class="timeline-container"></div>
</div>
</div>
<div id="notification" class="notification">
<span id="notification-message"></span>
</div>
<script src="js/app.js"></script>
</body>
</html>

532
public/js/app.js Normal file
View File

@ -0,0 +1,532 @@
// Store the current blocks for comparison
window.currentBlocks = {};
window.selectedBlockId = null;
window.eventSource = null;
window.maxReconnectAttempts = 5;
window.reconnectAttempts = 0;
window.reconnectTimeout = null;
window.serverConnected = false;
// Fetch blocks on page load
window.onload = function() {
fetchBlocks();
setupSSEConnection();
// Add manual reconnect button listeners
const reconnectButton = document.getElementById('reconnect-button');
if (reconnectButton) {
reconnectButton.addEventListener('click', function() {
window.reconnectAttempts = 0;
setupSSEConnection();
});
} else {
console.error('Reconnect button not found in DOM');
}
};
// Set up SSE connection for real-time updates
function setupSSEConnection() {
// Clear any existing SSE connection
if (window.eventSource) {
window.eventSource.close();
window.eventSource = null;
}
// Clear any pending reconnect timeout
if (window.reconnectTimeout) {
clearTimeout(window.reconnectTimeout);
window.reconnectTimeout = null;
}
// Update connection status UI
updateConnectionStatus('connecting');
// First check if server is reachable before attempting SSE connection
fetch('/api/server-status')
.then(response => {
if (!response.ok) {
throw new Error('Server returned status: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('Server is online:', data);
// If server check succeeds, now establish SSE connection
connectEventSource();
})
.catch(error => {
console.error('Server status check failed:', error);
updateConnectionStatus('failed');
// Only attempt to reconnect if we haven't exceeded max attempts
if (window.reconnectAttempts < window.maxReconnectAttempts) {
window.reconnectAttempts++;
// Increase delay with each attempt (exponential backoff)
const delay = Math.min(30000, 1000 * Math.pow(2, window.reconnectAttempts));
console.log(`Reconnect attempt ${window.reconnectAttempts}/${window.maxReconnectAttempts} in ${delay/1000} seconds`);
updateConnectionStatus('waiting', delay/1000);
// Try to reconnect after a delay
window.reconnectTimeout = setTimeout(() => {
setupSSEConnection();
}, delay);
} else {
console.log('Max reconnect attempts reached. Please reconnect manually.');
updateConnectionStatus('failed');
}
});
}
// Function to establish the actual EventSource connection
function connectEventSource() {
try {
// Set up new SSE connection
window.eventSource = new EventSource('/api/sse');
// Handle connection event
window.eventSource.addEventListener('connected', function(e) {
console.log('SSE Connection established');
window.reconnectAttempts = 0;
window.serverConnected = true;
updateConnectionStatus('connected');
});
// Handle real-time events
window.eventSource.addEventListener('new-event', function(e) {
try {
const newEvent = JSON.parse(e.data);
// Check if this is a block event
if (newEvent.EventType && newEvent.EventType.startsWith('Block')) {
console.log('Received real-time update:', newEvent);
// Always refresh the blocks list to catch new/updated blocks
fetchBlocks();
// Show notification
showNotification(`${newEvent.BlockName}: ${newEvent.EventType}`);
// If this event is for the currently selected block, update its timeline
if (window.selectedBlockId && newEvent.ObjectId === window.selectedBlockId) {
fetchBlockEvents(window.selectedBlockId);
}
}
} catch (error) {
console.error('Error processing real-time event:', error);
}
});
// Handle connection errors
window.eventSource.addEventListener('error', function(e) {
console.error('SSE connection error:', e);
window.serverConnected = false;
// Close the connection
if (window.eventSource) {
window.eventSource.close();
window.eventSource = null;
}
// Update connection status
updateConnectionStatus('disconnected');
// Only attempt to reconnect if we haven't exceeded max attempts
if (window.reconnectAttempts < window.maxReconnectAttempts) {
window.reconnectAttempts++;
// Increase delay with each attempt (exponential backoff)
const delay = Math.min(30000, 1000 * Math.pow(2, window.reconnectAttempts));
console.log(`Reconnect attempt ${window.reconnectAttempts}/${window.maxReconnectAttempts} in ${delay/1000} seconds`);
updateConnectionStatus('waiting', delay/1000);
// Try to reconnect after a delay
window.reconnectTimeout = setTimeout(() => {
setupSSEConnection();
}, delay);
} else {
console.log('Max reconnect attempts reached. Please reconnect manually.');
updateConnectionStatus('failed');
}
});
} catch (error) {
console.error('Error setting up SSE connection:', error);
updateConnectionStatus('failed');
}
// When leaving the page, close the connection
window.addEventListener('beforeunload', function() {
if (window.eventSource) {
window.eventSource.close();
window.eventSource = null;
}
});
}
// Function to update connection status UI
function updateConnectionStatus(status, seconds) {
const statusElement = document.getElementById('connection-status');
const reconnectButton = document.getElementById('reconnect-button');
if (!statusElement) return;
statusElement.className = 'status-indicator ' + status;
switch(status) {
case 'connected':
statusElement.textContent = 'Connected';
reconnectButton.style.display = 'none';
break;
case 'connecting':
statusElement.textContent = 'Connecting...';
reconnectButton.style.display = 'none';
break;
case 'disconnected':
statusElement.textContent = 'Disconnected';
reconnectButton.style.display = 'inline-block';
break;
case 'waiting':
statusElement.textContent = `Reconnecting in ${seconds}s`;
reconnectButton.style.display = 'inline-block';
break;
case 'failed':
statusElement.textContent = 'Connection failed';
reconnectButton.style.display = 'inline-block';
break;
default:
statusElement.textContent = 'Unknown status';
reconnectButton.style.display = 'inline-block';
}
}
// Fetch all blocks
function fetchBlocks() {
fetch('/api/blocks')
.then(response => response.json())
.then(blocks => {
const blocksDiv = document.getElementById('blocks');
// Save currently selected block ID
const currentlySelectedId = window.selectedBlockId;
// Store new blocks list but keep track of which ones are new/updated
const updatedBlocks = {};
// Prepare to rebuild the blocks list
blocksDiv.innerHTML = '';
if (blocks.length === 0) {
blocksDiv.innerHTML = '<p>No blocks found</p>';
return;
}
blocks.forEach(block => {
const blockDiv = document.createElement('div');
blockDiv.className = 'block-item';
// Check if this block was recently updated
const existingBlock = window.currentBlocks[block.id];
const isUpdated = existingBlock &&
(existingBlock.lastEventType !== block.lastEventType ||
existingBlock.timestamp !== block.timestamp);
// Check if this is a new block (not in currentBlocks)
const isNew = !window.currentBlocks[block.id];
if (isUpdated) {
blockDiv.classList.add('recently-updated');
}
if (isNew) {
blockDiv.classList.add('recently-updated');
showNotification(`New block detected: ${block.name}`);
}
// Mark as active if this was the selected block
if (currentlySelectedId && currentlySelectedId === block.id) {
blockDiv.classList.add('active');
}
blockDiv.dataset.id = block.id;
blockDiv.onclick = () => {
// Set active class
document.querySelectorAll('.block-item').forEach(item => {
item.classList.remove('active');
});
blockDiv.classList.add('active');
// Store selected block ID
window.selectedBlockId = block.id;
// Load timeline
loadBlockTimeline(block.id, block.name);
};
blockDiv.innerHTML = `
<strong>${block.name}</strong> (ID: ${block.id})
<small style="display: block; margin-top: 4px; color: #666;">Last update: ${timeAgo(new Date(block.timestamp))}</small>
`;
blocksDiv.appendChild(blockDiv);
// Store this block for future comparison
updatedBlocks[block.id] = block;
});
// Update the stored blocks list
window.currentBlocks = updatedBlocks;
})
.catch(error => console.error('Error fetching blocks:', error));
}
// Helper function to format time ago
function timeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);
let interval = Math.floor(seconds / 31536000);
if (interval > 1) return interval + ' years ago';
if (interval === 1) return 'a year ago';
interval = Math.floor(seconds / 2592000);
if (interval > 1) return interval + ' months ago';
if (interval === 1) return 'a month ago';
interval = Math.floor(seconds / 86400);
if (interval > 1) return interval + ' days ago';
if (interval === 1) return 'a day ago';
interval = Math.floor(seconds / 3600);
if (interval > 1) return interval + ' hours ago';
if (interval === 1) return 'an hour ago';
interval = Math.floor(seconds / 60);
if (interval > 1) return interval + ' minutes ago';
if (interval === 1) return 'a minute ago';
if (seconds < 10) return 'just now';
return Math.floor(seconds) + ' seconds ago';
}
// Load block timeline
function loadBlockTimeline(blockId, blockName) {
// Display block info
const blockInfo = document.getElementById('blockInfo');
blockInfo.innerHTML = `
<h3>${blockName}</h3>
<p>Block ID: ${blockId}</p>
<p><small>Real-time updates enabled</small></p>
`;
// Load block events
fetchBlockEvents(blockId);
}
// Fetch block events
function fetchBlockEvents(blockId) {
// Track current event count
const currentEvents = document.querySelectorAll('.timeline-item').length;
// Fetch block events
fetch('/api/events')
.then(response => response.json())
.then(events => {
// Filter events for this block
const blockEvents = events.filter(event =>
event.ObjectId === blockId && event.EventType && event.EventType.startsWith('Block')
).sort((a, b) => new Date(a.Timestamp) - new Date(b.Timestamp));
// Check if we have new events
if (blockEvents.length > currentEvents && currentEvents > 0) {
showNotification(`${blockEvents.length - currentEvents} new event(s) received`);
}
const timelineDiv = document.getElementById('blockTimeline');
// Store the scroll position
const scrollPosition = timelineDiv.scrollTop;
// Clear current timeline
timelineDiv.innerHTML = '';
if (blockEvents.length === 0) {
timelineDiv.innerHTML = '<p>No events recorded for this block</p>';
return;
}
// Process events
blockEvents.forEach(event => {
const date = new Date(event.Timestamp);
// Create timeline item
const timelineItem = document.createElement('div');
// Set appropriate class based on event type
let eventClass = '';
let actionLabel = '';
switch(event.EventType) {
case 'BlockAttributesChanged':
eventClass = 'attribute-edit';
actionLabel = 'Attribute Edit';
break;
case 'BlockMoved':
eventClass = 'moved';
actionLabel = 'Moved';
break;
case 'BlockRotated':
eventClass = 'rotated';
actionLabel = 'Rotated';
break;
case 'BlockScaled':
eventClass = 'scaled';
actionLabel = 'Scaled';
break;
case 'BlockStretched':
eventClass = 'stretched';
actionLabel = 'Stretched';
break;
default:
eventClass = '';
actionLabel = event.EventType.replace('Block', '');
}
timelineItem.className = `timeline-item ${eventClass}`;
// Create content for timeline item
let timelineContent = `
<div class="timeline-time">${date.toLocaleString()}</div>
<span class="action-type">${actionLabel}</span>
`;
// Add position information - always show current position
let position = event.NewPosition || event.Position;
if (position) {
timelineContent += `
<div class="block-action-details">
<strong>Position:</strong> (${position.X.toFixed(2)}, ${position.Y.toFixed(2)}, ${position.Z.toFixed(2)})
`;
// Add old position if this was a move event
if (event.EventType === 'BlockMoved' && event.OldPosition) {
timelineContent += `
<br><strong>Old Position:</strong> (${event.OldPosition.X.toFixed(2)}, ${event.OldPosition.Y.toFixed(2)}, ${event.OldPosition.Z.toFixed(2)})
`;
}
timelineContent += '</div>';
}
// Add attributes - always show all attributes
if (event.Attributes && Object.keys(event.Attributes).length > 0) {
timelineContent += '<div class="block-action-details">';
timelineContent += '<button class="collapsible">Attributes</button>';
timelineContent += '<div class="collapse-content">';
timelineContent += '<div class="attribute-grid">';
// Sort the attributes by key for consistent display
const sortedAttributes = Object.keys(event.Attributes).sort().reduce(
(obj, key) => {
obj[key] = event.Attributes[key];
return obj;
},
{}
);
for (const [key, value] of Object.entries(sortedAttributes)) {
// Check if this attribute was changed in this event
let attributeClass = 'attribute';
if (event.AttributeChanges && event.AttributeChanges[key]) {
attributeClass = 'changes';
}
timelineContent += `
<div>
<strong>${key}:</strong> <span class="${attributeClass}">${value}</span>
</div>
`;
}
timelineContent += '</div>'; // Close attribute-grid
timelineContent += '</div>'; // Close collapse-content
// If this was an attribute change, show what changed
if (event.EventType === 'BlockAttributesChanged' && event.AttributeChanges) {
timelineContent += '<div class="attribute-section"><strong>Changed Values:</strong>';
for (const [key, change] of Object.entries(event.AttributeChanges)) {
timelineContent += `
<div>
<strong>${key}:</strong>
<span class="initial">${change.OldValue}</span>
<span class="changes">${change.NewValue}</span>
</div>
`;
}
timelineContent += '</div>'; // Close attribute-section
}
timelineContent += '</div>'; // Close block-action-details
}
// Add other transformation details if available
if (event.EventType === 'BlockRotated' && event.OldRotation !== undefined && event.NewRotation !== undefined) {
timelineContent += `
<div class="block-action-details">
<strong>Rotation:</strong> ${(event.NewRotation * 180 / Math.PI).toFixed(2)}°
<br><strong>Old Rotation:</strong> ${(event.OldRotation * 180 / Math.PI).toFixed(2)}°
</div>
`;
} else if (event.EventType === 'BlockScaled' && event.OldScale && event.NewScale) {
timelineContent += `
<div class="block-action-details">
<strong>Scale:</strong> X=${event.NewScale.X.toFixed(3)}, Y=${event.NewScale.Y.toFixed(3)}, Z=${event.NewScale.Z.toFixed(3)}
<br><strong>Old Scale:</strong> X=${event.OldScale.X.toFixed(3)}, Y=${event.OldScale.Y.toFixed(3)}, Z=${event.OldScale.Z.toFixed(3)}
</div>
`;
}
timelineItem.innerHTML = timelineContent;
timelineDiv.appendChild(timelineItem);
});
// Restore scroll position
timelineDiv.scrollTop = scrollPosition;
// Initialize collapsible sections
activateCollapsibles();
})
.catch(error => {
document.getElementById('blockTimeline').innerHTML = `<p>Error: ${error.message}</p>`;
});
}
// Helper function to activate collapsible sections after refresh
function activateCollapsibles() {
const collapsibles = document.querySelectorAll('.collapsible');
collapsibles.forEach(collapsible => {
collapsible.addEventListener('click', function() {
this.classList.toggle('active-collapse');
const content = this.nextElementSibling;
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = content.scrollHeight + "px";
}
});
});
}
// Function to show notification
function showNotification(message) {
const notification = document.getElementById('notification-message');
notification.textContent = message;
const notificationDiv = document.getElementById('notification');
notificationDiv.className = 'notification show';
setTimeout(() => {
notificationDiv.className = 'notification';
}, 3000);
}

11
routes/autocadRoutes.js Normal file
View File

@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const autocadController = require('../controllers/autocadController');
// Route to receive AutoCAD events
router.post('/autocad-events', autocadController.receiveEvent);
// Route to get all stored events (for demo/debugging)
router.get('/autocad-events', autocadController.getAllEvents);
module.exports = router;

712
server.js Normal file
View File

@ -0,0 +1,712 @@
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const dotenv = require('dotenv');
const autocadRoutes = require('./routes/autocadRoutes');
const fs = require('fs');
const path = require('path');
// Load environment variables
dotenv.config();
// Initialize express app
const app = express();
// Middleware
app.use(cors());
app.use(express.json({ limit: '10mb' })); // Increase payload limit
// Set timeout for requests
app.use((req, res, next) => {
res.setTimeout(30000, () => {
console.error('Request timeout');
res.status(408).send('Request Timeout');
});
next();
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).send('Internal Server Error');
});
// Serve static files from the public directory
app.use(express.static('public'));
// Create storage directory if it doesn't exist
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir);
}
// Initialize event storage by categories
global.autocadEvents = {
all: [],
blockEvents: {
added: [],
erased: [],
stretched: [],
rotated: [],
scaled: [],
moved: [],
attributesChanged: [],
modified: []
},
entityEvents: {
added: [],
erased: [],
stretched: [],
modified: []
}
};
// Add a store of SSE clients for real-time updates
const sseClients = [];
// Performance monitoring
let eventCount = 0;
let lastSaveTime = Date.now();
const MAX_EVENTS_IN_MEMORY = 10000; // Limit total events in memory
// Middleware to handle SSE connections
app.get('/api/sse', (req, res) => {
// Set headers for SSE
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Send initial connection established message
res.write('event: connected\n');
res.write('data: Connection established\n\n');
// Add this client to the list
const clientId = Date.now();
const newClient = {
id: clientId,
res
};
sseClients.push(newClient);
// Handle client disconnect
req.on('close', () => {
console.log(`SSE Client ${clientId} disconnected`);
const index = sseClients.findIndex(client => client.id === clientId);
if (index !== -1) {
sseClients.splice(index, 1);
}
});
});
// Function to notify all SSE clients of a new event
function notifyClientsOfNewEvent(event) {
const failedClients = [];
sseClients.forEach((client, index) => {
try {
client.res.write(`event: new-event\n`);
client.res.write(`data: ${JSON.stringify(event)}\n\n`);
} catch (error) {
console.error(`Error sending to client ${client.id}:`, error);
failedClients.push(index);
}
});
// Remove failed clients (in reverse order to avoid index issues)
failedClients.reverse().forEach(index => {
console.log(`Removing failed SSE client ${sseClients[index].id}`);
sseClients.splice(index, 1);
});
}
// Routes
app.use('/api', autocadRoutes);
// Add connection test endpoint
app.post('/api/connection-test', (req, res) => {
console.log('Connection test received');
res.status(200).json({ success: true, message: 'Connection successful', serverTime: new Date().toISOString() });
});
// Add custom event handling middleware
app.post('/api/events', (req, res) => {
try {
const eventData = req.body;
// Handle connection test events specially
if (eventData.EventType === 'ConnectionTest') {
console.log('AutoCAD connection test received');
return res.status(200).json({
success: true,
message: 'Connection test successful',
serverTime: new Date().toISOString(),
eventsCount: global.autocadEvents.all.length
});
}
// Add timestamp if not provided
if (!eventData.timestamp) {
eventData.timestamp = new Date().toISOString();
}
// Store in global all events array
global.autocadEvents.all.push(eventData);
eventCount++;
// Limit memory usage by removing old events if we exceed MAX_EVENTS_IN_MEMORY
if (global.autocadEvents.all.length > MAX_EVENTS_IN_MEMORY) {
const excess = global.autocadEvents.all.length - MAX_EVENTS_IN_MEMORY;
// Remove oldest events
const removed = global.autocadEvents.all.splice(0, excess);
console.log(`Memory limit reached: Removed ${removed.length} old events`);
// Also remove from specific event arrays
for (const category of Object.keys(global.autocadEvents.blockEvents)) {
if (global.autocadEvents.blockEvents[category].length > MAX_EVENTS_IN_MEMORY / 10) {
const categoryExcess = global.autocadEvents.blockEvents[category].length - (MAX_EVENTS_IN_MEMORY / 10);
global.autocadEvents.blockEvents[category].splice(0, categoryExcess);
}
}
for (const category of Object.keys(global.autocadEvents.entityEvents)) {
if (global.autocadEvents.entityEvents[category].length > MAX_EVENTS_IN_MEMORY / 10) {
const categoryExcess = global.autocadEvents.entityEvents[category].length - (MAX_EVENTS_IN_MEMORY / 10);
global.autocadEvents.entityEvents[category].splice(0, categoryExcess);
}
}
}
// Categorize by event type
const eventType = eventData.EventType;
// Handle block-specific events
if (eventType.startsWith('Block')) {
switch (eventType) {
case 'BlockAdded':
global.autocadEvents.blockEvents.added.push(eventData);
break;
case 'BlockErased':
global.autocadEvents.blockEvents.erased.push(eventData);
break;
case 'BlockStretched':
global.autocadEvents.blockEvents.stretched.push(eventData);
break;
case 'BlockRotated':
global.autocadEvents.blockEvents.rotated.push(eventData);
break;
case 'BlockScaled':
global.autocadEvents.blockEvents.scaled.push(eventData);
break;
case 'BlockMoved':
global.autocadEvents.blockEvents.moved.push(eventData);
break;
case 'BlockAttributesChanged':
global.autocadEvents.blockEvents.attributesChanged.push(eventData);
break;
case 'BlockModified':
global.autocadEvents.blockEvents.modified.push(eventData);
break;
}
} else {
// Handle non-block entity events
switch (eventType) {
case 'Added':
global.autocadEvents.entityEvents.added.push(eventData);
break;
case 'Erased':
global.autocadEvents.entityEvents.erased.push(eventData);
break;
case 'Stretched':
global.autocadEvents.entityEvents.stretched.push(eventData);
break;
case 'Modified':
global.autocadEvents.entityEvents.modified.push(eventData);
break;
}
}
// Notify all connected clients of the new event
notifyClientsOfNewEvent(eventData);
// Periodically save events to disk (every 20 events or 5 minutes)
const currentTime = Date.now();
if (eventCount % 20 === 0 || (currentTime - lastSaveTime) > 300000) {
saveEventsToDisk();
lastSaveTime = currentTime;
}
// Enhanced logging based on event type
if (eventType.startsWith('Block')) {
// Basic log message with event type and block name
console.log(`Received ${eventType} event for ${eventData.BlockName}`);
// Use the formatting function for detailed output
if (process.env.VERBOSE_LOGGING === 'true') {
console.log(formatBlockData(eventData));
}
// Add specific position change logging
if (eventType === 'BlockMoved' && eventData.OldPosition && eventData.NewPosition) {
console.log(`POSITION CHANGE: From (${eventData.OldPosition.X.toFixed(2)}, ${eventData.OldPosition.Y.toFixed(2)}, ${eventData.OldPosition.Z.toFixed(2)}) to (${eventData.NewPosition.X.toFixed(2)}, ${eventData.NewPosition.Y.toFixed(2)}, ${eventData.NewPosition.Z.toFixed(2)})`);
}
} else {
// Standard logging for non-block events
console.log(`Received ${eventType} event for ${eventData.ObjectType}`);
}
res.status(200).json({ success: true, message: 'Event received' });
} catch (error) {
console.error('Error processing event:', error);
res.status(500).json({ success: false, message: 'Error processing event' });
}
});
// Add endpoints to get events by type
app.get('/api/events', (req, res) => {
res.json(global.autocadEvents.all);
});
app.get('/api/events/blocks', (req, res) => {
res.json(global.autocadEvents.blockEvents);
});
app.get('/api/events/blocks/:type', (req, res) => {
const eventType = req.params.type;
if (global.autocadEvents.blockEvents[eventType]) {
res.json(global.autocadEvents.blockEvents[eventType]);
} else {
res.status(404).json({ message: 'Event type not found' });
}
});
app.get('/api/events/entities', (req, res) => {
res.json(global.autocadEvents.entityEvents);
});
app.get('/api/events/entities/:type', (req, res) => {
const eventType = req.params.type;
if (global.autocadEvents.entityEvents[eventType]) {
res.json(global.autocadEvents.entityEvents[eventType]);
} else {
res.status(404).json({ message: 'Event type not found' });
}
});
// Add an endpoint to clear events
app.delete('/api/events', (req, res) => {
global.autocadEvents = {
all: [],
blockEvents: {
added: [],
erased: [],
stretched: [],
rotated: [],
scaled: [],
moved: [],
attributesChanged: [],
modified: []
},
entityEvents: {
added: [],
erased: [],
stretched: [],
modified: []
}
};
res.json({ success: true, message: 'All events cleared' });
});
// Function to save events to disk
function saveEventsToDisk() {
try {
const timestamp = new Date().toISOString().replace(/:/g, '-');
const filePath = path.join(dataDir, `events_${timestamp}.json`);
// Use a more memory-efficient approach for large event arrays
if (global.autocadEvents.all.length > 1000) {
// Write to file stream instead of creating a large string in memory
const fileStream = fs.createWriteStream(filePath);
fileStream.write('{\n');
fileStream.write('"all": [\n');
global.autocadEvents.all.forEach((event, index) => {
const eventJson = JSON.stringify(event);
fileStream.write(eventJson);
if (index < global.autocadEvents.all.length - 1) {
fileStream.write(',\n');
} else {
fileStream.write('\n');
}
});
fileStream.write('],\n');
fileStream.write('"blockEvents": ' + JSON.stringify(global.autocadEvents.blockEvents) + ',\n');
fileStream.write('"entityEvents": ' + JSON.stringify(global.autocadEvents.entityEvents) + '\n');
fileStream.write('}');
fileStream.end();
console.log(`Events saved to ${filePath} (stream method)`);
} else {
// For smaller datasets, use the simpler approach
fs.writeFileSync(filePath, JSON.stringify(global.autocadEvents, null, 2));
console.log(`Events saved to ${filePath}`);
}
} catch (error) {
console.error('Error saving events to disk:', error);
}
}
// Connect to MongoDB (optional - comment out if you don't want to use it)
// mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/autocad_tracker')
// .then(() => console.log('Connected to MongoDB'))
// .catch(err => console.error('Could not connect to MongoDB', err));
// Default route
app.get('/', (req, res) => {
res.redirect('/index.html');
});
// Legacy route for backward compatibility
app.get('/attribute-history', (req, res) => {
res.redirect('/index.html');
});
// Update the formatBlockData function for better color formatting
function formatBlockData(block) {
let output = '';
// Add basic block info
output += `\n--- Block Details ---`;
output += `\nName: ${block.BlockName}`;
output += `\nObject ID: ${block.ObjectId}`;
// Add position information
if (block.NewPosition) {
output += `\nPosition: (${block.NewPosition.X.toFixed(2)}, ${block.NewPosition.Y.toFixed(2)}, ${block.NewPosition.Z.toFixed(2)})`;
// Add old position if available (for moved blocks)
if (block.OldPosition) {
output += `\nOld Position: (${block.OldPosition.X.toFixed(2)}, ${block.OldPosition.Y.toFixed(2)}, ${block.OldPosition.Z.toFixed(2)})`;
}
} else if (block.Position) {
output += `\nPosition: (${block.Position.X.toFixed(2)}, ${block.Position.Y.toFixed(2)}, ${block.Position.Z.toFixed(2)})`;
}
// Add rotation information
if (block.NewRotation !== undefined) {
output += `\nRotation: ${(block.NewRotation * 180 / Math.PI).toFixed(2)} degrees`;
// Add old rotation if available
if (block.OldRotation !== undefined) {
output += `\nOld Rotation: ${(block.OldRotation * 180 / Math.PI).toFixed(2)} degrees`;
}
}
// Add scale information
if (block.NewScale) {
output += `\nScale: X=${block.NewScale.X.toFixed(3)}, Y=${block.NewScale.Y.toFixed(3)}, Z=${block.NewScale.Z.toFixed(3)}`;
// Add old scale if available
if (block.OldScale) {
output += `\nOld Scale: X=${block.OldScale.X.toFixed(3)}, Y=${block.OldScale.Y.toFixed(3)}, Z=${block.OldScale.Z.toFixed(3)}`;
}
}
// Add attribute information
if (block.Attributes && Object.keys(block.Attributes).length > 0) {
output += `\nAttributes:`;
for (const [key, value] of Object.entries(block.Attributes)) {
output += `\n ${key}: ${value}`;
}
} else {
output += `\nNo attributes`;
}
// Add attribute changes if available
if (block.AttributeChanges && Object.keys(block.AttributeChanges).length > 0) {
output += `\nAttribute Changes:`;
for (const [key, change] of Object.entries(block.AttributeChanges)) {
output += `\n ${key}: "${change.OldValue}" -> "${change.NewValue}"`;
}
}
return output;
}
// Add an endpoint to view detailed block information
app.get('/api/events/blocks/details/:id', (req, res) => {
const blockId = req.params.id;
// Search for the block in all categories
const allBlocks = Object.values(global.autocadEvents.blockEvents)
.flat()
.filter(event => event.ObjectId === blockId);
if (allBlocks.length === 0) {
return res.status(404).json({ message: 'Block not found' });
}
// Get the latest block data
const latestBlock = allBlocks[allBlocks.length - 1];
// Format the block data
const formattedData = {
blockData: latestBlock,
formatted: formatBlockData(latestBlock),
history: allBlocks.map(block => ({
timestamp: block.Timestamp,
eventType: block.EventType
}))
};
res.json(formattedData);
});
// Add an endpoint to get a list of all blocks with their latest attributes
app.get('/api/blocks', (req, res) => {
// Get all block events
const allBlockEvents = global.autocadEvents.all.filter(event =>
event.EventType && event.EventType.startsWith('Block')
);
// Create a map to store the latest state of each block
const blockMap = new Map();
// Process events to get the latest state of each block
allBlockEvents.forEach(event => {
// Use object ID as the unique identifier
const blockId = event.ObjectId;
// Update the map with the latest event for each block
blockMap.set(blockId, event);
});
// Convert map to array of blocks
const blocks = Array.from(blockMap.values()).map(block => ({
id: block.ObjectId,
name: block.BlockName,
position: block.NewPosition || block.Position,
attributes: block.Attributes || {},
lastEventType: block.EventType,
timestamp: block.Timestamp
}));
res.json(blocks);
});
// Add an endpoint to get all attribute changes
app.get('/api/blocks/attribute-changes', (req, res) => {
const attributeChanges = global.autocadEvents.blockEvents.attributesChanged;
// Process attribute changes to make them more meaningful
const processedChanges = attributeChanges.map(event => {
// Extract basic information
const { BlockName, ObjectId, Timestamp, Attributes, AttributeChanges } = event;
// Process each changed attribute
const changes = [];
if (AttributeChanges) {
for (const [tag, change] of Object.entries(AttributeChanges)) {
changes.push({
tag,
oldValue: change.OldValue,
newValue: change.NewValue,
changeTime: Timestamp
});
}
}
return {
blockName: BlockName,
objectId: ObjectId,
timestamp: Timestamp,
currentAttributes: Attributes || {},
changes
};
});
res.json(processedChanges);
});
// Add an endpoint to get attribute change history for a specific block
app.get('/api/blocks/:id/attribute-history', (req, res) => {
const blockId = req.params.id;
// Find all attribute change events for this block
const attributeChanges = global.autocadEvents.all
.filter(event =>
event.ObjectId === blockId &&
(event.AttributeChanges || event.EventType === 'BlockAttributesChanged')
)
.sort((a, b) => new Date(a.Timestamp) - new Date(b.Timestamp));
if (attributeChanges.length === 0) {
return res.status(404).json({ message: 'No attribute changes found for this block' });
}
// Process the attribute history
const history = [];
const latestAttributes = {};
attributeChanges.forEach(event => {
const timestamp = event.Timestamp;
// Track changes for each attribute
if (event.AttributeChanges) {
for (const [tag, change] of Object.entries(event.AttributeChanges)) {
history.push({
tag,
oldValue: change.OldValue,
newValue: change.NewValue,
timestamp,
blockName: event.BlockName
});
// Update latest attribute value
latestAttributes[tag] = change.NewValue;
}
}
// Also get initial values from Attributes field if present
if (event.Attributes && Object.keys(event.Attributes).length > 0) {
for (const [tag, value] of Object.entries(event.Attributes)) {
if (!latestAttributes[tag]) {
latestAttributes[tag] = value;
}
}
}
});
// Return formatted response
res.json({
blockId,
blockName: attributeChanges[0].BlockName,
latestAttributes,
history
});
});
// Add an endpoint to get position change history for a specific block
app.get('/api/blocks/:id/position-history', (req, res) => {
const blockId = req.params.id;
// Find all position change events for this block
const positionChanges = global.autocadEvents.all
.filter(event =>
event.ObjectId === blockId &&
(event.EventType === 'BlockMoved' ||
event.EventType === 'BlockModified' ||
event.EventType === 'BlockAdded') &&
(event.NewPosition || event.Position)
)
.sort((a, b) => new Date(a.Timestamp) - new Date(b.Timestamp));
if (positionChanges.length === 0) {
return res.status(404).json({ message: 'No position changes found for this block' });
}
// Process the position history
const history = positionChanges.map(event => {
const entry = {
timestamp: event.Timestamp,
eventType: event.EventType,
blockName: event.BlockName
};
// Add position data
if (event.NewPosition) {
entry.position = {
x: event.NewPosition.X,
y: event.NewPosition.Y,
z: event.NewPosition.Z
};
// Add old position if available
if (event.OldPosition) {
entry.oldPosition = {
x: event.OldPosition.X,
y: event.OldPosition.Y,
z: event.OldPosition.Z
};
}
} else if (event.Position) {
entry.position = {
x: event.Position.X,
y: event.Position.Y,
z: event.Position.Z
};
}
return entry;
});
// Get latest position
const latestEvent = positionChanges[positionChanges.length - 1];
const latestPosition = latestEvent.NewPosition || latestEvent.Position;
// Return formatted response
res.json({
blockId,
blockName: positionChanges[0].BlockName,
latestPosition: {
x: latestPosition.X,
y: latestPosition.Y,
z: latestPosition.Z
},
history
});
});
// Add an endpoint to get server status and performance metrics
app.get('/api/status', (req, res) => {
const status = {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
eventsCount: {
total: global.autocadEvents.all.length,
blocks: Object.keys(global.autocadEvents.blockEvents).reduce(
(total, key) => total + global.autocadEvents.blockEvents[key].length, 0
),
entities: Object.keys(global.autocadEvents.entityEvents).reduce(
(total, key) => total + global.autocadEvents.entityEvents[key].length, 0
)
},
sseClients: sseClients.length,
lastSaveTime: new Date(lastSaveTime).toISOString()
};
res.json(status);
});
// Add an endpoint for checking server status (lightweight compared to SSE)
app.get('/api/server-status', (req, res) => {
res.json({
status: 'online',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
console.log(`API endpoints available at:`);
console.log(` - GET /api/events: Get all events`);
console.log(` - GET /api/events/blocks: Get all block events`);
console.log(` - GET /api/events/blocks/:type: Get block events by type`);
console.log(` - GET /api/events/entities: Get all entity events`);
console.log(` - GET /api/events/entities/:type: Get entity events by type`);
console.log(` - DELETE /api/events: Clear all events`);
console.log(` - GET /api/events/blocks/details/:id: Get detailed block information`);
console.log(` - GET /api/blocks: Get a list of all blocks with their latest attributes`);
console.log(` - GET /api/blocks/attribute-changes: Get all attribute changes`);
console.log(` - GET /api/blocks/:id/attribute-history: Get attribute change history for a specific block`);
console.log(` - GET /api/blocks/:id/position-history: Get position change history for a specific block`);
console.log(` - GET /api/status: Get server status and performance metrics`);
console.log(` - GET /api/server-status: Get server status (lightweight)`);
console.log(` - Front-end interface available at: http://localhost:${PORT}`);
});

63
test-client.js Normal file
View File

@ -0,0 +1,63 @@
const fetch = require('node-fetch');
// Simulate sending AutoCAD events to our server
async function sendTestEvent() {
try {
// Sample data for an Added event
const addedEvent = {
EventType: "Added",
ObjectId: "A1B2C3D4",
ObjectType: "Line",
Position: { X: 100.5, Y: 200.8, Z: 0.0 },
Timestamp: new Date().toISOString()
};
// Send to server
console.log('Sending test data to server...');
const response = await fetch('http://localhost:3000/api/autocad-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(addedEvent),
});
const data = await response.json();
console.log('Server response:', data);
// Sample data for a Modified event
const modifiedEvent = {
EventType: "Modified",
ObjectId: "A1B2C3D4",
ObjectType: "Line",
OldPosition: { X: 100.5, Y: 200.8, Z: 0.0 },
NewPosition: { X: 150.2, Y: 250.3, Z: 0.0 },
Timestamp: new Date().toISOString()
};
// Send to server
console.log('Sending modification test data to server...');
const modResponse = await fetch('http://localhost:3000/api/autocad-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(modifiedEvent),
});
const modData = await modResponse.json();
console.log('Server response:', modData);
// Retrieve all events
console.log('Retrieving all events from server...');
const getResponse = await fetch('http://localhost:3000/api/autocad-events');
const allEvents = await getResponse.json();
console.log('All stored events:', JSON.stringify(allEvents, null, 2));
} catch (error) {
console.error('Error sending test data:', error);
}
}
// Run the test
sendTestEvent();