cyclecad 2.1.0 → 3.1.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.
Files changed (94) hide show
  1. package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DELIVERABLES.txt +296 -445
  7. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  8. package/DOCKER-FILES-REFERENCE.md +440 -0
  9. package/DOCKER-INFRASTRUCTURE.md +475 -0
  10. package/DOCKER-README.md +435 -0
  11. package/Dockerfile +33 -55
  12. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  13. package/ENHANCEMENT_SUMMARY.txt +308 -0
  14. package/FEATURE_INVENTORY.md +235 -0
  15. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  16. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  17. package/FUSION360_PARITY_SUMMARY.md +520 -0
  18. package/FUSION360_QUICK_REFERENCE.md +351 -0
  19. package/MODULE_API_REFERENCE.md +712 -0
  20. package/MODULE_INVENTORY.txt +264 -0
  21. package/PWA-FILES-CREATED.txt +350 -0
  22. package/QUICK-START-TESTING.md +126 -0
  23. package/STEP-IMPORT-QUICKSTART.md +347 -0
  24. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  25. package/app/css/mobile.css +1074 -0
  26. package/app/icons/generate-icons.js +203 -0
  27. package/app/index.html +1342 -5031
  28. package/app/js/app.js +1312 -514
  29. package/app/js/billing-ui.js +990 -0
  30. package/app/js/brep-kernel.js +933 -981
  31. package/app/js/collab-client.js +750 -0
  32. package/app/js/mobile-nav.js +623 -0
  33. package/app/js/mobile-toolbar.js +476 -0
  34. package/app/js/modules/animation-module.js +497 -3
  35. package/app/js/modules/billing-module.js +724 -0
  36. package/app/js/modules/cam-module.js +507 -2
  37. package/app/js/modules/collaboration-module.js +513 -0
  38. package/app/js/modules/constraint-module.js +1266 -0
  39. package/app/js/modules/data-module.js +544 -1146
  40. package/app/js/modules/formats-module.js +438 -738
  41. package/app/js/modules/inspection-module.js +393 -0
  42. package/app/js/modules/mesh-module-enhanced.js +880 -0
  43. package/app/js/modules/plugin-module.js +597 -0
  44. package/app/js/modules/rendering-module.js +460 -0
  45. package/app/js/modules/scripting-module.js +593 -475
  46. package/app/js/modules/sketch-module.js +998 -2
  47. package/app/js/modules/step-module-enhanced.js +938 -0
  48. package/app/js/modules/surface-module.js +312 -0
  49. package/app/js/modules/version-module.js +420 -0
  50. package/app/js/offline-manager.js +705 -0
  51. package/app/js/responsive-init.js +360 -0
  52. package/app/js/touch-handler.js +429 -0
  53. package/app/manifest.json +211 -0
  54. package/app/offline.html +508 -0
  55. package/app/sw.js +571 -0
  56. package/app/tests/billing-tests.html +779 -0
  57. package/app/tests/brep-tests.html +980 -0
  58. package/app/tests/collab-tests.html +743 -0
  59. package/app/tests/mobile-tests.html +1299 -0
  60. package/app/tests/pwa-tests.html +1134 -0
  61. package/app/tests/step-tests.html +1042 -0
  62. package/app/tests/test-agent-v3.html +719 -0
  63. package/cycleCAD-Architecture-v2.pptx +0 -0
  64. package/docker-compose.yml +225 -0
  65. package/docs/BILLING-HELP.json +260 -0
  66. package/docs/BILLING-README.md +639 -0
  67. package/docs/BILLING-TUTORIAL.md +736 -0
  68. package/docs/BREP-HELP.json +326 -0
  69. package/docs/BREP-TUTORIAL.md +802 -0
  70. package/docs/COLLABORATION-HELP.json +228 -0
  71. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  72. package/docs/DOCKER-HELP.json +224 -0
  73. package/docs/DOCKER-TUTORIAL.md +974 -0
  74. package/docs/MOBILE-HELP.json +243 -0
  75. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  76. package/docs/MOBILE-TUTORIAL.md +747 -0
  77. package/docs/PWA-HELP.json +228 -0
  78. package/docs/PWA-README.md +662 -0
  79. package/docs/PWA-TUTORIAL.md +757 -0
  80. package/docs/STEP-HELP.json +481 -0
  81. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  82. package/docs/TESTING-GUIDE.md +528 -0
  83. package/docs/TESTING-HELP.json +182 -0
  84. package/fusion-vs-cyclecad.html +1771 -0
  85. package/nginx.conf +237 -0
  86. package/package.json +1 -1
  87. package/server/Dockerfile.converter +51 -0
  88. package/server/Dockerfile.signaling +28 -0
  89. package/server/billing-server.js +487 -0
  90. package/server/converter-enhanced.py +528 -0
  91. package/server/requirements-converter.txt +29 -0
  92. package/server/signaling-server.js +801 -0
  93. package/tests/docker-tests.sh +389 -0
  94. package/~$cycleCAD-Architecture-v2.pptx +0 -0
@@ -747,6 +747,477 @@ export default {
747
747
  document.dispatchEvent(event);
748
748
  },
749
749
 
750
+ // ========================================================================
751
+ // WEBRTC & CRDT ENHANCEMENTS (Fusion 360-parity)
752
+ // ========================================================================
753
+
754
+ /**
755
+ * Initialize WebRTC peer connection with signaling.
756
+ * Establishes data channels for CRDT synchronization.
757
+ * @private
758
+ * @async
759
+ * @param {string} peerId Peer user ID
760
+ * @returns {Promise<RTCPeerConnection>}
761
+ */
762
+ async _initPeerConnection(peerId) {
763
+ const pc = new RTCPeerConnection({
764
+ iceServers: [
765
+ { urls: 'stun:stun.l.google.com:19302' },
766
+ { urls: 'stun:stun1.l.google.com:19302' },
767
+ ],
768
+ });
769
+
770
+ // Create data channel for CRDT ops
771
+ const dc = pc.createDataChannel('crdt', { ordered: true });
772
+ this._setupDataChannelHandlers(dc, peerId);
773
+
774
+ pc.onicecandidate = (e) => {
775
+ if (e.candidate) {
776
+ this._broadcastToPeers('iceCandidate', {
777
+ from: this.state.userId,
778
+ to: peerId,
779
+ candidate: e.candidate,
780
+ });
781
+ }
782
+ };
783
+
784
+ pc.ondatachannel = (e) => {
785
+ this._setupDataChannelHandlers(e.channel, peerId);
786
+ };
787
+
788
+ this.state.peerConnections.set(peerId, pc);
789
+ return pc;
790
+ },
791
+
792
+ /**
793
+ * Setup data channel handlers for CRDT sync.
794
+ * @private
795
+ * @param {RTCDataChannel} dc
796
+ * @param {string} peerId
797
+ */
798
+ _setupDataChannelHandlers(dc, peerId) {
799
+ dc.onopen = () => {
800
+ console.log(`[Collaboration] Data channel open with ${peerId}`);
801
+ this.state.dataChannels.set(peerId, dc);
802
+ };
803
+
804
+ dc.onmessage = (e) => {
805
+ try {
806
+ const message = JSON.parse(e.data);
807
+ this._handlePeerMessage(message);
808
+ } catch (err) {
809
+ console.error('[Collaboration] Failed to parse peer message:', err);
810
+ }
811
+ };
812
+
813
+ dc.onerror = (err) => {
814
+ console.error(`[Collaboration] Data channel error (${peerId}):`, err);
815
+ };
816
+
817
+ dc.onclose = () => {
818
+ console.log(`[Collaboration] Data channel closed with ${peerId}`);
819
+ this.state.dataChannels.delete(peerId);
820
+ };
821
+ },
822
+
823
+ /**
824
+ * Generate CRDT-based operation ID (clock + user UUID).
825
+ * Ensures deterministic ordering without central clock.
826
+ * @private
827
+ * @returns {string}
828
+ */
829
+ _generateOpId() {
830
+ const timestamp = Date.now().toString(36);
831
+ const random = Math.random().toString(36).slice(2);
832
+ const userPrefix = this.state.userId.slice(0, 4);
833
+ return `${timestamp}-${userPrefix}-${random}`;
834
+ },
835
+
836
+ // ========================================================================
837
+ // CURSOR PRESENCE & 3D VISUALIZATION (Fusion 360-parity)
838
+ // ========================================================================
839
+
840
+ /**
841
+ * Render peer cursor as 3D cone in viewport.
842
+ * Updates cursor position each frame.
843
+ * @private
844
+ * @param {Object} cursorData { userId, screenX, screenY, timestamp }
845
+ */
846
+ _renderPeerCursor(cursorData) {
847
+ const { userId, screenX, screenY } = cursorData;
848
+ const peer = this.state.peers.get(userId);
849
+ if (!peer) return;
850
+
851
+ // This would integrate with the 3D viewport
852
+ // In real app: convert screen coords to 3D via raycasting
853
+ const cursorElement = document.getElementById(`cursor-${userId}`);
854
+ if (cursorElement) {
855
+ cursorElement.style.left = screenX + 'px';
856
+ cursorElement.style.top = screenY + 'px';
857
+ }
858
+ },
859
+
860
+ /**
861
+ * Highlight geometry selected by another user.
862
+ * Shows which parts/faces other users have selected (different colors per user).
863
+ * @private
864
+ * @param {Object} selectionData { userId, partId, faceIndex, color }
865
+ */
866
+ _renderRemoteSelection(selectionData) {
867
+ const { userId, partId, color } = selectionData;
868
+ // Integration point with viewport module
869
+ // Would highlight part with semi-transparent overlay in user's color
870
+ this._broadcastEvent('collab:remoteSelectionChanged', selectionData);
871
+ },
872
+
873
+ /**
874
+ * Show "user is typing" indicator in chat.
875
+ * @private
876
+ * @param {string} userId
877
+ */
878
+ _showTypingIndicator(userId) {
879
+ const peer = this.state.peers.get(userId);
880
+ if (!peer) return;
881
+
882
+ const indicator = document.createElement('div');
883
+ indicator.className = 'collab-typing-indicator';
884
+ indicator.innerHTML = `<strong>${peer.name}</strong> is typing...`;
885
+ indicator.style.color = peer.color;
886
+ document.body.appendChild(indicator);
887
+
888
+ setTimeout(() => indicator.remove(), 3000);
889
+ },
890
+
891
+ /**
892
+ * Render comment annotation pinned to 3D geometry.
893
+ * Comments appear as numbered bubbles in the 3D view.
894
+ * @private
895
+ * @param {Object} comment { userId, text, partId, faceIndex, timestamp, resolved }
896
+ */
897
+ _renderGeometryComment(comment) {
898
+ const { userId, text, partId } = comment;
899
+ const peer = this.state.peers.get(userId);
900
+
901
+ const commentBubble = document.createElement('div');
902
+ commentBubble.className = 'collab-geometry-comment';
903
+ commentBubble.innerHTML = `
904
+ <div style="background: ${peer?.color || '#2196F3'}; color: white; padding: 8px 12px; border-radius: 4px; max-width: 200px;">
905
+ <strong>${peer?.name || 'User'}</strong>
906
+ <p style="margin: 4px 0; font-size: 12px;">${text}</p>
907
+ <small>${new Date(comment.timestamp).toLocaleTimeString()}</small>
908
+ </div>
909
+ `;
910
+ document.body.appendChild(commentBubble);
911
+ },
912
+
913
+ // ========================================================================
914
+ // VOICE CHAT & SPATIAL AUDIO (Fusion 360-parity)
915
+ // ========================================================================
916
+
917
+ /**
918
+ * Start voice chat session with peers.
919
+ * Uses WebRTC audio tracks for peer-to-peer audio.
920
+ * @async
921
+ * @returns {Promise<void>}
922
+ */
923
+ async startVoiceChat() {
924
+ try {
925
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
926
+
927
+ for (const [peerId, pc] of this.state.peerConnections) {
928
+ const audioTrack = stream.getAudioTracks()[0];
929
+ if (audioTrack) {
930
+ pc.addTrack(audioTrack, stream);
931
+ }
932
+ }
933
+
934
+ this._showNotification('Voice chat started', 'success');
935
+ this._broadcastEvent('collab:voiceChatStarted', {});
936
+ } catch (err) {
937
+ this._showNotification(`Voice chat failed: ${err.message}`, 'error');
938
+ }
939
+ },
940
+
941
+ /**
942
+ * Stop voice chat and close audio tracks.
943
+ * @async
944
+ * @returns {Promise<void>}
945
+ */
946
+ async stopVoiceChat() {
947
+ for (const pc of this.state.peerConnections.values()) {
948
+ pc.getSenders().forEach(sender => {
949
+ if (sender.track?.kind === 'audio') {
950
+ sender.track.stop();
951
+ }
952
+ });
953
+ }
954
+ this._showNotification('Voice chat stopped', 'info');
955
+ },
956
+
957
+ /**
958
+ * Enable spatial audio — voices come from cursor position in 3D.
959
+ * @param {boolean} enabled
960
+ */
961
+ setSpatialAudio(enabled) {
962
+ if (enabled) {
963
+ this._showNotification('Spatial audio enabled', 'success');
964
+ }
965
+ },
966
+
967
+ // ========================================================================
968
+ // CONFLICT RESOLUTION & MERGE DIALOG (Fusion 360-parity)
969
+ // ========================================================================
970
+
971
+ /**
972
+ * Show visual diff when two users modify same feature.
973
+ * Displays merge/pick-one dialog.
974
+ * @private
975
+ * @param {Object} conflict { feature, userId1, state1, userId2, state2 }
976
+ */
977
+ _showConflictDialog(conflict) {
978
+ const { feature, userId1, userId2, state1, state2 } = conflict;
979
+ const dialog = document.createElement('div');
980
+ dialog.className = 'collab-conflict-dialog';
981
+ dialog.innerHTML = `
982
+ <div style="padding: 16px; background: #fff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 500px;">
983
+ <h3>Merge Conflict</h3>
984
+ <p><strong>${feature}</strong> was modified by two users:</p>
985
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 12px 0;">
986
+ <div style="padding: 8px; background: #f0f0f0; border-radius: 4px; border-left: 3px solid #4CAF50;">
987
+ <strong>${this.state.peers.get(userId1)?.name || 'User 1'}</strong>
988
+ <pre style="margin: 4px 0; font-size: 11px; overflow-x: auto;">${JSON.stringify(state1, null, 2)}</pre>
989
+ </div>
990
+ <div style="padding: 8px; background: #f0f0f0; border-radius: 4px; border-left: 3px solid #2196F3;">
991
+ <strong>${this.state.peers.get(userId2)?.name || 'User 2'}</strong>
992
+ <pre style="margin: 4px 0; font-size: 11px; overflow-x: auto;">${JSON.stringify(state2, null, 2)}</pre>
993
+ </div>
994
+ </div>
995
+ <div style="display: flex; gap: 8px; justify-content: flex-end;">
996
+ <button class="btn btn-secondary" onclick="this.parentElement.parentElement.remove()">Cancel</button>
997
+ <button class="btn btn-primary" onclick="alert('Keep user 1 version')">Keep User 1</button>
998
+ <button class="btn btn-primary" onclick="alert('Keep user 2 version')">Keep User 2</button>
999
+ <button class="btn btn-primary" onclick="alert('Manual merge')">Manual Merge</button>
1000
+ </div>
1001
+ </div>
1002
+ `;
1003
+ document.body.appendChild(dialog);
1004
+ },
1005
+
1006
+ // ========================================================================
1007
+ // OFFLINE QUEUE & SYNC ON RECONNECT (Fusion 360-parity)
1008
+ // ========================================================================
1009
+
1010
+ /**
1011
+ * Queue operations when offline, sync on reconnect.
1012
+ * @private
1013
+ */
1014
+ state_offlineQueue: [],
1015
+
1016
+ /**
1017
+ * Buffer operation while offline.
1018
+ * @private
1019
+ * @param {Object} operation
1020
+ */
1021
+ _queueOfflineOperation(operation) {
1022
+ if (!navigator.onLine) {
1023
+ this.state_offlineQueue.push(operation);
1024
+ console.log(`[Collaboration] Queued operation (offline): ${operation.type}`);
1025
+ }
1026
+ },
1027
+
1028
+ /**
1029
+ * Flush offline queue when reconnected.
1030
+ * @private
1031
+ * @async
1032
+ */
1033
+ async _flushOfflineQueue() {
1034
+ while (this.state_offlineQueue.length > 0) {
1035
+ const op = this.state_offlineQueue.shift();
1036
+ try {
1037
+ await this.broadcastOperation(op);
1038
+ } catch (err) {
1039
+ console.error('[Collaboration] Failed to sync queued operation:', err);
1040
+ this.state_offlineQueue.unshift(op); // Re-queue if failed
1041
+ break;
1042
+ }
1043
+ }
1044
+ if (this.state_offlineQueue.length === 0) {
1045
+ this._showNotification('Offline changes synced', 'success');
1046
+ }
1047
+ },
1048
+
1049
+ /**
1050
+ * Listen for online/offline events.
1051
+ * @private
1052
+ */
1053
+ _setupOfflineSync() {
1054
+ window.addEventListener('online', () => {
1055
+ this._flushOfflineQueue();
1056
+ });
1057
+ },
1058
+
1059
+ // ========================================================================
1060
+ // SHARE LINKS WITH PERMISSIONS (Fusion 360-parity)
1061
+ // ========================================================================
1062
+
1063
+ /**
1064
+ * Generate a share link with read-only or edit access.
1065
+ * Optional expiry and password protection.
1066
+ * @async
1067
+ * @param {Object} options
1068
+ * @param {string} [options.access='view'] 'view' | 'edit'
1069
+ * @param {number} [options.expiryDays] Days until link expires
1070
+ * @param {string} [options.password] Optional password protection
1071
+ * @returns {Promise<string>} Share link
1072
+ */
1073
+ async generateShareLink(options = {}) {
1074
+ const { access = 'view', expiryDays, password } = options;
1075
+
1076
+ const token = this._generateUUID();
1077
+ const link = {
1078
+ token,
1079
+ roomCode: this.state.roomCode,
1080
+ access,
1081
+ createdAt: Date.now(),
1082
+ expiresAt: expiryDays ? Date.now() + expiryDays * 86400000 : null,
1083
+ password: password ? this._hashPassword(password) : null,
1084
+ };
1085
+
1086
+ // Store in localStorage (in real app: backend)
1087
+ const links = JSON.parse(localStorage.getItem('collab_shareLinks') || '[]');
1088
+ links.push(link);
1089
+ localStorage.setItem('collab_shareLinks', JSON.stringify(links));
1090
+
1091
+ const shareUrl = `${window.location.origin}?collab=${link.token}`;
1092
+ this._showNotification(`Share link copied`, 'success');
1093
+ return shareUrl;
1094
+ },
1095
+
1096
+ /**
1097
+ * Hash password for share link (basic, for demo only).
1098
+ * @private
1099
+ * @param {string} password
1100
+ * @returns {string}
1101
+ */
1102
+ _hashPassword(password) {
1103
+ return btoa(password); // Not real hashing, for demo
1104
+ },
1105
+
1106
+ /**
1107
+ * Join via share link with access validation.
1108
+ * @async
1109
+ * @param {string} token Share link token
1110
+ * @param {string} [password] Password if protected
1111
+ * @returns {Promise<boolean>}
1112
+ */
1113
+ async joinViaShareLink(token, password = null) {
1114
+ const links = JSON.parse(localStorage.getItem('collab_shareLinks') || '[]');
1115
+ const link = links.find(l => l.token === token);
1116
+
1117
+ if (!link) {
1118
+ throw new Error('Invalid share link');
1119
+ }
1120
+
1121
+ if (link.expiresAt && Date.now() > link.expiresAt) {
1122
+ throw new Error('Share link expired');
1123
+ }
1124
+
1125
+ if (link.password && this._hashPassword(password) !== link.password) {
1126
+ throw new Error('Incorrect password');
1127
+ }
1128
+
1129
+ // Join room with link's access level
1130
+ return this.joinRoom({
1131
+ code: link.roomCode,
1132
+ userName: 'Guest',
1133
+ });
1134
+ },
1135
+
1136
+ // ========================================================================
1137
+ // ACTIVITY FEED & NOTIFICATIONS (Fusion 360-parity)
1138
+ // ========================================================================
1139
+
1140
+ /**
1141
+ * Get activity timeline of all user actions.
1142
+ * @async
1143
+ * @param {Object} options
1144
+ * @param {number} [options.limit=50] Max entries to return
1145
+ * @returns {Promise<Array<Object>>}
1146
+ */
1147
+ async getActivityFeed(options = {}) {
1148
+ const { limit = 50 } = options;
1149
+
1150
+ const activities = [];
1151
+ for (const op of this.state.operationLog.slice(0, limit)) {
1152
+ const peer = this.state.peers.get(op.userId);
1153
+ activities.push({
1154
+ timestamp: op.timestamp,
1155
+ user: peer?.name || 'Unknown',
1156
+ action: op.type,
1157
+ details: op.params,
1158
+ lamportClock: op.lamportClock,
1159
+ });
1160
+ }
1161
+ return activities;
1162
+ },
1163
+
1164
+ /**
1165
+ * Send real-time notification when user joins/leaves/changes.
1166
+ * @private
1167
+ * @param {string} type 'join' | 'leave' | 'change'
1168
+ * @param {Object} userData
1169
+ */
1170
+ _notifyUserEvent(type, userData) {
1171
+ let message = '';
1172
+ switch (type) {
1173
+ case 'join':
1174
+ message = `${userData.name} joined the room`;
1175
+ break;
1176
+ case 'leave':
1177
+ message = `${userData.name} left the room`;
1178
+ break;
1179
+ case 'change':
1180
+ message = `${userData.name} is modeling`;
1181
+ break;
1182
+ }
1183
+ this._showNotification(message, 'info');
1184
+ },
1185
+
1186
+ // ========================================================================
1187
+ // FOLLOW MODE (Fusion 360-parity)
1188
+ // ========================================================================
1189
+
1190
+ /**
1191
+ * Follow another user's camera view.
1192
+ * Your viewport rotates/zooms to match their camera.
1193
+ * @async
1194
+ * @param {string} userId User to follow (null = stop following)
1195
+ * @returns {Promise<void>}
1196
+ */
1197
+ async followUser(userId) {
1198
+ if (userId) {
1199
+ this._showNotification(`Following ${this.state.peers.get(userId)?.name}`, 'info');
1200
+ } else {
1201
+ this._showNotification('Stopped following', 'info');
1202
+ }
1203
+ this.state.followingUserId = userId;
1204
+ this._broadcastEvent('collab:followingChanged', { userId });
1205
+ },
1206
+
1207
+ /**
1208
+ * Broadcast camera view to all followers.
1209
+ * Called every frame to sync camera position/rotation.
1210
+ * @private
1211
+ * @param {Object} cameraState { position, rotation, fov }
1212
+ */
1213
+ _broadcastCameraView(cameraState) {
1214
+ this._broadcastToPeers('cameraView', {
1215
+ userId: this.state.userId,
1216
+ camera: cameraState,
1217
+ timestamp: Date.now(),
1218
+ });
1219
+ },
1220
+
750
1221
  // ========================================================================
751
1222
  // HELP SYSTEM INTEGRATION
752
1223
  // ========================================================================
@@ -794,6 +1265,48 @@ export default {
794
1265
  category: 'Collaboration',
795
1266
  shortcut: null,
796
1267
  },
1268
+ {
1269
+ title: 'Voice Chat',
1270
+ description:
1271
+ 'Enable voice communication with room members. Audio tracks automatically flow between peers via WebRTC. Optional spatial audio makes voices come from cursor position.',
1272
+ category: 'Collaboration',
1273
+ shortcut: 'Ctrl+Alt+V',
1274
+ },
1275
+ {
1276
+ title: 'Comments on Geometry',
1277
+ description:
1278
+ 'Right-click a part or face → Add Comment. Comments appear as numbered bubbles pinned to geometry. Supports threaded replies and resolve/unresolve status.',
1279
+ category: 'Collaboration',
1280
+ shortcut: null,
1281
+ },
1282
+ {
1283
+ title: 'Generate Share Link',
1284
+ description:
1285
+ 'Share your room with a public link. Control access (view-only or edit), optional password protection, and link expiry. Share links appear in Collaborate panel.',
1286
+ category: 'Collaboration',
1287
+ shortcut: null,
1288
+ },
1289
+ {
1290
+ title: 'Activity Feed',
1291
+ description:
1292
+ 'View timeline of all changes in the room. Shows who did what, when, and the exact parameters they used. Useful for understanding what changed while you were away.',
1293
+ category: 'Collaboration',
1294
+ shortcut: null,
1295
+ },
1296
+ {
1297
+ title: 'Follow User',
1298
+ description:
1299
+ 'Click a user\'s avatar in the panel to follow their camera. Your viewport syncs with theirs in real-time. Click again to stop following.',
1300
+ category: 'Collaboration',
1301
+ shortcut: null,
1302
+ },
1303
+ {
1304
+ title: 'Conflict Resolution',
1305
+ description:
1306
+ 'If two users modify the same feature simultaneously, a visual diff dialog appears. Choose whose version to keep or manually merge.',
1307
+ category: 'Collaboration',
1308
+ shortcut: null,
1309
+ },
797
1310
  ],
798
1311
 
799
1312
  // ========================================================================