first commit
This commit is contained in:
commit
99ed39df07
1
.cursorignore
Normal file
1
.cursorignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
||||
3
.env
Normal file
3
.env
Normal 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
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
||||
1649
AutoCadTrack.cs
Normal file
1649
AutoCadTrack.cs
Normal file
File diff suppressed because it is too large
Load Diff
50
README.md
Normal file
50
README.md
Normal 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
|
||||
33
controllers/autocadController.js
Normal file
33
controllers/autocadController.js
Normal 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);
|
||||
};
|
||||
1225
data/events_2025-05-16T12-44-37.278Z.json
Normal file
1225
data/events_2025-05-16T12-44-37.278Z.json
Normal file
File diff suppressed because it is too large
Load Diff
39
models/AutocadEvent.js
Normal file
39
models/AutocadEvent.js
Normal 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
1594
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal 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
225
public/css/style.css
Normal 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
103
public/index.html
Normal 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
532
public/js/app.js
Normal 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
11
routes/autocadRoutes.js
Normal 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
712
server.js
Normal 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
63
test-client.js
Normal 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();
|
||||
Loading…
x
Reference in New Issue
Block a user