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.
- 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/DELIVERABLES.txt +296 -445
- 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/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- 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 +1342 -5031
- package/app/js/app.js +1312 -514
- 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/animation-module.js +497 -3
- package/app/js/modules/billing-module.js +724 -0
- package/app/js/modules/cam-module.js +507 -2
- package/app/js/modules/collaboration-module.js +513 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +544 -1146
- package/app/js/modules/formats-module.js +438 -738
- package/app/js/modules/inspection-module.js +393 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/plugin-module.js +597 -0
- package/app/js/modules/rendering-module.js +460 -0
- package/app/js/modules/scripting-module.js +593 -475
- package/app/js/modules/sketch-module.js +998 -2
- package/app/js/modules/step-module-enhanced.js +938 -0
- package/app/js/modules/surface-module.js +312 -0
- package/app/js/modules/version-module.js +420 -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/cycleCAD-Architecture-v2.pptx +0 -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
- 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
|
// ========================================================================
|