cyclecad 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
- package/BILLING-INDEX.md +293 -0
- package/BILLING-INTEGRATION-GUIDE.md +414 -0
- package/COLLABORATION-INDEX.md +440 -0
- package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
- package/DOCKER-BUILD-MANIFEST.txt +483 -0
- package/DOCKER-FILES-REFERENCE.md +440 -0
- package/DOCKER-INFRASTRUCTURE.md +475 -0
- package/DOCKER-README.md +435 -0
- package/Dockerfile +33 -55
- package/PWA-FILES-CREATED.txt +350 -0
- package/QUICK-START-TESTING.md +126 -0
- package/STEP-IMPORT-QUICKSTART.md +347 -0
- package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
- package/app/css/mobile.css +1074 -0
- package/app/icons/generate-icons.js +203 -0
- package/app/index.html +93 -0
- package/app/js/billing-ui.js +990 -0
- package/app/js/brep-kernel.js +933 -981
- package/app/js/collab-client.js +750 -0
- package/app/js/mobile-nav.js +623 -0
- package/app/js/mobile-toolbar.js +476 -0
- package/app/js/modules/billing-module.js +724 -0
- package/app/js/modules/step-module-enhanced.js +938 -0
- package/app/js/offline-manager.js +705 -0
- package/app/js/responsive-init.js +360 -0
- package/app/js/touch-handler.js +429 -0
- package/app/manifest.json +211 -0
- package/app/offline.html +508 -0
- package/app/sw.js +571 -0
- package/app/tests/billing-tests.html +779 -0
- package/app/tests/brep-tests.html +980 -0
- package/app/tests/collab-tests.html +743 -0
- package/app/tests/mobile-tests.html +1299 -0
- package/app/tests/pwa-tests.html +1134 -0
- package/app/tests/step-tests.html +1042 -0
- package/app/tests/test-agent-v3.html +719 -0
- package/docker-compose.yml +225 -0
- package/docs/BILLING-HELP.json +260 -0
- package/docs/BILLING-README.md +639 -0
- package/docs/BILLING-TUTORIAL.md +736 -0
- package/docs/BREP-HELP.json +326 -0
- package/docs/BREP-TUTORIAL.md +802 -0
- package/docs/COLLABORATION-HELP.json +228 -0
- package/docs/COLLABORATION-TUTORIAL.md +818 -0
- package/docs/DOCKER-HELP.json +224 -0
- package/docs/DOCKER-TUTORIAL.md +974 -0
- package/docs/MOBILE-HELP.json +243 -0
- package/docs/MOBILE-RESPONSIVE-README.md +378 -0
- package/docs/MOBILE-TUTORIAL.md +747 -0
- package/docs/PWA-HELP.json +228 -0
- package/docs/PWA-README.md +662 -0
- package/docs/PWA-TUTORIAL.md +757 -0
- package/docs/STEP-HELP.json +481 -0
- package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
- package/docs/TESTING-GUIDE.md +528 -0
- package/docs/TESTING-HELP.json +182 -0
- package/fusion-vs-cyclecad.html +1771 -0
- package/nginx.conf +237 -0
- package/package.json +1 -1
- package/server/Dockerfile.converter +51 -0
- package/server/Dockerfile.signaling +28 -0
- package/server/billing-server.js +487 -0
- package/server/converter-enhanced.py +528 -0
- package/server/requirements-converter.txt +29 -0
- package/server/signaling-server.js +801 -0
- package/tests/docker-tests.sh +389 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
# cycleCAD Live Collaboration Tutorial
|
|
2
|
+
|
|
3
|
+
## What is Real-Time Collaboration in CAD?
|
|
4
|
+
|
|
5
|
+
Real-time collaboration enables multiple engineers to work on the same 3D model simultaneously, seeing each other's cursors, selections, and edits in live time. Instead of emailing files back and forth or merging conflicts manually, everyone is always working on the latest version.
|
|
6
|
+
|
|
7
|
+
### Key Benefits
|
|
8
|
+
- **Live cursor sharing** — See where each team member is looking
|
|
9
|
+
- **Live selection sync** — Know which parts others are editing
|
|
10
|
+
- **Operation replay** — All changes are logged and can be reviewed
|
|
11
|
+
- **Chat & comments** — Discuss designs without leaving the app
|
|
12
|
+
- **Offline support** — Changes queue and sync when reconnected
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Architecture Overview
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌─────────────────────────────────────────────────────────┐
|
|
20
|
+
│ cycleCAD Collaboration System │
|
|
21
|
+
├─────────────────────────────────────────────────────────┤
|
|
22
|
+
│ │
|
|
23
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
24
|
+
│ │ User A │ │ User B │ │ User C │ │
|
|
25
|
+
│ │ Browser │ │ Browser │ │ Browser │ │
|
|
26
|
+
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
|
27
|
+
│ │ │ │ │
|
|
28
|
+
│ └──────────────────┼──────────────────┘ │
|
|
29
|
+
│ │ │
|
|
30
|
+
│ ┌─────▼──────┐ │
|
|
31
|
+
│ │ WebSocket │ │
|
|
32
|
+
│ │ Bridge │ │
|
|
33
|
+
│ └─────┬──────┘ │
|
|
34
|
+
│ │ │
|
|
35
|
+
│ ┌──────────────────┼──────────────────┐ │
|
|
36
|
+
│ │ │ │ │
|
|
37
|
+
│ ┌────▼─────┐ ┌──────▼──────┐ ┌─────▼────┐ │
|
|
38
|
+
│ │ Signaling │ │ CRDT │ │ Chat │ │
|
|
39
|
+
│ │ Server │ │ Engine │ │ Server │ │
|
|
40
|
+
│ └───────────┘ └─────────────┘ └──────────┘ │
|
|
41
|
+
│ │ │
|
|
42
|
+
│ ┌──────▼──────┐ │
|
|
43
|
+
│ │ Room State │ │
|
|
44
|
+
│ │ (Persisted) │ │
|
|
45
|
+
│ └─────────────┘ │
|
|
46
|
+
└─────────────────────────────────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Components
|
|
50
|
+
|
|
51
|
+
1. **Signaling Server** (`server/signaling-server.js`)
|
|
52
|
+
- Manages WebSocket connections
|
|
53
|
+
- Routes messages between clients
|
|
54
|
+
- Maintains room state and user presence
|
|
55
|
+
- Persists operations to disk
|
|
56
|
+
|
|
57
|
+
2. **Collaboration Client** (`app/js/collab-client.js`)
|
|
58
|
+
- Browser-side WebSocket manager
|
|
59
|
+
- Handles WebRTC peer connections
|
|
60
|
+
- Implements offline queue
|
|
61
|
+
- Auto-reconnect with exponential backoff
|
|
62
|
+
|
|
63
|
+
3. **CRDT Engine** (integrated in app.js)
|
|
64
|
+
- Last-writer-wins (LWW) for simple properties
|
|
65
|
+
- Operation log for geometry changes
|
|
66
|
+
- Conflict resolution without central server
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Setting Up the Signaling Server
|
|
71
|
+
|
|
72
|
+
### Local Development
|
|
73
|
+
|
|
74
|
+
#### Option 1: Direct Node.js
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Install dependencies
|
|
78
|
+
npm install ws express
|
|
79
|
+
|
|
80
|
+
# Start the signaling server
|
|
81
|
+
node server/signaling-server.js
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Server runs on `ws://localhost:8788` with HTTP health checks at `http://localhost:8788/health`.
|
|
85
|
+
|
|
86
|
+
#### Option 2: npm script
|
|
87
|
+
|
|
88
|
+
Add to `package.json`:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"scripts": {
|
|
93
|
+
"collab:server": "node server/signaling-server.js",
|
|
94
|
+
"collab:server:dev": "NODE_ENV=development node server/signaling-server.js"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Run:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm run collab:server
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Docker Deployment
|
|
106
|
+
|
|
107
|
+
#### Build the image
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
docker build -t cyclecad-signaling -f server/Dockerfile.signaling server/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Run the container
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker run -p 8788:8788 cyclecad-signaling
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### With docker-compose
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
docker-compose up signaling
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Cloud Deployment
|
|
126
|
+
|
|
127
|
+
#### AWS (Lightsail)
|
|
128
|
+
|
|
129
|
+
1. Create a Node.js instance
|
|
130
|
+
2. Upload `server/signaling-server.js` and `package.json`
|
|
131
|
+
3. Install dependencies: `npm install`
|
|
132
|
+
4. Use PM2 for process management: `npm install -g pm2 && pm2 start server/signaling-server.js`
|
|
133
|
+
5. Configure security group to allow port 8788
|
|
134
|
+
6. Use Route 53 to point `collab.cyclecad.com` to the instance
|
|
135
|
+
|
|
136
|
+
#### Google Cloud (Cloud Run)
|
|
137
|
+
|
|
138
|
+
Create `Dockerfile`:
|
|
139
|
+
|
|
140
|
+
```dockerfile
|
|
141
|
+
FROM node:20-alpine
|
|
142
|
+
WORKDIR /app
|
|
143
|
+
COPY server/package.json .
|
|
144
|
+
RUN npm install
|
|
145
|
+
COPY server/ .
|
|
146
|
+
EXPOSE 8788
|
|
147
|
+
CMD ["node", "signaling-server.js"]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Deploy:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
gcloud run deploy cyclecad-signaling \
|
|
154
|
+
--source . \
|
|
155
|
+
--platform managed \
|
|
156
|
+
--allow-unauthenticated
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Heroku
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git push heroku main
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Client-Side Integration
|
|
168
|
+
|
|
169
|
+
### 1. Initialize the Collaboration Client
|
|
170
|
+
|
|
171
|
+
In your `app/js/app.js`:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
// Initialize collaboration client
|
|
175
|
+
const collabClient = new CollaborationClient('ws://localhost:8788');
|
|
176
|
+
|
|
177
|
+
// Or connect to cloud server:
|
|
178
|
+
// const collabClient = new CollaborationClient('wss://collab.cyclecad.com');
|
|
179
|
+
|
|
180
|
+
// Set up event callbacks
|
|
181
|
+
collabClient.on('connected', () => {
|
|
182
|
+
console.log('Connected to collaboration server');
|
|
183
|
+
updateUI('collaborationStatus', 'Connected');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
collabClient.on('disconnected', () => {
|
|
187
|
+
console.log('Disconnected from collaboration server');
|
|
188
|
+
updateUI('collaborationStatus', 'Disconnected');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
collabClient.on('userJoined', ({ userId, name, color }) => {
|
|
192
|
+
console.log(`${name} joined the session`);
|
|
193
|
+
renderUserInList(userId, name, color);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
collabClient.on('userLeft', ({ userId }) => {
|
|
197
|
+
console.log(`User ${userId} left`);
|
|
198
|
+
removeUserFromList(userId);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
collabClient.on('operationReceived', ({ userId, op, timestamp }) => {
|
|
202
|
+
applyRemoteOperation(op);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
collabClient.on('chatMessage', ({ name, color, text, timestamp }) => {
|
|
206
|
+
appendChatMessage(name, text, color, timestamp);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
collabClient.on('cursorUpdate', ({ userId, x, y }) => {
|
|
210
|
+
renderRemoteCursor(userId, x, y);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
collabClient.on('error', (error) => {
|
|
214
|
+
console.error('Collaboration error:', error);
|
|
215
|
+
showErrorNotification(error.message);
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 2. Create or Join a Room
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
// Create a new collaboration room
|
|
223
|
+
function createCollaborationSession() {
|
|
224
|
+
const roomId = `project-${Date.now()}`;
|
|
225
|
+
const sessionName = prompt('Session name:') || `Session ${roomId}`;
|
|
226
|
+
|
|
227
|
+
collabClient.createRoom(roomId, {
|
|
228
|
+
name: sessionName,
|
|
229
|
+
maxUsers: 10
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
collabClient.joinRoom(
|
|
234
|
+
roomId,
|
|
235
|
+
'user-' + crypto.getRandomValues(new Uint8Array(4)).join('-'),
|
|
236
|
+
prompt('Your name:') || 'Anonymous Engineer'
|
|
237
|
+
);
|
|
238
|
+
}, 500);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Join an existing room
|
|
242
|
+
function joinCollaborationSession() {
|
|
243
|
+
const roomId = prompt('Enter room ID:');
|
|
244
|
+
const userName = prompt('Your name:') || 'Anonymous Engineer';
|
|
245
|
+
const password = prompt('Room password (if private):') || null;
|
|
246
|
+
|
|
247
|
+
collabClient.joinRoom(
|
|
248
|
+
roomId,
|
|
249
|
+
'user-' + crypto.getRandomValues(new Uint8Array(4)).join('-'),
|
|
250
|
+
userName,
|
|
251
|
+
password
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 3. Share Cursor Position
|
|
257
|
+
|
|
258
|
+
Track mouse movement and send to server (throttled):
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// Listen to mouse moves in the 3D viewport
|
|
262
|
+
document.addEventListener('mousemove', (event) => {
|
|
263
|
+
// Normalize to viewport coordinates
|
|
264
|
+
const rect = viewport.domElement.getBoundingClientRect();
|
|
265
|
+
const x = (event.clientX - rect.left) / rect.width;
|
|
266
|
+
const y = (event.clientY - rect.top) / rect.height;
|
|
267
|
+
|
|
268
|
+
collabClient.updateCursor(x, y);
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 4. Share Part Selection
|
|
273
|
+
|
|
274
|
+
When user selects a part:
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
function onPartSelected(partId) {
|
|
278
|
+
// Local selection
|
|
279
|
+
selectedParts = [partId];
|
|
280
|
+
|
|
281
|
+
// Broadcast to collaborators
|
|
282
|
+
collabClient.updateSelection(selectedParts);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function onMultiSelect(partIds) {
|
|
286
|
+
selectedParts = partIds;
|
|
287
|
+
collabClient.updateSelection(partIds);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 5. Send Operations
|
|
292
|
+
|
|
293
|
+
When user performs a geometry operation:
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
function onExtrude(sketchId, depth) {
|
|
297
|
+
const op = {
|
|
298
|
+
type: 'extrude',
|
|
299
|
+
sketchId,
|
|
300
|
+
depth,
|
|
301
|
+
timestamp: Date.now()
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Apply locally
|
|
305
|
+
applyExtrude(sketchId, depth);
|
|
306
|
+
|
|
307
|
+
// Send to collaborators
|
|
308
|
+
collabClient.sendOperation(op);
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 6. Chat Integration
|
|
313
|
+
|
|
314
|
+
Send and receive chat messages:
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
function sendChatMessage(text) {
|
|
318
|
+
if (!text.trim()) return;
|
|
319
|
+
|
|
320
|
+
collabClient.sendMessage(text);
|
|
321
|
+
|
|
322
|
+
// Clear input
|
|
323
|
+
document.querySelector('#chat-input').value = '';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// In collabClient.on('chatMessage', ...) callback
|
|
327
|
+
function appendChatMessage(name, text, color, timestamp) {
|
|
328
|
+
const chatPanel = document.querySelector('#chat-panel');
|
|
329
|
+
const msg = document.createElement('div');
|
|
330
|
+
msg.className = 'chat-message';
|
|
331
|
+
msg.style.borderLeft = `3px solid ${color}`;
|
|
332
|
+
msg.innerHTML = `
|
|
333
|
+
<span class="chat-name" style="color: ${color};">${escapeHtml(name)}</span>
|
|
334
|
+
<span class="chat-text">${escapeHtml(text)}</span>
|
|
335
|
+
<span class="chat-time">${new Date(timestamp).toLocaleTimeString()}</span>
|
|
336
|
+
`;
|
|
337
|
+
chatPanel.appendChild(msg);
|
|
338
|
+
chatPanel.scrollTop = chatPanel.scrollHeight;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## How WebRTC P2P Works
|
|
345
|
+
|
|
346
|
+
Once signaling is complete, direct peer-to-peer connections are established:
|
|
347
|
+
|
|
348
|
+
### 1. Signaling Phase (via WebSocket)
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
User A (Browser) Signaling Server User B (Browser)
|
|
352
|
+
│ │ │
|
|
353
|
+
│──────── Offer ─────────────────>│ │
|
|
354
|
+
│ │────── Offer ──────────────>│
|
|
355
|
+
│ │ │
|
|
356
|
+
│<────── ICE Candidates ──────────│<────────────────────────────│
|
|
357
|
+
│ │ │
|
|
358
|
+
│<────── Answer ─────────────────│ │
|
|
359
|
+
│ │<────── Answer ─────────────│
|
|
360
|
+
│ │ │
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 2. Direct P2P Phase (DataChannel)
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
User A (Browser) <──────────────────── DataChannel ────────────────> User B (Browser)
|
|
367
|
+
(Low latency, high bandwidth)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Key WebRTC Details
|
|
371
|
+
|
|
372
|
+
- **STUN servers**: Public servers that help discover your public IP for NAT traversal
|
|
373
|
+
- **TURN servers**: Relay servers if direct P2P isn't possible (firewall/NAT restrictions)
|
|
374
|
+
- **Data channels**: Ordered, reliable messaging between peers (like a WebSocket but P2P)
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## CRDT Basics and Conflict Resolution
|
|
379
|
+
|
|
380
|
+
Conflict-free Replicated Data Type (CRDT) allows all users to apply operations independently without central coordination.
|
|
381
|
+
|
|
382
|
+
### Approach 1: Last-Writer-Wins (LWW)
|
|
383
|
+
|
|
384
|
+
Simple properties like part name or visibility:
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
// User A: renames part to "Housing"
|
|
388
|
+
collabClient.sendOperation({
|
|
389
|
+
type: 'rename',
|
|
390
|
+
partId: 'part-123',
|
|
391
|
+
name: 'Housing',
|
|
392
|
+
timestamp: 1711000000000
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// User B: renames same part to "Enclosure"
|
|
396
|
+
collabClient.sendOperation({
|
|
397
|
+
type: 'rename',
|
|
398
|
+
partId: 'part-123',
|
|
399
|
+
name: 'Enclosure',
|
|
400
|
+
timestamp: 1711000001000 // User B's change came 1 second later
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Result: Part is renamed to "Enclosure" (timestamp wins)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Approach 2: Operation Log
|
|
407
|
+
|
|
408
|
+
Geometry operations are cumulative — order matters:
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
// User A performs operation at index 42
|
|
412
|
+
collabClient.sendOperation({
|
|
413
|
+
type: 'fillet',
|
|
414
|
+
edgeId: 'edge-456',
|
|
415
|
+
radius: 5,
|
|
416
|
+
operationIndex: 42
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// User B sees User A's op and applies it locally
|
|
420
|
+
// Then User B performs operation at index 43
|
|
421
|
+
collabClient.sendOperation({
|
|
422
|
+
type: 'chamfer',
|
|
423
|
+
edgeId: 'edge-789',
|
|
424
|
+
distance: 2,
|
|
425
|
+
operationIndex: 43
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Both users end up with [op0, op1, ..., op42, op43] in same order
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Approach 3: Vector Clocks
|
|
432
|
+
|
|
433
|
+
For partial ordering (if needed):
|
|
434
|
+
|
|
435
|
+
```javascript
|
|
436
|
+
// Each client maintains a vector clock
|
|
437
|
+
const vectorClock = {
|
|
438
|
+
'user-a': 5,
|
|
439
|
+
'user-b': 3,
|
|
440
|
+
'user-c': 2
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// User A's next operation is: [6, 3, 2]
|
|
444
|
+
// User C's next operation is: [5, 3, 3]
|
|
445
|
+
|
|
446
|
+
// If clocks differ, we can determine causality
|
|
447
|
+
// and apply operations in the right order
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Offline Mode and Reconnection
|
|
453
|
+
|
|
454
|
+
When the client loses connection:
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
┌─────────────────────────────────────────┐
|
|
458
|
+
│ Offline Operation Queue │
|
|
459
|
+
├─────────────────────────────────────────┤
|
|
460
|
+
│ Op 1: Extrude sketch-1, depth=10 │
|
|
461
|
+
│ Op 2: Fillet edge-5, radius=2 │
|
|
462
|
+
│ Op 3: Rename part-3 → "Housing" │
|
|
463
|
+
│ Op 4: Hide part-1 │
|
|
464
|
+
└─────────────────────────────────────────┘
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
When reconnected:
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
// CollaborationClient automatically:
|
|
471
|
+
// 1. Detects connection restored
|
|
472
|
+
// 2. Replays all queued operations
|
|
473
|
+
// 3. Syncs server state
|
|
474
|
+
|
|
475
|
+
collabClient.on('connected', () => {
|
|
476
|
+
// Queue will be synced automatically
|
|
477
|
+
console.log('Offline operations synced');
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Security Considerations
|
|
484
|
+
|
|
485
|
+
### 1. Room Passwords
|
|
486
|
+
|
|
487
|
+
Protect rooms with optional passwords:
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
collabClient.createRoom('secret-project', {
|
|
491
|
+
password: 'my-secure-password-123',
|
|
492
|
+
maxUsers: 5
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Others must provide password to join
|
|
496
|
+
collabClient.joinRoom(
|
|
497
|
+
'secret-project',
|
|
498
|
+
userId,
|
|
499
|
+
userName,
|
|
500
|
+
'my-secure-password-123' // Must match
|
|
501
|
+
);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### 2. JWT Authentication (Optional)
|
|
505
|
+
|
|
506
|
+
For production, add JWT tokens to handshake:
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
const token = await fetch('/api/auth/token').then(r => r.json());
|
|
510
|
+
|
|
511
|
+
collabClient.ws.send(JSON.stringify({
|
|
512
|
+
type: 'authenticate',
|
|
513
|
+
token: token.jwt,
|
|
514
|
+
userId: token.userId
|
|
515
|
+
}));
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 3. TLS/SSL
|
|
519
|
+
|
|
520
|
+
Always use `wss://` (secure WebSocket) in production:
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
const isProduction = window.location.protocol === 'https:';
|
|
524
|
+
const signalServerUrl = isProduction
|
|
525
|
+
? 'wss://collab.cyclecad.com'
|
|
526
|
+
: 'ws://localhost:8788';
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Troubleshooting
|
|
532
|
+
|
|
533
|
+
### WebSocket Connection Fails
|
|
534
|
+
|
|
535
|
+
**Symptom**: "Cannot connect to signaling server"
|
|
536
|
+
|
|
537
|
+
**Causes**:
|
|
538
|
+
- Signaling server not running
|
|
539
|
+
- Wrong URL (check http://localhost:8788/health)
|
|
540
|
+
- Firewall blocking port 8788
|
|
541
|
+
- CORS issues
|
|
542
|
+
|
|
543
|
+
**Fix**:
|
|
544
|
+
```bash
|
|
545
|
+
# Check server health
|
|
546
|
+
curl http://localhost:8788/health
|
|
547
|
+
|
|
548
|
+
# Verify port is open
|
|
549
|
+
lsof -i :8788
|
|
550
|
+
|
|
551
|
+
# Check firewall rules
|
|
552
|
+
sudo ufw status
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Cursor Updates Lag
|
|
556
|
+
|
|
557
|
+
**Symptom**: Remote cursors update slowly or jerkily
|
|
558
|
+
|
|
559
|
+
**Causes**:
|
|
560
|
+
- Network latency
|
|
561
|
+
- Throttle interval too long (default 100ms)
|
|
562
|
+
- Browser performance issues
|
|
563
|
+
|
|
564
|
+
**Fix**:
|
|
565
|
+
```javascript
|
|
566
|
+
// Reduce throttle interval
|
|
567
|
+
collabClient.cursorUpdateInterval = 50; // 50ms instead of 100ms
|
|
568
|
+
|
|
569
|
+
// Or increase if bandwidth is constrained
|
|
570
|
+
collabClient.cursorUpdateInterval = 200; // 200ms
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### WebRTC Data Channel Never Opens
|
|
574
|
+
|
|
575
|
+
**Symptom**: P2P messages not being sent
|
|
576
|
+
|
|
577
|
+
**Causes**:
|
|
578
|
+
- STUN/TURN server unreachable
|
|
579
|
+
- Strict firewall/NAT
|
|
580
|
+
- Browser doesn't support WebRTC DataChannels
|
|
581
|
+
|
|
582
|
+
**Fix**:
|
|
583
|
+
```javascript
|
|
584
|
+
// Add TURN servers for fallback
|
|
585
|
+
collabClient.iceServers = [
|
|
586
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
587
|
+
{
|
|
588
|
+
urls: 'turn:turnserver.example.com',
|
|
589
|
+
username: 'user',
|
|
590
|
+
credential: 'pass'
|
|
591
|
+
}
|
|
592
|
+
];
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Operations Not Syncing
|
|
596
|
+
|
|
597
|
+
**Symptom**: User A's edits not visible to User B
|
|
598
|
+
|
|
599
|
+
**Causes**:
|
|
600
|
+
- Operation not sent before closing connection
|
|
601
|
+
- Server not relaying operations
|
|
602
|
+
- Client ignoring remote operations
|
|
603
|
+
|
|
604
|
+
**Fix**:
|
|
605
|
+
```javascript
|
|
606
|
+
// Ensure operation is sent
|
|
607
|
+
collabClient.on('operation', (message) => {
|
|
608
|
+
console.log('Received operation:', message);
|
|
609
|
+
// Debug: Are we getting the message?
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Check server logs
|
|
613
|
+
tail -f ~/.cyclecad/rooms-state.json
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Room Persistence Not Working
|
|
617
|
+
|
|
618
|
+
**Symptom**: Room state lost after server restart
|
|
619
|
+
|
|
620
|
+
**Cause**: State file not being saved
|
|
621
|
+
|
|
622
|
+
**Fix**:
|
|
623
|
+
```bash
|
|
624
|
+
# Check state file exists
|
|
625
|
+
ls -la /path/to/rooms-state.json
|
|
626
|
+
|
|
627
|
+
# Check permissions
|
|
628
|
+
chmod 644 rooms-state.json
|
|
629
|
+
|
|
630
|
+
# Verify server can write
|
|
631
|
+
sudo -u nobody touch /path/to/test-write.txt
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## API Reference
|
|
637
|
+
|
|
638
|
+
### CollaborationClient
|
|
639
|
+
|
|
640
|
+
```javascript
|
|
641
|
+
// Constructor
|
|
642
|
+
const client = new CollaborationClient(signalServerUrl);
|
|
643
|
+
|
|
644
|
+
// Events
|
|
645
|
+
client.on('connected', callback);
|
|
646
|
+
client.on('disconnected', callback);
|
|
647
|
+
client.on('userJoined', callback);
|
|
648
|
+
client.on('userLeft', callback);
|
|
649
|
+
client.on('operationReceived', callback);
|
|
650
|
+
client.on('chatMessage', callback);
|
|
651
|
+
client.on('cursorUpdate', callback);
|
|
652
|
+
client.on('selectionUpdate', callback);
|
|
653
|
+
client.on('error', callback);
|
|
654
|
+
|
|
655
|
+
// Room management
|
|
656
|
+
client.createRoom(roomId, options);
|
|
657
|
+
client.joinRoom(roomId, userId, userName, password);
|
|
658
|
+
client.leaveRoom();
|
|
659
|
+
|
|
660
|
+
// Sharing
|
|
661
|
+
client.updateCursor(x, y);
|
|
662
|
+
client.updateSelection(partIds);
|
|
663
|
+
client.sendOperation(op);
|
|
664
|
+
client.sendMessage(text);
|
|
665
|
+
|
|
666
|
+
// Info
|
|
667
|
+
client.getUsers();
|
|
668
|
+
client.getUser(userId);
|
|
669
|
+
client.getRoomInfo();
|
|
670
|
+
|
|
671
|
+
// Connection
|
|
672
|
+
client.connect();
|
|
673
|
+
client.disconnect();
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Signaling Server REST API
|
|
677
|
+
|
|
678
|
+
```bash
|
|
679
|
+
# Health check
|
|
680
|
+
GET /health
|
|
681
|
+
→ { status: 'healthy', clients: 5, rooms: 2 }
|
|
682
|
+
|
|
683
|
+
# Server stats
|
|
684
|
+
GET /stats
|
|
685
|
+
→ { clients: 5, rooms: 2, uptime: 3600, memory: {...} }
|
|
686
|
+
|
|
687
|
+
# List all rooms
|
|
688
|
+
GET /rooms
|
|
689
|
+
→ { count: 2, rooms: [{id, name, users, ...}, ...] }
|
|
690
|
+
|
|
691
|
+
# Get specific room
|
|
692
|
+
GET /rooms/:roomId
|
|
693
|
+
→ { room: {...} }
|
|
694
|
+
|
|
695
|
+
# Reset room (clear operations)
|
|
696
|
+
POST /rooms/:roomId/reset
|
|
697
|
+
|
|
698
|
+
# Close room
|
|
699
|
+
POST /rooms/:roomId/close
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Example: Full Collaboration Session
|
|
705
|
+
|
|
706
|
+
```javascript
|
|
707
|
+
// 1. Initialize client
|
|
708
|
+
const collab = new CollaborationClient('wss://collab.cyclecad.com');
|
|
709
|
+
|
|
710
|
+
// 2. Handle events
|
|
711
|
+
collab.on('connected', () => {
|
|
712
|
+
document.getElementById('status').textContent = '🟢 Connected';
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
collab.on('userJoined', ({ name }) => {
|
|
716
|
+
showNotification(`${name} joined the session`);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
collab.on('operationReceived', ({ op }) => {
|
|
720
|
+
// Apply operation from remote user
|
|
721
|
+
const result = applyOperation(op);
|
|
722
|
+
updateViewport(result);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// 3. Create session
|
|
726
|
+
function newSession() {
|
|
727
|
+
const roomId = `session-${Date.now()}`;
|
|
728
|
+
collab.createRoom(roomId, { maxUsers: 10 });
|
|
729
|
+
|
|
730
|
+
setTimeout(() => {
|
|
731
|
+
collab.joinRoom(
|
|
732
|
+
roomId,
|
|
733
|
+
'user-' + generateId(),
|
|
734
|
+
'John Doe'
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
// Show share link
|
|
738
|
+
showShareLink(`cyclecad.com?room=${roomId}`);
|
|
739
|
+
}, 500);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 4. Perform operation
|
|
743
|
+
function performExtrude(depth) {
|
|
744
|
+
const op = { type: 'extrude', depth, timestamp: Date.now() };
|
|
745
|
+
|
|
746
|
+
// Local
|
|
747
|
+
applyOperation(op);
|
|
748
|
+
|
|
749
|
+
// Remote
|
|
750
|
+
collab.sendOperation(op);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 5. Chat
|
|
754
|
+
document.querySelector('#send-btn').onclick = () => {
|
|
755
|
+
const text = document.querySelector('#chat-input').value;
|
|
756
|
+
collab.sendMessage(text);
|
|
757
|
+
};
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## Performance Tuning
|
|
763
|
+
|
|
764
|
+
### Reduce Message Rate
|
|
765
|
+
|
|
766
|
+
```javascript
|
|
767
|
+
// Default cursor throttle is 100ms = 10 updates/sec
|
|
768
|
+
// For faster networks:
|
|
769
|
+
collabClient.cursorUpdateInterval = 50; // 20 updates/sec
|
|
770
|
+
|
|
771
|
+
// For slower networks:
|
|
772
|
+
collabClient.cursorUpdateInterval = 200; // 5 updates/sec
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Operation Batching
|
|
776
|
+
|
|
777
|
+
```javascript
|
|
778
|
+
// Batch multiple operations before sending
|
|
779
|
+
const batch = [];
|
|
780
|
+
|
|
781
|
+
batch.push({ type: 'extrude', depth: 10 });
|
|
782
|
+
batch.push({ type: 'fillet', radius: 2 });
|
|
783
|
+
|
|
784
|
+
collabClient.send({
|
|
785
|
+
type: 'batch-operation',
|
|
786
|
+
operations: batch
|
|
787
|
+
});
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### Memory Management
|
|
791
|
+
|
|
792
|
+
```javascript
|
|
793
|
+
// Limit operation history
|
|
794
|
+
if (collabClient.operations.length > 10000) {
|
|
795
|
+
collabClient.operations = collabClient.operations.slice(-5000);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Limit chat history
|
|
799
|
+
if (collabClient.chatMessages.length > 500) {
|
|
800
|
+
collabClient.chatMessages = collabClient.chatMessages.slice(-250);
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## Next Steps
|
|
807
|
+
|
|
808
|
+
1. **Deploy server**: Choose hosting (Docker, AWS, GCP, Heroku)
|
|
809
|
+
2. **Integrate client**: Wire `collab-client.js` into your app
|
|
810
|
+
3. **Test locally**: Run signaling server and open 2 browser windows
|
|
811
|
+
4. **Add UI**: Create collaboration panel with user list, chat, cursor display
|
|
812
|
+
5. **Monitor**: Set up logging and alerting for server health
|
|
813
|
+
|
|
814
|
+
For more details, see:
|
|
815
|
+
- `server/signaling-server.js` — Full server source
|
|
816
|
+
- `app/js/collab-client.js` — Full client source
|
|
817
|
+
- `docs/COLLABORATION-HELP.json` — User-facing help
|
|
818
|
+
|