From e28806cc5a87a5408405c6adfe248d7d6bb090d3 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Fri, 12 Sep 2025 17:53:45 +0200 Subject: [PATCH] now supports multiple simulators, controller navigation tab has simulator menu, qr code urls set simulator to correspond with each simulator, user can switch controller's simulator with menu, menu updates when new simulators attach and leave. (#53) --- .../Resources/Prefabs/GroundPlane.prefab | 4 +- .../Assets/Scripts/Core/Brewster.cs | 14 +- .../Assets/Scripts/Core/SpaceCraft.cs | 62 +-- .../Assets/Scripts/Views/CollectionsView.cs | 18 +- .../Schemas/Generated/CollectionSchema.cs | 16 +- .../Views/Schemas/Generated/ItemSchema.cs | 22 +- .../StreamingAssets/SpaceCraft/spacecraft.js | 413 +++++++++++------- WebSites/SpaceCraft/Build/SpaceCraft.data | 4 +- .../SpaceCraft/Build/SpaceCraft.framework.js | 18 +- .../SpaceCraft/Build/SpaceCraft.loader.js | 2 +- WebSites/SpaceCraft/Build/SpaceCraft.wasm | 4 +- .../StreamingAssets/SpaceCraft/spacecraft.js | 413 +++++++++++------- WebSites/controller/build/SimulatorState.d.ts | 2 - .../controller/build/SimulatorState.d.ts.map | 2 +- WebSites/controller/build/SimulatorState.js | 8 - .../controller/build/SimulatorState.js.map | 2 +- .../controller/build/SpacetimeController.d.ts | 5 +- .../build/SpacetimeController.d.ts.map | 2 +- .../controller/build/SpacetimeController.js | 150 ++++++- .../build/SpacetimeController.js.map | 2 +- WebSites/controller/build/TabNavigate.d.ts | 1 + .../controller/build/TabNavigate.d.ts.map | 2 +- WebSites/controller/build/TabNavigate.js | 42 +- WebSites/controller/build/TabNavigate.js.map | 2 +- WebSites/controller/src/SimulatorState.ts | 8 - .../controller/src/SpacetimeController.ts | 148 ++++++- WebSites/controller/src/TabNavigate.ts | 45 +- 27 files changed, 931 insertions(+), 480 deletions(-) diff --git a/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab b/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab index 08070dda..5402b6bb 100644 --- a/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab +++ b/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab @@ -49,8 +49,8 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1019336410923223640} - m_Enabled: 0 - m_CastShadows: 1 + m_Enabled: 1 + m_CastShadows: 0 m_ReceiveShadows: 1 m_DynamicOccludee: 1 m_StaticShadowCaster: 0 diff --git a/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs b/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs index 9aac2ff8..70ff903e 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs @@ -96,7 +96,7 @@ public void LoadContentFromJson(JObject content) return; } - Debug.Log("Brewster: Starting to load content from JSON"); + // Debug.Log("Brewster: Starting to load content from JSON"); //Debug.Log($"Brewster: Content JSON structure: {content}"); // Clear existing content first @@ -144,24 +144,24 @@ public void LoadContentFromJson(JObject content) Collection collection = ScriptableObject.CreateInstance(); // Debug the JSON before import - Debug.Log($"Brewster: About to import collection '{collectionId}' from JSON: {collectionJson}"); + // Debug.Log($"Brewster: About to import collection '{collectionId}' from JSON: {collectionJson}"); // Import from the nested collection object collection.ImportFromJToken(collectionJson); // Debug the collection after import - Debug.Log($"Brewster: After import - collection ID='{collection.Id}', Title='{collection.Title}'"); + // Debug.Log($"Brewster: After import - collection ID='{collection.Id}', Title='{collection.Title}'"); // Collection should already have ID from ImportFromJToken, // but if not, use the collection ID from the JSON key if (string.IsNullOrEmpty(collection.Id)) { - Debug.Log($"Brewster: Setting explicit ID for collection '{collectionId}' as it was not found in metadata"); + // Debug.Log($"Brewster: Setting explicit ID for collection '{collectionId}' as it was not found in metadata"); collection.Id = collectionId; } _collections[collectionId] = collection; - Debug.Log($"Brewster: Successfully added collection '{collectionId}' with ID '{collection.Id}'"); + // Debug.Log($"Brewster: Successfully added collection '{collectionId}' with ID '{collection.Id}'"); // Check for itemsIndex in the collection JArray itemsIndex = collectionDir["itemsIndex"] as JArray; @@ -238,7 +238,7 @@ public void LoadContentFromJson(JObject content) // If no ID from JSON, use the key as ID if (string.IsNullOrEmpty(item.Id)) { - Debug.Log($"Brewster: Setting explicit ID for item '{itemId}' as it was not found in metadata"); + // Debug.Log($"Brewster: Setting explicit ID for item '{itemId}' as it was not found in metadata"); item.Id = itemId; } @@ -256,7 +256,7 @@ public void LoadContentFromJson(JObject content) } // Notify that content is loaded - Debug.Log($"Brewster: Content loading complete. Loaded {_collections.Count} collections and {_items.Count} items from provided JSON."); + // Debug.Log($"Brewster: Content loading complete. Loaded {_collections.Count} collections and {_items.Count} items from provided JSON."); // Fire the event to notify all listeners OnAllContentLoaded?.Invoke(); diff --git a/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs b/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs index 6ea10f15..b31ce516 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs @@ -74,10 +74,10 @@ public List SelectedItemIds HashSet newSet = new HashSet(value ?? new List()); // Handle null input if (!currentSet.SetEquals(newSet)) { - Debug.Log($"[SpaceCraft] Setter: Updating SelectedItemIds (Count: {newSet.Count})"); + // Debug.Log($"[SpaceCraft] Setter: Updating SelectedItemIds (Count: {newSet.Count})"); _selectedItemIds = new List(newSet); // Store a copy defensively selectedItemsChanged = true; // Set flag because state list changed - Debug.Log($"[SpaceCraft] Setter: SelectedItemIds updated to {string.Join(", ", _selectedItemIds)}"); + // Debug.Log($"[SpaceCraft] Setter: SelectedItemIds updated to {string.Join(", ", _selectedItemIds)}"); UpdateSelectionVisuals(); } } @@ -95,10 +95,10 @@ public List HighlightedItemIds var newValue = value ?? new List(); if (!AreEquivalentHighlightLists(_highlightedItemIds, newValue)) { - Debug.Log($"[SpaceCraft] Setter: Updating HighlightedItemIds (Count: {newValue.Count})"); + // Debug.Log($"[SpaceCraft] Setter: Updating HighlightedItemIds (Count: {newValue.Count})"); _highlightedItemIds = new List(newValue); // Store a copy highlightedItemsChanged = true; // Set flag because state list changed - Debug.Log($"[SpaceCraft] Setter: HighlightedItemIds updated to {string.Join(", ", _highlightedItemIds)}"); + // Debug.Log($"[SpaceCraft] Setter: HighlightedItemIds updated to {string.Join(", ", _highlightedItemIds)}"); UpdateHighlightVisuals(); } } @@ -340,18 +340,18 @@ private void FixedUpdate() string firstItemId = collectionsView.GetFirstDisplayedItemId(); if (!string.IsNullOrEmpty(firstItemId)) { - Debug.Log($"[SpaceCraft] Automatically selecting first item: {firstItemId}"); + // Debug.Log($"[SpaceCraft] Automatically selecting first item: {firstItemId}"); if (SelectedItemIds.Count > 0) { SelectedItemIds.Clear(); selectedItemsChanged = true; - Debug.Log($"[SpaceCraft] Setter: SelectedItemIds cleared to {string.Join(", ", SelectedItemIds)}"); + // Debug.Log($"[SpaceCraft] Setter: SelectedItemIds cleared to {string.Join(", ", SelectedItemIds)}"); } SelectItem("auto_select", "System", firstItemId); } else { - Debug.Log("[SpaceCraft] No first item found to select automatically."); + // Debug.Log("[SpaceCraft] No first item found to select automatically."); } } @@ -429,7 +429,7 @@ public void SelectItem(string clientId, string clientName, string itemId) { if (string.IsNullOrEmpty(itemId)) return; // Log client info - Debug.Log($"[SpaceCraft] SelectItem API called by {clientName}({clientId}): {itemId}"); + // Debug.Log($"[SpaceCraft] SelectItem API called by {clientName}({clientId}): {itemId}"); List newList; // Handle single selection mode @@ -461,7 +461,7 @@ public void DeselectItem(string clientId, string clientName, string itemId) { if (string.IsNullOrEmpty(itemId) || !_selectedItemIds.Contains(itemId)) return; // Log client info - Debug.Log($"[SpaceCraft] DeselectItem API called by {clientName}({clientId}): {itemId}"); + // Debug.Log($"[SpaceCraft] DeselectItem API called by {clientName}({clientId}): {itemId}"); // Create a new list without the item List newList = new List(_selectedItemIds); @@ -489,7 +489,7 @@ public void DeselectItem(string clientId, string clientName, string itemId) /// public void DeselectAllItems() { - Debug.Log("[SpaceCraft] DeselectAllItems API called"); + // Debug.Log("[SpaceCraft] DeselectAllItems API called"); if (_selectedItemIds.Count > 0) { // Assign an empty list to the property @@ -548,17 +548,17 @@ public void ToggleCurrentItemSelection(string clientId, string clientName, strin public void ToggleItemSelection(string clientId, string clientName, string screenId, string itemId) { if (string.IsNullOrEmpty(itemId)) return; - Debug.Log($"[SpaceCraft] ToggleItemSelection API called by clientId: {clientId}, clientName: {clientName}, screenId: {screenId}, itemId: {itemId}"); + // Debug.Log($"[SpaceCraft] ToggleItemSelection API called by clientId: {clientId}, clientName: {clientName}, screenId: {screenId}, itemId: {itemId}"); List newList = new List(_selectedItemIds); if (newList.Contains(itemId)) { // Item is selected, so deselect it - Debug.Log($"[SpaceCraft] Toggle: Deselecting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Deselecting {itemId}"); newList.Remove(itemId); } else { // Item is not selected, so select it - Debug.Log($"[SpaceCraft] Toggle: Selecting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Selecting {itemId}"); if (!multiSelectEnabled) { // Single select mode: clear list before adding @@ -591,7 +591,7 @@ public void SetSelectedItems(string clientId, string clientName, List it { List newList = itemIds ?? new List(); // Log client info - Debug.Log($"[SpaceCraft] SetSelectedItems API called by {clientName}({clientId}) with {newList.Count} items."); + // Debug.Log($"[SpaceCraft] SetSelectedItems API called by {clientName}({clientId}) with {newList.Count} items."); // Enforce single selection mode if active if (!multiSelectEnabled && newList.Count > 1) @@ -621,7 +621,7 @@ public void AddSelectedItems(string clientId, string clientName, List it { if (itemIds == null || itemIds.Count == 0) return; // Log client info - Debug.Log($"[SpaceCraft] AddSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] AddSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); // Handle single select mode separately if (!multiSelectEnabled) @@ -662,7 +662,7 @@ public void RemoveSelectedItems(string clientId, string clientName, List { if (itemIds == null || itemIds.Count == 0 || _selectedItemIds.Count == 0) return; // Log client info - Debug.Log($"[SpaceCraft] RemoveSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] RemoveSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); List newList = new List(_selectedItemIds); bool changed = false; @@ -696,7 +696,7 @@ public void RemoveSelectedItems(string clientId, string clientName, List public void SetMultiSelectMode(bool enable) { if (inputManager == null) return; - Debug.Log($"[SpaceCraft] SetMultiSelectMode called: {enable}"); + // Debug.Log($"[SpaceCraft] SetMultiSelectMode called: {enable}"); if (multiSelectEnabled != enable) { @@ -737,7 +737,7 @@ public int GetHighlightCount(string itemId) /// public void SetHighlightedItems(string clientId, string clientName, List itemIds) { - Debug.Log($"[SpaceCraft] SetHighlightedItems API called by {clientName}({clientId}) with {itemIds?.Count ?? 0} items."); + // Debug.Log($"[SpaceCraft] SetHighlightedItems API called by {clientName}({clientId}) with {itemIds?.Count ?? 0} items."); // Assign the new list to the property to trigger updates if different HighlightedItemIds = itemIds ?? new List(); @@ -749,7 +749,7 @@ public void SetHighlightedItems(string clientId, string clientName, List public void AddHighlightedItems(string clientId, string clientName, List itemIds) { if (itemIds == null || itemIds.Count == 0) return; - Debug.Log($"[SpaceCraft] AddHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] AddHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); // Create new list, add items, and assign back to property List newList = new List(HighlightedItemIds); @@ -763,7 +763,7 @@ public void AddHighlightedItems(string clientId, string clientName, List public void RemoveHighlightedItems(string clientId, string clientName, List itemIds) { if (itemIds == null || itemIds.Count == 0 || HighlightedItemIds.Count == 0) return; - Debug.Log($"[SpaceCraft] RemoveHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] RemoveHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); // Create new list and remove one instance of each specified item List newList = new List(HighlightedItemIds); @@ -972,7 +972,7 @@ private ItemView FindItemSafe(string itemId) ItemView itemView = FindItemViewById(itemId); // Call local method if (itemView == null) { - Debug.LogWarning($"[SpaceCraft] FindItemSafe: Item with ID '{itemId}' not found via CollectionsView."); + // Debug.LogWarning($"[SpaceCraft] FindItemSafe: Item with ID '{itemId}' not found via CollectionsView."); } return itemView; @@ -997,7 +997,7 @@ public void UpdateSelectionVisuals() { if (inputManager == null) return; - Debug.Log($"[SpaceCraft] Updating selection visuals for {SelectedItemIds.Count} selected items."); + // Debug.Log($"[SpaceCraft] Updating selection visuals for {SelectedItemIds.Count} selected items."); List allItemViews = inputManager.GetAllItemViews(); foreach (ItemView view in allItemViews) @@ -1051,7 +1051,7 @@ public void UpdateHighlightVisuals() { if (inputManager == null) return; - Debug.Log($"[SpaceCraft] Updating highlight visuals for {HighlightedItemIds.Count} total highlights."); + // Debug.Log($"[SpaceCraft] Updating highlight visuals for {HighlightedItemIds.Count} total highlights."); List allItemViews = inputManager.GetAllItemViews(); // Calculate counts first @@ -1082,15 +1082,15 @@ private void SendEvents() // Send events only when changes have occurred - reduce bridge traffic if (selectedItemsChanged) { - Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); + // Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); SendEventName("SelectedItemsChanged"); selectedItemsChanged = false; // Reset flag here - Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); + // Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); } if (highlightedItemsChanged) { - Debug.Log("SpaceCraft: HighlightedItemsChanged: highlightedItemIds: " + string.Join(",", HighlightedItemIds)); + // Debug.Log("SpaceCraft: HighlightedItemsChanged: highlightedItemIds: " + string.Join(",", HighlightedItemIds)); SendEventName("HighlightedItemsChanged"); highlightedItemsChanged = false; // Reset flag here } @@ -1129,17 +1129,17 @@ public void OnItemHighlightEnd(ItemView item) public void ToggleItemHighlight(string controllerId, string controllerName, string screenId, string itemId) { if (string.IsNullOrEmpty(itemId)) return; - Debug.Log($"[SpaceCraft] ToggleItemHighlight API called by controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, itemId: {itemId}"); + // Debug.Log($"[SpaceCraft] ToggleItemHighlight API called by controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, itemId: {itemId}"); List newList = new List(HighlightedItemIds); if (newList.Contains(itemId)) { // Item is highlighted, so unhighlight it - Debug.Log($"[SpaceCraft] Toggle: Unhighlighting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Unhighlighting {itemId}"); newList.Remove(itemId); } else { // Item is not highlighted, so highlight it - Debug.Log($"[SpaceCraft] Toggle: Highlighting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Highlighting {itemId}"); newList.Add(itemId); } // Assign the potentially modified list back to the property @@ -1152,7 +1152,7 @@ public void ToggleItemHighlight(string controllerId, string controllerName, stri public void MoveSelection(string controllerId, string controllerName, string screenId, string direction, float dx = 0f, float dy = 0f) { // Expects "north", "south", "east", "west" from controller - Debug.Log($"[SpaceCraft] MoveSelection called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}, dx: {dx}, dy: {dy}"); + // Debug.Log($"[SpaceCraft] MoveSelection called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}, dx: {dx}, dy: {dy}"); // Pass direction and mouse deltas to CollectionsView collectionsView?.MoveSelection(controllerId, controllerName, direction, dx, dy); // Note: CollectionsView will call SelectItem which will sync the highlight @@ -1164,7 +1164,7 @@ public void MoveSelection(string controllerId, string controllerName, string scr public void MoveHighlight(string controllerId, string controllerName, string screenId, string direction) { // Expects "north", "south", "east", "west" from controller - Debug.Log($"[SpaceCraft] MoveHighlight called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}"); + // Debug.Log($"[SpaceCraft] MoveHighlight called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}"); // Pass direction directly to CollectionsView collectionsView?.MoveHighlight(controllerId, controllerName, direction); } diff --git a/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs b/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs index db60ed8f..362c2a57 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs @@ -35,7 +35,7 @@ private void Start() } else { - Debug.Log($"CollectionsView: itemInfoPanel is assigned: {itemInfoPanel.name}"); + // Debug.Log($"CollectionsView: itemInfoPanel is assigned: {itemInfoPanel.name}"); } // Ensure Brewster instance exists @@ -115,7 +115,7 @@ public void DisplayAllCollections() } } - Debug.Log($"CollectionsView: Displayed {collectionViews.Count} collections."); + // Debug.Log($"CollectionsView: Displayed {collectionViews.Count} collections."); } /// @@ -177,7 +177,7 @@ private CollectionView CreateCollectionView(Collection collection) /// private void HandleAllContentLoaded() { - Debug.Log("CollectionsView: Received OnAllContentLoaded event."); + // Debug.Log("CollectionsView: Received OnAllContentLoaded event."); // Hide details panel initially HideItemDetails(); @@ -215,7 +215,7 @@ private void UpdateDetailPanel() } else { - Debug.LogWarning($"CollectionsView: UpdateDetailPanel - Could not find ItemView for selected id: {selectedId}"); + // Debug.LogWarning($"CollectionsView: UpdateDetailPanel - Could not find ItemView for selected id: {selectedId}"); } } } @@ -227,7 +227,7 @@ private void UpdateDetailPanel() if (currentDisplayedItem != itemToDisplay) { currentDisplayedItem = itemToDisplay; - Debug.Log($"CollectionsView: UpdateDetailPanel - Displaying item details for: {itemToDisplay.Title}"); + // Debug.Log($"CollectionsView: UpdateDetailPanel - Displaying item details for: {itemToDisplay.Title}"); DisplayItemDetails(itemToDisplay); } } @@ -237,7 +237,7 @@ private void UpdateDetailPanel() if (currentDisplayedItem != null) { currentDisplayedItem = null; - Debug.Log("CollectionsView: UpdateDetailPanel - Hiding item details (no item to display)"); + // Debug.Log("CollectionsView: UpdateDetailPanel - Hiding item details (no item to display)"); HideItemDetails(); } } @@ -269,12 +269,12 @@ public void DisplayItemDetails(Item item) if (itemView != null && !string.IsNullOrEmpty(itemView.collectionId)) { Collection collection = Brewster.Instance.GetCollection(itemView.collectionId); - Debug.Log($"CollectionsView: Retrieved collection with ID='{collection?.Id}', Title='{collection?.Title}', Type={collection?.GetType().Name}"); + // Debug.Log($"CollectionsView: Retrieved collection with ID='{collection?.Id}', Title='{collection?.Title}', Type={collection?.GetType().Name}"); if (collection != null) { - Debug.Log($"CollectionsView: About to format title - collection.Title='{collection.Title}', item.Title='{item.Title}'"); + // Debug.Log($"CollectionsView: About to format title - collection.Title='{collection.Title}', item.Title='{item.Title}'"); displayTitle = $"{collection.Title}\n{item.Title}"; - Debug.Log($"CollectionsView: Final formatted displayTitle='{displayTitle}'"); + // Debug.Log($"CollectionsView: Final formatted displayTitle='{displayTitle}'"); } else { diff --git a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs index b1419fd3..95daff53 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs @@ -117,7 +117,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'id' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: id is null" ); + // Debug.Log($"ItemSchema: id is null" ); } // Use converter: StringOrNullToStringConverter @@ -132,7 +132,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'title' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: title is null" ); + // Debug.Log($"ItemSchema: title is null" ); } // Use converter: StringArrayOrStringOrNullToStringConverter @@ -147,7 +147,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'description' with StringArrayOrStringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: description is null" ); + // Debug.Log($"ItemSchema: description is null" ); } // Use converter: StringOrNullToStringConverter @@ -162,7 +162,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'creator' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: creator is null" ); + // Debug.Log($"ItemSchema: creator is null" ); } // Use converter: SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter @@ -177,7 +177,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'subject' with SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: subject is null" ); + // Debug.Log($"ItemSchema: subject is null" ); } // Use converter: StringOrNullToStringConverter @@ -192,7 +192,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'mediatype' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: mediatype is null" ); + // Debug.Log($"ItemSchema: mediatype is null" ); } // Use converter: StringOrNullToStringConverter @@ -207,7 +207,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverImage' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverImage is null" ); + // Debug.Log($"ItemSchema: coverImage is null" ); } // Use converter: StringOrNullToStringConverter @@ -222,7 +222,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'query' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: query is null" ); + // Debug.Log($"ItemSchema: query is null" ); } } diff --git a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs index 19d54a3f..156550f1 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs @@ -141,7 +141,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'id' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: id is null" ); + // Debug.Log($"ItemSchema: id is null" ); } // Use converter: StringOrNullToStringConverter @@ -156,7 +156,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'title' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: title is null" ); + // Debug.Log($"ItemSchema: title is null" ); } // Use converter: StringArrayOrStringOrNullToStringConverter @@ -171,7 +171,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'description' with StringArrayOrStringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: description is null" ); + // Debug.Log($"ItemSchema: description is null" ); } // Use converter: StringArrayOrStringOrNullToStringConverter @@ -186,7 +186,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'creator' with StringArrayOrStringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: creator is null" ); + // Debug.Log($"ItemSchema: creator is null" ); } // Use converter: SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter @@ -201,7 +201,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'subject' with SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: subject is null" ); + // Debug.Log($"ItemSchema: subject is null" ); } // Use converter: SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter @@ -216,7 +216,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'tags' with SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: tags is null" ); + // Debug.Log($"ItemSchema: tags is null" ); } // Use converter: StringArrayOrStringOrNullToStringArrayConverter @@ -231,7 +231,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'collection' with StringArrayOrStringOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: collection is null" ); + // Debug.Log($"ItemSchema: collection is null" ); } // Use converter: StringOrNullToStringConverter @@ -246,7 +246,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'mediatype' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: mediatype is null" ); + // Debug.Log($"ItemSchema: mediatype is null" ); } // Use converter: StringOrNullToStringConverter @@ -261,7 +261,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverImage' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverImage is null" ); + // Debug.Log($"ItemSchema: coverImage is null" ); } // Use converter: StringOrNumberOrNullToIntegerConverter @@ -276,7 +276,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverWidth' with StringOrNumberOrNullToIntegerConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverWidth is null" ); + // Debug.Log($"ItemSchema: coverWidth is null" ); } // Use converter: StringOrNumberOrNullToIntegerConverter @@ -291,7 +291,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverHeight' with StringOrNumberOrNullToIntegerConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverHeight is null" ); + // Debug.Log($"ItemSchema: coverHeight is null" ); } } diff --git a/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js b/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js index 12a8fcf3..c9ef6b1e 100644 --- a/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js +++ b/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js @@ -71,10 +71,6 @@ // - action can be "tap" or directional ("north","south","east","west","up","down") // - "tap" applies a scale impulse to the highlighted item // - Directions call MoveHighlight -// 'broadcast' { event: 'simulatorTakeover' }: -// - {newSimulatorId, newSimulatorName, startTime} -// - Signals that a new simulator has taken control -// - Used to manage multiple simulator instances // // --- PRESENCE EVENTS (Client connection tracking) --- // 'presence' { event: 'sync' }: Full state of all connected clients @@ -116,6 +112,7 @@ class SpaceCraftSim { static deepIndexPath = 'StreamingAssets/Content/index-deep.json'; static controllerHtmlPath = '../controller/'; static clientChannelName = 'spacecraft'; + static simulatorNamePrefix = 'SpaceCraft'; /** * Get the channel name from URL query parameter or use default @@ -124,7 +121,10 @@ class SpaceCraftSim { static getChannelName() { const urlParams = new URLSearchParams(window.location.search); const channelFromUrl = urlParams.get('channel'); - return channelFromUrl || this.clientChannelName; + if (channelFromUrl) return channelFromUrl; + // Fallback: use the host name of the web page URL instead of the hard-coded default + const host = window.location && window.location.hostname; + return host; } /** @@ -138,7 +138,8 @@ class SpaceCraftSim { this.identity = { clientId: this.clientId, // Unique ID for this simulator instance clientType: "simulator", // Fixed type for simulator - clientName: "SpaceCraft Simulator", // Human-readable name + clientName: SpaceCraftSim.simulatorNamePrefix, // Start without a number; will assign on presence + simulatorIndex: 0, startTime: Date.now() // When this simulator instance started }; @@ -158,6 +159,8 @@ class SpaceCraftSim { // Supabase channel reference this.clientChannel = null; + this._indexClaims = []; + this._indexTimer = null; this.presenceVersion = 0; // For tracking changes to presence state // Fetch timeout reference @@ -196,7 +199,7 @@ class SpaceCraftSim { * Initializes the content promise to fetch data early */ initContentPromise() { - console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); + // console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); window.contentPromise = fetch(SpaceCraftSim.deepIndexPath) .then(response => { @@ -218,7 +221,7 @@ class SpaceCraftSim { if (this.domContentLoaded) return; // Prevent double execution this.domContentLoaded = true; - console.log("[SpaceCraft] DOM loaded. Initializing QR code."); + // console.log("[SpaceCraft] DOM loaded. Initializing QR code."); // Check for QR Code library dependency if (typeof QRCode === 'undefined') { @@ -226,12 +229,16 @@ class SpaceCraftSim { return; // Stop further initialization if QR code can't be generated } - // Generate QR code - this.generateQRCodes(); + // Generate QR code only after simulatorIndex is assigned; otherwise defer + if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) { + this.generateQRCodes(); + } else { + try { console.log('[Sim] QR generation deferred until simulatorIndex is assigned'); } catch {} + } // Basic initialization is considered complete after DOM/QR setup this.isInitialized = true; - console.log("[SpaceCraft] DOM and QR code initialization complete."); + // console.log("[SpaceCraft] DOM and QR code initialization complete."); // Now that the DOM is ready, proceed to configure and load Unity this.configureAndLoadUnity(); @@ -241,7 +248,7 @@ class SpaceCraftSim { * Generate QR code for controller based on qrCodeDefinitions. */ generateQRCodes() { - console.log("[SpaceCraft] Generating QR code based on definitions..."); + // console.log("[SpaceCraft] Generating QR code based on definitions..."); const qrContainer = document.getElementById('qrcodes-container'); if (!qrContainer) { @@ -273,6 +280,8 @@ class SpaceCraftSim { if (currentChannel !== SpaceCraftSim.clientChannelName) { qrParams.set('channel', currentChannel); } + // Include simulator index so controllers target the right simulator + try { if (this.simulatorIndex != null) qrParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} const currentSearchParams = qrParams.toString() ? '?' + qrParams.toString() : ''; @@ -284,19 +293,19 @@ class SpaceCraftSim { // Ensure it ends with a slash if not already present baseDirectory = explicitBaseUrl.endsWith('/') ? explicitBaseUrl : explicitBaseUrl + '/'; usingExplicitUrl = true; - console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); } else { // Fallback: Calculate the base directory path from window.location baseDirectory = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1); - console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); } - if (!usingExplicitUrl) { - console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); - } - console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); + // if (!usingExplicitUrl) { + // console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // } + // console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); // Loop through the defined QR codes this.qrCodeDefinitions.forEach(definition => { @@ -307,6 +316,8 @@ class SpaceCraftSim { if (definition.type) { qrSpecificParams.set('type', definition.type); } + // Always add simulator index to the QR links once assigned + try { if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) qrSpecificParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} // Build the complete search params string const finalSearchParams = qrSpecificParams.toString() ? '?' + qrSpecificParams.toString() : ''; @@ -316,32 +327,25 @@ class SpaceCraftSim { // Construct the full absolute URL for the QR code message const fullAbsoluteUrl = new URL(targetRelativeUrl, baseDirectory).toString(); - console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); - console.log(` - Relative URL: ${targetRelativeUrl}`); - console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); + // console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); + // console.log(` - Relative URL: ${targetRelativeUrl}`); + // console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); - // 1. Create the link element (will act as a button/link) + // 1. Create the link element (standard anchor with href for accessibility/copyability) const linkElement = document.createElement('a'); linkElement.classList.add('qrcode-link'); // Add a general class for styling - linkElement.style.cursor = 'pointer'; + linkElement.style.cursor = 'pointer'; + linkElement.href = fullAbsoluteUrl; // Allow right-click/copy link + linkElement.target = '_blank'; + linkElement.rel = 'noopener noreferrer'; // Add position class if defined if (definition.position) { linkElement.classList.add(`qr-position-${definition.position}`); } - // Define window features (optional, but helps encourage a new window) - const windowFeatures = 'resizable=yes,scrollbars=yes'; - // Define a unique window name based on the definition ID - const windowName = definition.id + '-window'; // e.g., "navigator-qr-window" - - // Add onclick handler to open in a new window - linkElement.onclick = (event) => { - event.preventDefault(); // Prevent any default link behavior if href was somehow still present - console.log(`[SpaceCraft QR Click] Opening ${windowName} for ${definition.label} with URL: ${fullAbsoluteUrl}`); - window.open(fullAbsoluteUrl, windowName, windowFeatures); - return false; // Prevent further event propagation - }; + // Define a unique window name if needed (left unused since standard anchor navigation is enabled) + // const windowName = definition.id + '-window'; // 2. Generate the QR code SVG const qrSvgElement = QRCode({ @@ -364,7 +368,8 @@ class SpaceCraftSim { qrContainer.appendChild(linkElement); }); - console.log("[SpaceCraft] QR code generated successfully."); + // console.log("[SpaceCraft] QR code generated successfully."); + this.qrCodesGenerated = true; } catch (error) { console.error("[SpaceCraft] Error generating QR code:", error); @@ -376,7 +381,7 @@ class SpaceCraftSim { * Configure and initiate the loading of the Unity instance. */ configureAndLoadUnity() { - console.log("[SpaceCraft] Configuring Unity..."); + // console.log("[SpaceCraft] Configuring Unity..."); // Ensure Bridge is available (should be loaded via script tag before this) window.bridge = window.bridge || new Bridge(); @@ -384,7 +389,7 @@ class SpaceCraftSim { console.error("[SpaceCraft] CRITICAL: Bridge object not found or invalid!"); return; // Cannot proceed without the Bridge } - console.log("[SpaceCraft] Bridge instance checked/created."); + // console.log("[SpaceCraft] Bridge instance checked/created."); // --- Unity Loader Configuration --- // Note: Template variables like {{{ LOADER_FILENAME }}} are replaced by Unity during build. @@ -392,7 +397,7 @@ class SpaceCraftSim { // IMPORTANT: Make sure these template variables match your Unity WebGL template settings const loaderUrl = buildUrl + "/SpaceCraft.loader.js"; // Assuming default naming - console.log("[SpaceCraft] Unity loader URL:", loaderUrl); + // console.log("[SpaceCraft] Unity loader URL:", loaderUrl); const config = { dataUrl: buildUrl + "/SpaceCraft.data", @@ -406,7 +411,7 @@ class SpaceCraftSim { // memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}", // symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}", }; - console.log("[SpaceCraft] Unity configuration prepared:", config); + // console.log("[SpaceCraft] Unity configuration prepared:", config); // --- Get DOM Elements --- const container = document.querySelector("#unity-container"); @@ -418,20 +423,20 @@ class SpaceCraftSim { console.error("[SpaceCraft] Required DOM elements (#unity-container, #unity-canvas, #unity-fullscreen-button) not found."); return; // Cannot proceed without essential DOM elements } - console.log("[SpaceCraft] Unity DOM elements retrieved."); + // console.log("[SpaceCraft] Unity DOM elements retrieved."); // Force canvas fullscreen sizing canvas.style.width = "100%"; canvas.style.height = "100%"; // --- Load Unity Script --- - console.log("[SpaceCraft] Creating Unity loader script element..."); + // console.log("[SpaceCraft] Creating Unity loader script element..."); const script = document.createElement("script"); script.src = loaderUrl; // --- Define Unity Instance Creation Logic (runs after loader script loads) --- script.onload = () => { - console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); + // console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); // Check if createUnityInstance function exists (it should be defined by loaderUrl script) if (typeof createUnityInstance === 'undefined') { @@ -441,20 +446,20 @@ class SpaceCraftSim { createUnityInstance(canvas, config, (progress) => { // Optional: Update loading progress UI - console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); + //console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); // if (progressBarFull) { // progressBarFull.style.width = 100 * progress + "%"; // } }).then((unityInstance) => { // Unity Instance Creation Complete - console.log("[SpaceCraft] Unity instance created successfully."); + // console.log("[SpaceCraft] Unity instance created successfully."); // Store Unity instance globally for access window.unityInstance = unityInstance; // Setup fullscreen button functionality fullscreenButton.onclick = () => { - console.log("[SpaceCraft] Fullscreen button clicked."); + // console.log("[SpaceCraft] Fullscreen button clicked."); unityInstance.SetFullscreen(1); }; @@ -462,9 +467,9 @@ class SpaceCraftSim { // This tells the Bridge JS library that Unity is ready and provides the instance. // The Bridge library internally handles linking with the Unity instance. // Bridge C# code (BridgeTransportWebGL.Awake/Start) will eventually send "StartedUnity". - console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); + // console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); window.bridge.start("WebGL", JSON.stringify({})); // Empty config for now - console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); + // console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); }).catch((message) => { console.error("[SpaceCraft] Error creating Unity instance:", message); @@ -473,7 +478,7 @@ class SpaceCraftSim { // --- Add Loader Script to Document --- document.body.appendChild(script); - console.log("[SpaceCraft] Unity loader script added to document."); + // console.log("[SpaceCraft] Unity loader script added to document."); } /** @@ -487,7 +492,7 @@ class SpaceCraftSim { try { const content = await window.contentPromise; if (content) { - console.log("[SpaceCraft] Successfully loaded content from early fetch"); + // console.log("[SpaceCraft] Successfully loaded content from early fetch"); return content; } } catch (earlyFetchError) { @@ -496,7 +501,7 @@ class SpaceCraftSim { } // Direct fetch if early fetch failed or wasn't available - console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); + // console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); const response = await fetch(SpaceCraftSim.deepIndexPath); if (!response.ok) { @@ -504,9 +509,7 @@ class SpaceCraftSim { } const content = await response.json(); - console.log("[SpaceCraft] Content fetch successful, got:", - Object.keys(content).join(", ") - ); + // console.log("[SpaceCraft] Content fetch successful, got:", Object.keys(content).join(", ")); // Return the exact content as-is, expecting it to be correctly formatted return content; @@ -522,7 +525,7 @@ class SpaceCraftSim { */ async loadCollectionsAndCreateSpaceCraft() { - console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); + // console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); // Ensure basic initialization (DOM, QR code) happened. It should have by now. if (!this.isInitialized) { @@ -559,11 +562,11 @@ class SpaceCraftSim { // Extract tags from loaded content items this.availableTags = this.createUnifiedTagsList(); this.state.tags = this.availableTags; - console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); + // console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); if (this.availableTags.length > 0) { - console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); + // console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); } else { - console.log("[SpaceCraft] No tags found in content items"); + // console.log("[SpaceCraft] No tags found in content items"); } this.setupSupabase(); @@ -574,7 +577,7 @@ class SpaceCraftSim { // Create the SpaceCraft object via Bridge - pass content exactly as received this.createSpaceCraftObject(this.loadedContent); - console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); + // console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); } /** @@ -582,7 +585,7 @@ class SpaceCraftSim { * @param {Object} content - The content data to initialize SpaceCraft with */ createSpaceCraftObject(content) { - console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); + // console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); // Create the actual SpaceCraft object via Bridge with content data this.spaceCraft = window.bridge.createObject({ @@ -597,9 +600,9 @@ class SpaceCraftSim { createMagnet: function (magnetData) { try { - console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); } let magnetId = magnetData.magnetId; if (!magnetId) { @@ -627,9 +630,9 @@ class SpaceCraftSim { updateMagnet: function (magnetData) { try { - console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); } const magnetId = magnetData.magnetId; if (!magnetId) { @@ -672,7 +675,7 @@ class SpaceCraftSim { window.bridge.destroyObject(magnetBridge); - console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); + // console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); return true; }, @@ -710,9 +713,9 @@ class SpaceCraftSim { "unityMetaData": "UnityMetaData" }, handler: (obj, results) => { - console.log("[SpaceCraft] ContentLoaded event received from Unity"); - console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); - console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); + // console.log("[SpaceCraft] ContentLoaded event received from Unity"); + // console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); + // console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); this.state.unityMetaData = results.unityMetaData; } }, @@ -753,7 +756,10 @@ class SpaceCraftSim { // Create the ground plane as a child of SpaceCraft this.groundPlane = window.bridge.createObject({ prefab: "Prefabs/GroundPlane", - parent: this.spaceCraft + parent: this.spaceCraft, + update: { + "transform:Cube/component:MeshRenderer/material/color": { r: 0.0, g: .2, b: 0.0 }, + } }); // Store references globally @@ -771,7 +777,8 @@ class SpaceCraftSim { // Client identity clientType: 'simulator', clientId: this.clientId, - clientName: 'Spacecraft Simulator', + clientName: 'Spacecraft', + simulatorIndex: 0, // Collection/screen state currentScreenId: 'main', @@ -833,15 +840,26 @@ class SpaceCraftSim { } } - /** - * Syncs the current state to Supabase presence - */ + mirrorIdentityToState() { + try { + if (!this.state) return; + this.state.clientId = this.identity.clientId; + this.state.clientType = this.identity.clientType; + this.state.clientName = this.identity.clientName; + // simulatorIndex may be undefined for non-simulators; default to 0 + this.state.simulatorIndex = (typeof this.identity.simulatorIndex === 'number') ? this.identity.simulatorIndex : 0; + try { console.log('[Sim] mirrorIdentityToState:', { id: this.state.clientId, name: this.state.clientName, simulatorIndex: this.state.simulatorIndex }); } catch {} + } catch {} + } + syncStateToPresence() { if (!this.clientChannel) { - console.warn("[SpaceCraft] Attempted to sync presence, but clientChannel is null."); return; } - + // Ensure identity is reflected in state before publishing + this.mirrorIdentityToState(); + + try { console.log('[Sim] syncStateToPresence track shared:', { simulatorIndex: this.state && this.state.simulatorIndex, clientName: this.state && this.state.clientName }); } catch {} this.clientChannel.track({ ...this.identity, shared: { ...this.state } @@ -852,7 +870,11 @@ class SpaceCraftSim { setupSupabase() { const channelName = SpaceCraftSim.getChannelName(); - console.log(`[SpaceCraft] Setting up Supabase client with channel: ${channelName}`); + try { console.log('[Sim] setupSupabase channel =', channelName); } catch {} + if (!window.supabase || typeof window.supabase.createClient !== 'function') { + console.error('[Sim] Supabase JS library missing or invalid on simulator page'); + return; + } // Create a Supabase client const client = window.supabase.createClient( @@ -861,14 +883,38 @@ class SpaceCraftSim { ); // Create a channel for client communication - this.clientChannel = client.channel(channelName); - + this.clientChannel = client.channel(channelName, { config: { presence: { key: this.identity.clientId } } }); + try { console.log('[Sim] clientChannel created for', channelName, 'with presence key', this.identity.clientId); } catch {} + this._indexClaims = []; + this._indexTimer = null; + this.clientChannel .on('broadcast', {}, (data) => { - console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); + // console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); if (data.payload && data.payload.targetSimulatorId) { - console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + // console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + } + }) + + .on('broadcast', { event: 'indexClaim' }, (data) => { + try { + const claim = data && data.payload || {}; + if (!claim.clientId || typeof claim.index !== 'number') return; + const now = Date.now(); + this._indexClaims.push({ clientId: claim.clientId, index: claim.index, ts: now }); + // keep recent 2s + this._indexClaims = this._indexClaims.filter(c => now - c.ts < 2000); + try { + console.log('[Sim] received indexClaim:', { clientId: claim.clientId, index: claim.index, ts: now }); + console.log('[Sim] indexClaim window (<=2s):', this._indexClaims); + } catch {} + } catch {} + }) + + .on('presence', { event: 'sync' }, () => { + try { console.log('[Sim] presence:sync received'); this.ensureUniqueSimulatorName(); } catch (e) { + console.warn('[SpaceCraft] ensureUniqueSimulatorName error:', e); } }) @@ -943,7 +989,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -976,7 +1022,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -1064,36 +1110,15 @@ class SpaceCraftSim { this.spaceCraft.pushMagnet(magnetId, deltaX, deltaY); }) - .on('broadcast', { event: 'simulatorTakeover' }, (data) => { - // Another simulator is trying to take over - if (data.payload.newSimulatorId === this.identity.clientId) { - return; // Our own takeover message, ignore - } - - // If this simulator started later than the new one, let it take over - if (this.identity.startTime < data.payload.startTime) { - console.log(`[SpaceCraft] Another simulator has taken over: ${data.payload.newSimulatorId}`); - this.shutdown(); - } else { - console.log(`[SpaceCraft] Ignoring takeover from older simulator: ${data.payload.newSimulatorId}`); - this.sendTakeoverEvent(); - } - }) - - .on('presence', { event: 'sync' }, () => { - const allPresences = this.clientChannel.presenceState(); - }) - .on('presence', { event: 'join' }, ({ newPresences }) => { for (const presence of newPresences) { // Skip our own presence if (presence.clientId === this.identity.clientId) continue; + // console.log(`[SpaceCraft] Another ${presence.clientType} joined: ${presence.clientId} ${presence.clientName}`); + // Check if this is a simulator joining if (presence.clientType === "simulator") { - console.log(`[SpaceCraft] Another simulator joined: ${presence.clientId}`); - // Send takeover event to establish dominance - this.sendTakeoverEvent(); } else { // A controller client joined this.updateClientInfo( @@ -1109,73 +1134,133 @@ class SpaceCraftSim { for (const presence of leftPresences) { // Remove from our client registry if (this.clients[presence.clientId]) { - console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); + // console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); delete this.clients[presence.clientId]; } } }) - .subscribe((status) => { + .subscribe((status) => { + try { console.log('[Sim] subscribe status:', status); } catch {} if (status === 'SUBSCRIBED') { - console.log("[SpaceCraft] Successfully subscribed to client channel"); - + try { console.log('[Sim] SUBSCRIBED: tracking presence and starting index negotiation'); } catch {} + try { console.log('[Sim] track identity:', { clientId: this.identity.clientId, clientName: this.identity.clientName, simulatorIndex: this.identity.simulatorIndex }); } catch {} this.clientChannel.track({ ...this.identity, - shared: { ...this.state } // Nest state under 'shared' + shared: { ...this.state } }); - - this.sendTakeoverEvent(); + try { this.ensureUniqueSimulatorName(); } catch {} } }); + // Attempt to restore previously assigned simulator index for this channel (persists across reconnects) + // Intentionally NOT restoring simulatorIndex from localStorage to avoid duplicate indices across tabs + try { console.log('[Sim] init: not restoring simulatorIndex from localStorage; will negotiate via presence'); } catch {} + this.syncStateToPresence(); } - /** - * Sends a takeover event to notify other simulators and clients - */ - sendTakeoverEvent() { - if (!this.clientChannel) { - console.warn("[SpaceCraft] Cannot send takeover event: Client channel not initialized."); - return; - } - - // console.log(`[SpaceCraft] Sending simulator takeover notification`); - - this.clientChannel.send({ - type: 'broadcast', - event: 'simulatorTakeover', - payload: { - newSimulatorId: this.identity.clientId, - newSimulatorName: this.identity.clientName, - startTime: this.identity.startTime - } - }).catch(err => console.error("[SpaceCraft] Error sending takeover event:", err)); + broadcastIndexClaim(index) { + try { + console.log('[Sim] broadcastIndexClaim sending', { index, clientId: this.identity && this.identity.clientId }); + this.clientChannel && this.clientChannel.send({ + type: 'broadcast', + event: 'indexClaim', + payload: { clientId: this.identity.clientId, index } + }); + } catch {} } - - /** - * Gracefully shuts down this simulator instance - */ - shutdown() { - console.log("[SpaceCraft] Shutting down simulator due to takeover"); - - // Clean up resources - no try/catch, just direct error logging - if (this.clientChannel) { - this.clientChannel.unsubscribe().catch(err => { - console.error("[SpaceCraft] Error unsubscribing from channel:", err); + + ensureUniqueSimulatorName() { + try { + // If already assigned, nothing to do + if (this.simulatorIndex && this.simulatorIndex > 0) return; + const state = this.clientChannel && this.clientChannel.presenceState ? this.clientChannel.presenceState() : null; + if (!state) return; + const prefix = SpaceCraftSim.simulatorNamePrefix; + // Compute observed max from presence (explicit index, name suffix) and shared maxIndexSeen + try { console.log('[Sim] ensureUniqueSimulatorName start; presence keys=', Object.keys(state || {})); } catch {} + let totalSimulators = 0; + let presenceMax = 0; + let maxSeenShared = 0; + Object.values(state).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + totalSimulators += 1; + try { console.log('[Sim] presence simulator:', { clientId: p.clientId, nameTop: p.clientName, nameShared: p.shared && p.shared.clientName, idxTop: p.simulatorIndex, idxShared: p.shared && p.shared.simulatorIndex, maxIndexSeen: p.shared && p.shared.maxIndexSeen }); } catch {} + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > presenceMax) presenceMax = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > presenceMax) presenceMax = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const m = p.clientName.match(/(\d+)\s*$/); + if (m) { + const n = parseInt(m[1], 10); + if (n > presenceMax) presenceMax = n; + } + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > maxSeenShared) maxSeenShared = p.shared.maxIndexSeen; + } + }); }); - } - - // Redirect to a shutdown page or reload - alert("Another SpaceCraft simulator has taken control. This window will now close."); - window.close(); - - // If window doesn't close (e.g., window not opened by script), reload - setTimeout(() => { - window.location.reload(); - }, 1000); + // Do not read maxIndexSeen from localStorage; avoid cross-tab coupling + const now = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now - c.ts < 2000); + const claimsMax = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const base = Math.max(presenceMax, maxSeenShared, claimsMax); + const sameBaseClaims = this._indexClaims.filter(c => c.index === base); + const rank = sameBaseClaims.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const proposed = (totalSimulators <= 1 && base === 0) ? 1 : (base + rank + 1); + console.log(`[Sim] proposal: ts=${now} tot=${totalSimulators} presenceMax=${presenceMax} maxSeenShared=${maxSeenShared} claimsMax=${claimsMax} base=${base} rank=${rank} proposed=${proposed}`); + this.broadcastIndexClaim(proposed); + // finalize after a short coordination window + clearTimeout(this._indexTimer); + this._indexTimer = setTimeout(() => { + const now2 = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now2 - c.ts < 2000); + const claimsMax2 = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const presenceMax2 = (() => { + let m = 0; + Object.values(this.clientChannel.presenceState() || {}).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > m) m = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > m) m = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const mm = p.clientName.match(/(\d+)\s*$/); + if (mm) m = Math.max(m, parseInt(mm[1], 10)); + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > m) m = p.shared.maxIndexSeen; + } + }); + }); + return m; + })(); + const base2 = Math.max(presenceMax2, claimsMax2); + const sameBase2 = this._indexClaims.filter(c => c.index === base2); + const rank2 = sameBase2.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const finalIndex = (final => (final > 0 ? final : 1))( (base2 + rank2 + 1) ); + console.log(`[Sim] finalize: claimsMax2=${claimsMax2} presenceMax2=${presenceMax2} base2=${base2} rank2=${rank2} finalIndex=${finalIndex}`); + if (!this.simulatorIndex || this.simulatorIndex === 0) { + this.simulatorIndex = finalIndex; + const nextName = `${prefix} ${this.simulatorIndex}`; + this.identity.clientName = nextName; + this.identity.simulatorIndex = this.simulatorIndex; + this.state.clientName = nextName; + this.state.simulatorIndex = this.simulatorIndex; + const newMaxSeen = Math.max(base2, this.simulatorIndex); + this.state.maxIndexSeen = newMaxSeen; + // Do not persist indices to localStorage to avoid cross-tab duplication + console.log(`[Sim] assigned: simulatorIndex=${this.simulatorIndex} clientName="${this.identity.clientName}"`); + this.syncStateToPresence(); + if (this.domContentLoaded) { + this.generateQRCodes(); + } + } + }, 400); + } catch {} } + // Takeover functionality removed: multi-simulator is supported + /** * Sends a Supabase broadcast event on the channel. * @param {string} clientId - The unique ID of the target client. @@ -1194,7 +1279,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); + // console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); // Add simulator ID to payload so client knows who sent it const fullPayload = { @@ -1220,7 +1305,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); + // console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); // Add simulator ID to payload so clients know who sent it const fullPayload = { @@ -1272,13 +1357,7 @@ class SpaceCraftSim { * Logs the current status of the SpaceCraft instance */ logStatus() { - console.log("[SpaceCraft Debug] Status:", { - DOMReady: this.domContentLoaded, - BasicInitialized: this.isInitialized, - BridgeAvailable: !!window.bridge, - SpaceCraft: !!this.spaceCraft, - SupabaseLoaded: typeof window.supabase !== 'undefined' - }); + // console.log("[SpaceCraft Debug] Status:", { DOMReady: this.domContentLoaded, BasicInitialized: this.isInitialized, BridgeAvailable: !!window.bridge, SpaceCraft: !!this.spaceCraft, SupabaseLoaded: typeof window.supabase !== 'undefined' }); } /** @@ -1354,7 +1433,7 @@ class SpaceCraftSim { */ createUnifiedTagsList() { if (!this.loadedContent || !this.loadedContent.collections) { - console.log("[SpaceCraft] No collections data available for creating tags list"); + // console.log("[SpaceCraft] No collections data available for creating tags list"); return []; } @@ -1380,7 +1459,7 @@ class SpaceCraftSim { } const sortedTags = Array.from(allTags).sort(); - console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); + // console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); return sortedTags; } } @@ -1408,4 +1487,4 @@ if (document.readyState === 'loading') { // setInterval(() => window.SpaceCraft.logStatus(), 5000); } -console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); +// console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.data b/WebSites/SpaceCraft/Build/SpaceCraft.data index 18d18e88..0036c9d5 100644 --- a/WebSites/SpaceCraft/Build/SpaceCraft.data +++ b/WebSites/SpaceCraft/Build/SpaceCraft.data @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7edc96cf0521ff7a52a41dc0bb07a08d609542314970a20ee4217cf1af03589b -size 10233506 +oid sha256:f959831d24b6f35803df1ae390b3cab7d3e34c2824fb5cf522734c50f6774fd6 +size 10229906 diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.framework.js b/WebSites/SpaceCraft/Build/SpaceCraft.framework.js index 30956cd2..89351c75 100644 --- a/WebSites/SpaceCraft/Build/SpaceCraft.framework.js +++ b/WebSites/SpaceCraft/Build/SpaceCraft.framework.js @@ -1277,10 +1277,10 @@ function dbg(text) { // === Body === var ASM_CONSTS = { - 5876176: () => { Module['emscripten_get_now_backup'] = performance.now; }, - 5876231: ($0) => { performance.now = function() { return $0; }; }, - 5876279: ($0) => { performance.now = function() { return $0; }; }, - 5876327: () => { performance.now = Module['emscripten_get_now_backup']; } + 5875856: () => { Module['emscripten_get_now_backup'] = performance.now; }, + 5875911: ($0) => { performance.now = function() { return $0; }; }, + 5875959: ($0) => { performance.now = function() { return $0; }; }, + 5876007: () => { performance.now = Module['emscripten_get_now_backup']; } }; @@ -18751,9 +18751,9 @@ var wasmImports = { "invoke_viiififiii": invoke_viiififiii, "invoke_viiifii": invoke_viiifii, "invoke_viiii": invoke_viiii, + "invoke_viiiiffi": invoke_viiiiffi, "invoke_viiiifi": invoke_viiiifi, "invoke_viiiii": invoke_viiiii, - "invoke_viiiiiffi": invoke_viiiiiffi, "invoke_viiiiii": invoke_viiiiii, "invoke_viiiiiifii": invoke_viiiiiifii, "invoke_viiiiiii": invoke_viiiiiii, @@ -19152,7 +19152,7 @@ var dynCall_vjiiiii = Module["dynCall_vjiiiii"] = createExportWrapper("dynCall_v /** @type {function(...*):?} */ var dynCall_jiiiii = Module["dynCall_jiiiii"] = createExportWrapper("dynCall_jiiiii"); /** @type {function(...*):?} */ -var dynCall_viiiiiffi = Module["dynCall_viiiiiffi"] = createExportWrapper("dynCall_viiiiiffi"); +var dynCall_viiiiffi = Module["dynCall_viiiiffi"] = createExportWrapper("dynCall_viiiiffi"); /** @type {function(...*):?} */ var dynCall_viiffii = Module["dynCall_viiffii"] = createExportWrapper("dynCall_viiffii"); /** @type {function(...*):?} */ @@ -19206,7 +19206,7 @@ var dynCall_vifffffi = Module["dynCall_vifffffi"] = createExportWrapper("dynCall /** @type {function(...*):?} */ var dynCall_viffffffi = Module["dynCall_viffffffi"] = createExportWrapper("dynCall_viffffffi"); /** @type {function(...*):?} */ -var dynCall_viiiiffi = Module["dynCall_viiiiffi"] = createExportWrapper("dynCall_viiiiffi"); +var dynCall_viiiiiffi = Module["dynCall_viiiiiffi"] = createExportWrapper("dynCall_viiiiiffi"); /** @type {function(...*):?} */ var dynCall_ijiii = Module["dynCall_ijiii"] = createExportWrapper("dynCall_ijiii"); /** @type {function(...*):?} */ @@ -20460,10 +20460,10 @@ function invoke_viiff(index,a1,a2,a3,a4) { } } -function invoke_viiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8) { +function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7) { var sp = stackSave(); try { - dynCall_viiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8); + dynCall_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7); } catch(e) { stackRestore(sp); if (e !== e+0) throw e; diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.loader.js b/WebSites/SpaceCraft/Build/SpaceCraft.loader.js index 194cdf62..67b91e12 100644 --- a/WebSites/SpaceCraft/Build/SpaceCraft.loader.js +++ b/WebSites/SpaceCraft/Build/SpaceCraft.loader.js @@ -54,7 +54,7 @@ function createUnityInstance(canvas, config, onProgress) { preserveDrawingBuffer: false, powerPreference: 2, }, - wasmFileSize: 82679103, + wasmFileSize: 82670661, cacheControl: function (url) { return (url == Module.dataUrl || url.match(/\.bundle/)) ? "must-revalidate" : "no-store"; }, diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.wasm b/WebSites/SpaceCraft/Build/SpaceCraft.wasm index f1d9cdc7..9d58a0ce 100755 --- a/WebSites/SpaceCraft/Build/SpaceCraft.wasm +++ b/WebSites/SpaceCraft/Build/SpaceCraft.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:745f28ebcb1b888a55ff325d9f0efa4e01fc0ade972a956ddf37495d41095b18 -size 82679103 +oid sha256:e686457720fff6e536ad2728e990487564e68058ad4762a04b3772f3b6562606 +size 82670661 diff --git a/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js b/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js index 12a8fcf3..c9ef6b1e 100644 --- a/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js +++ b/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js @@ -71,10 +71,6 @@ // - action can be "tap" or directional ("north","south","east","west","up","down") // - "tap" applies a scale impulse to the highlighted item // - Directions call MoveHighlight -// 'broadcast' { event: 'simulatorTakeover' }: -// - {newSimulatorId, newSimulatorName, startTime} -// - Signals that a new simulator has taken control -// - Used to manage multiple simulator instances // // --- PRESENCE EVENTS (Client connection tracking) --- // 'presence' { event: 'sync' }: Full state of all connected clients @@ -116,6 +112,7 @@ class SpaceCraftSim { static deepIndexPath = 'StreamingAssets/Content/index-deep.json'; static controllerHtmlPath = '../controller/'; static clientChannelName = 'spacecraft'; + static simulatorNamePrefix = 'SpaceCraft'; /** * Get the channel name from URL query parameter or use default @@ -124,7 +121,10 @@ class SpaceCraftSim { static getChannelName() { const urlParams = new URLSearchParams(window.location.search); const channelFromUrl = urlParams.get('channel'); - return channelFromUrl || this.clientChannelName; + if (channelFromUrl) return channelFromUrl; + // Fallback: use the host name of the web page URL instead of the hard-coded default + const host = window.location && window.location.hostname; + return host; } /** @@ -138,7 +138,8 @@ class SpaceCraftSim { this.identity = { clientId: this.clientId, // Unique ID for this simulator instance clientType: "simulator", // Fixed type for simulator - clientName: "SpaceCraft Simulator", // Human-readable name + clientName: SpaceCraftSim.simulatorNamePrefix, // Start without a number; will assign on presence + simulatorIndex: 0, startTime: Date.now() // When this simulator instance started }; @@ -158,6 +159,8 @@ class SpaceCraftSim { // Supabase channel reference this.clientChannel = null; + this._indexClaims = []; + this._indexTimer = null; this.presenceVersion = 0; // For tracking changes to presence state // Fetch timeout reference @@ -196,7 +199,7 @@ class SpaceCraftSim { * Initializes the content promise to fetch data early */ initContentPromise() { - console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); + // console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); window.contentPromise = fetch(SpaceCraftSim.deepIndexPath) .then(response => { @@ -218,7 +221,7 @@ class SpaceCraftSim { if (this.domContentLoaded) return; // Prevent double execution this.domContentLoaded = true; - console.log("[SpaceCraft] DOM loaded. Initializing QR code."); + // console.log("[SpaceCraft] DOM loaded. Initializing QR code."); // Check for QR Code library dependency if (typeof QRCode === 'undefined') { @@ -226,12 +229,16 @@ class SpaceCraftSim { return; // Stop further initialization if QR code can't be generated } - // Generate QR code - this.generateQRCodes(); + // Generate QR code only after simulatorIndex is assigned; otherwise defer + if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) { + this.generateQRCodes(); + } else { + try { console.log('[Sim] QR generation deferred until simulatorIndex is assigned'); } catch {} + } // Basic initialization is considered complete after DOM/QR setup this.isInitialized = true; - console.log("[SpaceCraft] DOM and QR code initialization complete."); + // console.log("[SpaceCraft] DOM and QR code initialization complete."); // Now that the DOM is ready, proceed to configure and load Unity this.configureAndLoadUnity(); @@ -241,7 +248,7 @@ class SpaceCraftSim { * Generate QR code for controller based on qrCodeDefinitions. */ generateQRCodes() { - console.log("[SpaceCraft] Generating QR code based on definitions..."); + // console.log("[SpaceCraft] Generating QR code based on definitions..."); const qrContainer = document.getElementById('qrcodes-container'); if (!qrContainer) { @@ -273,6 +280,8 @@ class SpaceCraftSim { if (currentChannel !== SpaceCraftSim.clientChannelName) { qrParams.set('channel', currentChannel); } + // Include simulator index so controllers target the right simulator + try { if (this.simulatorIndex != null) qrParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} const currentSearchParams = qrParams.toString() ? '?' + qrParams.toString() : ''; @@ -284,19 +293,19 @@ class SpaceCraftSim { // Ensure it ends with a slash if not already present baseDirectory = explicitBaseUrl.endsWith('/') ? explicitBaseUrl : explicitBaseUrl + '/'; usingExplicitUrl = true; - console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); } else { // Fallback: Calculate the base directory path from window.location baseDirectory = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1); - console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); } - if (!usingExplicitUrl) { - console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); - } - console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); + // if (!usingExplicitUrl) { + // console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // } + // console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); // Loop through the defined QR codes this.qrCodeDefinitions.forEach(definition => { @@ -307,6 +316,8 @@ class SpaceCraftSim { if (definition.type) { qrSpecificParams.set('type', definition.type); } + // Always add simulator index to the QR links once assigned + try { if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) qrSpecificParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} // Build the complete search params string const finalSearchParams = qrSpecificParams.toString() ? '?' + qrSpecificParams.toString() : ''; @@ -316,32 +327,25 @@ class SpaceCraftSim { // Construct the full absolute URL for the QR code message const fullAbsoluteUrl = new URL(targetRelativeUrl, baseDirectory).toString(); - console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); - console.log(` - Relative URL: ${targetRelativeUrl}`); - console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); + // console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); + // console.log(` - Relative URL: ${targetRelativeUrl}`); + // console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); - // 1. Create the link element (will act as a button/link) + // 1. Create the link element (standard anchor with href for accessibility/copyability) const linkElement = document.createElement('a'); linkElement.classList.add('qrcode-link'); // Add a general class for styling - linkElement.style.cursor = 'pointer'; + linkElement.style.cursor = 'pointer'; + linkElement.href = fullAbsoluteUrl; // Allow right-click/copy link + linkElement.target = '_blank'; + linkElement.rel = 'noopener noreferrer'; // Add position class if defined if (definition.position) { linkElement.classList.add(`qr-position-${definition.position}`); } - // Define window features (optional, but helps encourage a new window) - const windowFeatures = 'resizable=yes,scrollbars=yes'; - // Define a unique window name based on the definition ID - const windowName = definition.id + '-window'; // e.g., "navigator-qr-window" - - // Add onclick handler to open in a new window - linkElement.onclick = (event) => { - event.preventDefault(); // Prevent any default link behavior if href was somehow still present - console.log(`[SpaceCraft QR Click] Opening ${windowName} for ${definition.label} with URL: ${fullAbsoluteUrl}`); - window.open(fullAbsoluteUrl, windowName, windowFeatures); - return false; // Prevent further event propagation - }; + // Define a unique window name if needed (left unused since standard anchor navigation is enabled) + // const windowName = definition.id + '-window'; // 2. Generate the QR code SVG const qrSvgElement = QRCode({ @@ -364,7 +368,8 @@ class SpaceCraftSim { qrContainer.appendChild(linkElement); }); - console.log("[SpaceCraft] QR code generated successfully."); + // console.log("[SpaceCraft] QR code generated successfully."); + this.qrCodesGenerated = true; } catch (error) { console.error("[SpaceCraft] Error generating QR code:", error); @@ -376,7 +381,7 @@ class SpaceCraftSim { * Configure and initiate the loading of the Unity instance. */ configureAndLoadUnity() { - console.log("[SpaceCraft] Configuring Unity..."); + // console.log("[SpaceCraft] Configuring Unity..."); // Ensure Bridge is available (should be loaded via script tag before this) window.bridge = window.bridge || new Bridge(); @@ -384,7 +389,7 @@ class SpaceCraftSim { console.error("[SpaceCraft] CRITICAL: Bridge object not found or invalid!"); return; // Cannot proceed without the Bridge } - console.log("[SpaceCraft] Bridge instance checked/created."); + // console.log("[SpaceCraft] Bridge instance checked/created."); // --- Unity Loader Configuration --- // Note: Template variables like {{{ LOADER_FILENAME }}} are replaced by Unity during build. @@ -392,7 +397,7 @@ class SpaceCraftSim { // IMPORTANT: Make sure these template variables match your Unity WebGL template settings const loaderUrl = buildUrl + "/SpaceCraft.loader.js"; // Assuming default naming - console.log("[SpaceCraft] Unity loader URL:", loaderUrl); + // console.log("[SpaceCraft] Unity loader URL:", loaderUrl); const config = { dataUrl: buildUrl + "/SpaceCraft.data", @@ -406,7 +411,7 @@ class SpaceCraftSim { // memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}", // symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}", }; - console.log("[SpaceCraft] Unity configuration prepared:", config); + // console.log("[SpaceCraft] Unity configuration prepared:", config); // --- Get DOM Elements --- const container = document.querySelector("#unity-container"); @@ -418,20 +423,20 @@ class SpaceCraftSim { console.error("[SpaceCraft] Required DOM elements (#unity-container, #unity-canvas, #unity-fullscreen-button) not found."); return; // Cannot proceed without essential DOM elements } - console.log("[SpaceCraft] Unity DOM elements retrieved."); + // console.log("[SpaceCraft] Unity DOM elements retrieved."); // Force canvas fullscreen sizing canvas.style.width = "100%"; canvas.style.height = "100%"; // --- Load Unity Script --- - console.log("[SpaceCraft] Creating Unity loader script element..."); + // console.log("[SpaceCraft] Creating Unity loader script element..."); const script = document.createElement("script"); script.src = loaderUrl; // --- Define Unity Instance Creation Logic (runs after loader script loads) --- script.onload = () => { - console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); + // console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); // Check if createUnityInstance function exists (it should be defined by loaderUrl script) if (typeof createUnityInstance === 'undefined') { @@ -441,20 +446,20 @@ class SpaceCraftSim { createUnityInstance(canvas, config, (progress) => { // Optional: Update loading progress UI - console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); + //console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); // if (progressBarFull) { // progressBarFull.style.width = 100 * progress + "%"; // } }).then((unityInstance) => { // Unity Instance Creation Complete - console.log("[SpaceCraft] Unity instance created successfully."); + // console.log("[SpaceCraft] Unity instance created successfully."); // Store Unity instance globally for access window.unityInstance = unityInstance; // Setup fullscreen button functionality fullscreenButton.onclick = () => { - console.log("[SpaceCraft] Fullscreen button clicked."); + // console.log("[SpaceCraft] Fullscreen button clicked."); unityInstance.SetFullscreen(1); }; @@ -462,9 +467,9 @@ class SpaceCraftSim { // This tells the Bridge JS library that Unity is ready and provides the instance. // The Bridge library internally handles linking with the Unity instance. // Bridge C# code (BridgeTransportWebGL.Awake/Start) will eventually send "StartedUnity". - console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); + // console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); window.bridge.start("WebGL", JSON.stringify({})); // Empty config for now - console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); + // console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); }).catch((message) => { console.error("[SpaceCraft] Error creating Unity instance:", message); @@ -473,7 +478,7 @@ class SpaceCraftSim { // --- Add Loader Script to Document --- document.body.appendChild(script); - console.log("[SpaceCraft] Unity loader script added to document."); + // console.log("[SpaceCraft] Unity loader script added to document."); } /** @@ -487,7 +492,7 @@ class SpaceCraftSim { try { const content = await window.contentPromise; if (content) { - console.log("[SpaceCraft] Successfully loaded content from early fetch"); + // console.log("[SpaceCraft] Successfully loaded content from early fetch"); return content; } } catch (earlyFetchError) { @@ -496,7 +501,7 @@ class SpaceCraftSim { } // Direct fetch if early fetch failed or wasn't available - console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); + // console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); const response = await fetch(SpaceCraftSim.deepIndexPath); if (!response.ok) { @@ -504,9 +509,7 @@ class SpaceCraftSim { } const content = await response.json(); - console.log("[SpaceCraft] Content fetch successful, got:", - Object.keys(content).join(", ") - ); + // console.log("[SpaceCraft] Content fetch successful, got:", Object.keys(content).join(", ")); // Return the exact content as-is, expecting it to be correctly formatted return content; @@ -522,7 +525,7 @@ class SpaceCraftSim { */ async loadCollectionsAndCreateSpaceCraft() { - console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); + // console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); // Ensure basic initialization (DOM, QR code) happened. It should have by now. if (!this.isInitialized) { @@ -559,11 +562,11 @@ class SpaceCraftSim { // Extract tags from loaded content items this.availableTags = this.createUnifiedTagsList(); this.state.tags = this.availableTags; - console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); + // console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); if (this.availableTags.length > 0) { - console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); + // console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); } else { - console.log("[SpaceCraft] No tags found in content items"); + // console.log("[SpaceCraft] No tags found in content items"); } this.setupSupabase(); @@ -574,7 +577,7 @@ class SpaceCraftSim { // Create the SpaceCraft object via Bridge - pass content exactly as received this.createSpaceCraftObject(this.loadedContent); - console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); + // console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); } /** @@ -582,7 +585,7 @@ class SpaceCraftSim { * @param {Object} content - The content data to initialize SpaceCraft with */ createSpaceCraftObject(content) { - console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); + // console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); // Create the actual SpaceCraft object via Bridge with content data this.spaceCraft = window.bridge.createObject({ @@ -597,9 +600,9 @@ class SpaceCraftSim { createMagnet: function (magnetData) { try { - console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); } let magnetId = magnetData.magnetId; if (!magnetId) { @@ -627,9 +630,9 @@ class SpaceCraftSim { updateMagnet: function (magnetData) { try { - console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); } const magnetId = magnetData.magnetId; if (!magnetId) { @@ -672,7 +675,7 @@ class SpaceCraftSim { window.bridge.destroyObject(magnetBridge); - console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); + // console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); return true; }, @@ -710,9 +713,9 @@ class SpaceCraftSim { "unityMetaData": "UnityMetaData" }, handler: (obj, results) => { - console.log("[SpaceCraft] ContentLoaded event received from Unity"); - console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); - console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); + // console.log("[SpaceCraft] ContentLoaded event received from Unity"); + // console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); + // console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); this.state.unityMetaData = results.unityMetaData; } }, @@ -753,7 +756,10 @@ class SpaceCraftSim { // Create the ground plane as a child of SpaceCraft this.groundPlane = window.bridge.createObject({ prefab: "Prefabs/GroundPlane", - parent: this.spaceCraft + parent: this.spaceCraft, + update: { + "transform:Cube/component:MeshRenderer/material/color": { r: 0.0, g: .2, b: 0.0 }, + } }); // Store references globally @@ -771,7 +777,8 @@ class SpaceCraftSim { // Client identity clientType: 'simulator', clientId: this.clientId, - clientName: 'Spacecraft Simulator', + clientName: 'Spacecraft', + simulatorIndex: 0, // Collection/screen state currentScreenId: 'main', @@ -833,15 +840,26 @@ class SpaceCraftSim { } } - /** - * Syncs the current state to Supabase presence - */ + mirrorIdentityToState() { + try { + if (!this.state) return; + this.state.clientId = this.identity.clientId; + this.state.clientType = this.identity.clientType; + this.state.clientName = this.identity.clientName; + // simulatorIndex may be undefined for non-simulators; default to 0 + this.state.simulatorIndex = (typeof this.identity.simulatorIndex === 'number') ? this.identity.simulatorIndex : 0; + try { console.log('[Sim] mirrorIdentityToState:', { id: this.state.clientId, name: this.state.clientName, simulatorIndex: this.state.simulatorIndex }); } catch {} + } catch {} + } + syncStateToPresence() { if (!this.clientChannel) { - console.warn("[SpaceCraft] Attempted to sync presence, but clientChannel is null."); return; } - + // Ensure identity is reflected in state before publishing + this.mirrorIdentityToState(); + + try { console.log('[Sim] syncStateToPresence track shared:', { simulatorIndex: this.state && this.state.simulatorIndex, clientName: this.state && this.state.clientName }); } catch {} this.clientChannel.track({ ...this.identity, shared: { ...this.state } @@ -852,7 +870,11 @@ class SpaceCraftSim { setupSupabase() { const channelName = SpaceCraftSim.getChannelName(); - console.log(`[SpaceCraft] Setting up Supabase client with channel: ${channelName}`); + try { console.log('[Sim] setupSupabase channel =', channelName); } catch {} + if (!window.supabase || typeof window.supabase.createClient !== 'function') { + console.error('[Sim] Supabase JS library missing or invalid on simulator page'); + return; + } // Create a Supabase client const client = window.supabase.createClient( @@ -861,14 +883,38 @@ class SpaceCraftSim { ); // Create a channel for client communication - this.clientChannel = client.channel(channelName); - + this.clientChannel = client.channel(channelName, { config: { presence: { key: this.identity.clientId } } }); + try { console.log('[Sim] clientChannel created for', channelName, 'with presence key', this.identity.clientId); } catch {} + this._indexClaims = []; + this._indexTimer = null; + this.clientChannel .on('broadcast', {}, (data) => { - console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); + // console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); if (data.payload && data.payload.targetSimulatorId) { - console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + // console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + } + }) + + .on('broadcast', { event: 'indexClaim' }, (data) => { + try { + const claim = data && data.payload || {}; + if (!claim.clientId || typeof claim.index !== 'number') return; + const now = Date.now(); + this._indexClaims.push({ clientId: claim.clientId, index: claim.index, ts: now }); + // keep recent 2s + this._indexClaims = this._indexClaims.filter(c => now - c.ts < 2000); + try { + console.log('[Sim] received indexClaim:', { clientId: claim.clientId, index: claim.index, ts: now }); + console.log('[Sim] indexClaim window (<=2s):', this._indexClaims); + } catch {} + } catch {} + }) + + .on('presence', { event: 'sync' }, () => { + try { console.log('[Sim] presence:sync received'); this.ensureUniqueSimulatorName(); } catch (e) { + console.warn('[SpaceCraft] ensureUniqueSimulatorName error:', e); } }) @@ -943,7 +989,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -976,7 +1022,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -1064,36 +1110,15 @@ class SpaceCraftSim { this.spaceCraft.pushMagnet(magnetId, deltaX, deltaY); }) - .on('broadcast', { event: 'simulatorTakeover' }, (data) => { - // Another simulator is trying to take over - if (data.payload.newSimulatorId === this.identity.clientId) { - return; // Our own takeover message, ignore - } - - // If this simulator started later than the new one, let it take over - if (this.identity.startTime < data.payload.startTime) { - console.log(`[SpaceCraft] Another simulator has taken over: ${data.payload.newSimulatorId}`); - this.shutdown(); - } else { - console.log(`[SpaceCraft] Ignoring takeover from older simulator: ${data.payload.newSimulatorId}`); - this.sendTakeoverEvent(); - } - }) - - .on('presence', { event: 'sync' }, () => { - const allPresences = this.clientChannel.presenceState(); - }) - .on('presence', { event: 'join' }, ({ newPresences }) => { for (const presence of newPresences) { // Skip our own presence if (presence.clientId === this.identity.clientId) continue; + // console.log(`[SpaceCraft] Another ${presence.clientType} joined: ${presence.clientId} ${presence.clientName}`); + // Check if this is a simulator joining if (presence.clientType === "simulator") { - console.log(`[SpaceCraft] Another simulator joined: ${presence.clientId}`); - // Send takeover event to establish dominance - this.sendTakeoverEvent(); } else { // A controller client joined this.updateClientInfo( @@ -1109,73 +1134,133 @@ class SpaceCraftSim { for (const presence of leftPresences) { // Remove from our client registry if (this.clients[presence.clientId]) { - console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); + // console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); delete this.clients[presence.clientId]; } } }) - .subscribe((status) => { + .subscribe((status) => { + try { console.log('[Sim] subscribe status:', status); } catch {} if (status === 'SUBSCRIBED') { - console.log("[SpaceCraft] Successfully subscribed to client channel"); - + try { console.log('[Sim] SUBSCRIBED: tracking presence and starting index negotiation'); } catch {} + try { console.log('[Sim] track identity:', { clientId: this.identity.clientId, clientName: this.identity.clientName, simulatorIndex: this.identity.simulatorIndex }); } catch {} this.clientChannel.track({ ...this.identity, - shared: { ...this.state } // Nest state under 'shared' + shared: { ...this.state } }); - - this.sendTakeoverEvent(); + try { this.ensureUniqueSimulatorName(); } catch {} } }); + // Attempt to restore previously assigned simulator index for this channel (persists across reconnects) + // Intentionally NOT restoring simulatorIndex from localStorage to avoid duplicate indices across tabs + try { console.log('[Sim] init: not restoring simulatorIndex from localStorage; will negotiate via presence'); } catch {} + this.syncStateToPresence(); } - /** - * Sends a takeover event to notify other simulators and clients - */ - sendTakeoverEvent() { - if (!this.clientChannel) { - console.warn("[SpaceCraft] Cannot send takeover event: Client channel not initialized."); - return; - } - - // console.log(`[SpaceCraft] Sending simulator takeover notification`); - - this.clientChannel.send({ - type: 'broadcast', - event: 'simulatorTakeover', - payload: { - newSimulatorId: this.identity.clientId, - newSimulatorName: this.identity.clientName, - startTime: this.identity.startTime - } - }).catch(err => console.error("[SpaceCraft] Error sending takeover event:", err)); + broadcastIndexClaim(index) { + try { + console.log('[Sim] broadcastIndexClaim sending', { index, clientId: this.identity && this.identity.clientId }); + this.clientChannel && this.clientChannel.send({ + type: 'broadcast', + event: 'indexClaim', + payload: { clientId: this.identity.clientId, index } + }); + } catch {} } - - /** - * Gracefully shuts down this simulator instance - */ - shutdown() { - console.log("[SpaceCraft] Shutting down simulator due to takeover"); - - // Clean up resources - no try/catch, just direct error logging - if (this.clientChannel) { - this.clientChannel.unsubscribe().catch(err => { - console.error("[SpaceCraft] Error unsubscribing from channel:", err); + + ensureUniqueSimulatorName() { + try { + // If already assigned, nothing to do + if (this.simulatorIndex && this.simulatorIndex > 0) return; + const state = this.clientChannel && this.clientChannel.presenceState ? this.clientChannel.presenceState() : null; + if (!state) return; + const prefix = SpaceCraftSim.simulatorNamePrefix; + // Compute observed max from presence (explicit index, name suffix) and shared maxIndexSeen + try { console.log('[Sim] ensureUniqueSimulatorName start; presence keys=', Object.keys(state || {})); } catch {} + let totalSimulators = 0; + let presenceMax = 0; + let maxSeenShared = 0; + Object.values(state).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + totalSimulators += 1; + try { console.log('[Sim] presence simulator:', { clientId: p.clientId, nameTop: p.clientName, nameShared: p.shared && p.shared.clientName, idxTop: p.simulatorIndex, idxShared: p.shared && p.shared.simulatorIndex, maxIndexSeen: p.shared && p.shared.maxIndexSeen }); } catch {} + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > presenceMax) presenceMax = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > presenceMax) presenceMax = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const m = p.clientName.match(/(\d+)\s*$/); + if (m) { + const n = parseInt(m[1], 10); + if (n > presenceMax) presenceMax = n; + } + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > maxSeenShared) maxSeenShared = p.shared.maxIndexSeen; + } + }); }); - } - - // Redirect to a shutdown page or reload - alert("Another SpaceCraft simulator has taken control. This window will now close."); - window.close(); - - // If window doesn't close (e.g., window not opened by script), reload - setTimeout(() => { - window.location.reload(); - }, 1000); + // Do not read maxIndexSeen from localStorage; avoid cross-tab coupling + const now = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now - c.ts < 2000); + const claimsMax = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const base = Math.max(presenceMax, maxSeenShared, claimsMax); + const sameBaseClaims = this._indexClaims.filter(c => c.index === base); + const rank = sameBaseClaims.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const proposed = (totalSimulators <= 1 && base === 0) ? 1 : (base + rank + 1); + console.log(`[Sim] proposal: ts=${now} tot=${totalSimulators} presenceMax=${presenceMax} maxSeenShared=${maxSeenShared} claimsMax=${claimsMax} base=${base} rank=${rank} proposed=${proposed}`); + this.broadcastIndexClaim(proposed); + // finalize after a short coordination window + clearTimeout(this._indexTimer); + this._indexTimer = setTimeout(() => { + const now2 = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now2 - c.ts < 2000); + const claimsMax2 = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const presenceMax2 = (() => { + let m = 0; + Object.values(this.clientChannel.presenceState() || {}).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > m) m = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > m) m = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const mm = p.clientName.match(/(\d+)\s*$/); + if (mm) m = Math.max(m, parseInt(mm[1], 10)); + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > m) m = p.shared.maxIndexSeen; + } + }); + }); + return m; + })(); + const base2 = Math.max(presenceMax2, claimsMax2); + const sameBase2 = this._indexClaims.filter(c => c.index === base2); + const rank2 = sameBase2.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const finalIndex = (final => (final > 0 ? final : 1))( (base2 + rank2 + 1) ); + console.log(`[Sim] finalize: claimsMax2=${claimsMax2} presenceMax2=${presenceMax2} base2=${base2} rank2=${rank2} finalIndex=${finalIndex}`); + if (!this.simulatorIndex || this.simulatorIndex === 0) { + this.simulatorIndex = finalIndex; + const nextName = `${prefix} ${this.simulatorIndex}`; + this.identity.clientName = nextName; + this.identity.simulatorIndex = this.simulatorIndex; + this.state.clientName = nextName; + this.state.simulatorIndex = this.simulatorIndex; + const newMaxSeen = Math.max(base2, this.simulatorIndex); + this.state.maxIndexSeen = newMaxSeen; + // Do not persist indices to localStorage to avoid cross-tab duplication + console.log(`[Sim] assigned: simulatorIndex=${this.simulatorIndex} clientName="${this.identity.clientName}"`); + this.syncStateToPresence(); + if (this.domContentLoaded) { + this.generateQRCodes(); + } + } + }, 400); + } catch {} } + // Takeover functionality removed: multi-simulator is supported + /** * Sends a Supabase broadcast event on the channel. * @param {string} clientId - The unique ID of the target client. @@ -1194,7 +1279,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); + // console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); // Add simulator ID to payload so client knows who sent it const fullPayload = { @@ -1220,7 +1305,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); + // console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); // Add simulator ID to payload so clients know who sent it const fullPayload = { @@ -1272,13 +1357,7 @@ class SpaceCraftSim { * Logs the current status of the SpaceCraft instance */ logStatus() { - console.log("[SpaceCraft Debug] Status:", { - DOMReady: this.domContentLoaded, - BasicInitialized: this.isInitialized, - BridgeAvailable: !!window.bridge, - SpaceCraft: !!this.spaceCraft, - SupabaseLoaded: typeof window.supabase !== 'undefined' - }); + // console.log("[SpaceCraft Debug] Status:", { DOMReady: this.domContentLoaded, BasicInitialized: this.isInitialized, BridgeAvailable: !!window.bridge, SpaceCraft: !!this.spaceCraft, SupabaseLoaded: typeof window.supabase !== 'undefined' }); } /** @@ -1354,7 +1433,7 @@ class SpaceCraftSim { */ createUnifiedTagsList() { if (!this.loadedContent || !this.loadedContent.collections) { - console.log("[SpaceCraft] No collections data available for creating tags list"); + // console.log("[SpaceCraft] No collections data available for creating tags list"); return []; } @@ -1380,7 +1459,7 @@ class SpaceCraftSim { } const sortedTags = Array.from(allTags).sort(); - console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); + // console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); return sortedTags; } } @@ -1408,4 +1487,4 @@ if (document.readyState === 'loading') { // setInterval(() => window.SpaceCraft.logStatus(), 5000); } -console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); +// console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); diff --git a/WebSites/controller/build/SimulatorState.d.ts b/WebSites/controller/build/SimulatorState.d.ts index d1014429..dcb60e47 100644 --- a/WebSites/controller/build/SimulatorState.d.ts +++ b/WebSites/controller/build/SimulatorState.d.ts @@ -13,8 +13,6 @@ export declare class SimulatorState extends Node { currentCollectionId: string; currentCollectionItems: Array; currentScreenId: string; - currentSearchGravity: number; - currentSearchString: string; highlightedItem: any; highlightedItemId: string; highlightedItemIds: Array; diff --git a/WebSites/controller/build/SimulatorState.d.ts.map b/WebSites/controller/build/SimulatorState.d.ts.map index ef527e1e..91c81426 100644 --- a/WebSites/controller/build/SimulatorState.d.ts.map +++ b/WebSites/controller/build/SimulatorState.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"SimulatorState.d.ts","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA8B,SAAS,EAAE,MAAM,QAAQ,CAAC;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,EAC7C,CAAC;AAEF,qBACa,cAAe,SAAQ,IAAI;IAG5B,QAAQ,EAAE,MAAM,CAAC;IAGjB,UAAU,EAAE,MAAM,CAAC;IAGnB,UAAU,EAAE,MAAM,CAAC;IAGnB,gBAAgB,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAG7B,iBAAiB,EAAE,UAAU,CAAC;IAG9B,mBAAmB,EAAE,MAAM,CAAC;IAG5B,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGtC,eAAe,EAAE,MAAM,CAAC;IAGxB,oBAAoB,EAAE,MAAM,CAAC;IAG7B,mBAAmB,EAAE,MAAM,CAAC;IAG5B,eAAe,EAAE,GAAG,CAAC;IAGrB,iBAAiB,EAAE,MAAM,CAAC;IAG1B,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGlC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAGpB,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGzB,YAAY,EAAE,GAAG,CAAC;IAGlB,cAAc,EAAE,MAAM,CAAC;IAGvB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAG/B,WAAW,EAAE,MAAM,CAAC;IAGpB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGpB,aAAa,EAAE,MAAM,CAAC;IAE9B,MAAM,CAAC,KAAK,EAAE,cAAc;CA0B/B"} \ No newline at end of file +{"version":3,"file":"SimulatorState.d.ts","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA8B,SAAS,EAAE,MAAM,QAAQ,CAAC;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,EAC7C,CAAC;AAEF,qBACa,cAAe,SAAQ,IAAI;IAG5B,QAAQ,EAAE,MAAM,CAAC;IAGjB,UAAU,EAAE,MAAM,CAAC;IAGnB,UAAU,EAAE,MAAM,CAAC;IAGnB,gBAAgB,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAG7B,iBAAiB,EAAE,UAAU,CAAC;IAG9B,mBAAmB,EAAE,MAAM,CAAC;IAG5B,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGtC,eAAe,EAAE,MAAM,CAAC;IAGxB,eAAe,EAAE,GAAG,CAAC;IAGrB,iBAAiB,EAAE,MAAM,CAAC;IAG1B,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGlC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAGpB,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGzB,YAAY,EAAE,GAAG,CAAC;IAGlB,cAAc,EAAE,MAAM,CAAC;IAGvB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAG/B,WAAW,EAAE,MAAM,CAAC;IAGpB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGpB,aAAa,EAAE,MAAM,CAAC;IAE9B,MAAM,CAAC,KAAK,EAAE,cAAc;CAwB/B"} \ No newline at end of file diff --git a/WebSites/controller/build/SimulatorState.js b/WebSites/controller/build/SimulatorState.js index 903c53ee..788613df 100644 --- a/WebSites/controller/build/SimulatorState.js +++ b/WebSites/controller/build/SimulatorState.js @@ -16,8 +16,6 @@ let SimulatorState = class SimulatorState extends Node { currentCollectionId: state.currentCollectionId || '', currentCollectionItems: state.currentCollectionItems || [], currentScreenId: state.currentScreenId || '', - currentSearchGravity: state.currentSearchGravity || 0, - currentSearchString: state.currentSearchString || '', highlightedItem: state.highlightedItem || null, highlightedItemId: state.highlightedItemId || '', highlightedItemIds: state.highlightedItemIds || [], @@ -56,12 +54,6 @@ __decorate([ __decorate([ ReactiveProperty({ type: String }) ], SimulatorState.prototype, "currentScreenId", void 0); -__decorate([ - ReactiveProperty({ type: Number }) -], SimulatorState.prototype, "currentSearchGravity", void 0); -__decorate([ - ReactiveProperty({ type: String }) -], SimulatorState.prototype, "currentSearchString", void 0); __decorate([ ReactiveProperty({ type: Object }) ], SimulatorState.prototype, "highlightedItem", void 0); diff --git a/WebSites/controller/build/SimulatorState.js.map b/WebSites/controller/build/SimulatorState.js.map index dd5fce39..5df9324c 100644 --- a/WebSites/controller/build/SimulatorState.js.map +++ b/WebSites/controller/build/SimulatorState.js.map @@ -1 +1 @@ -{"version":3,"file":"SimulatorState.js","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":"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","sourcesContent":["import { Node, Register, ReactiveProperty, NodeProps } from 'io-gui';\n\nexport type Collection = {\n description: string;\n id: string;\n}\n\nexport type SimulatorStateProps = NodeProps & {\n};\n\n@Register\nexport class SimulatorState extends Node {\n\n @ReactiveProperty({type: String})\n declare clientId: string;\n\n @ReactiveProperty({type: String})\n declare clientName: string;\n\n @ReactiveProperty({type: String})\n declare clientType: string;\n\n @ReactiveProperty()\n declare connectedClients: Array; // TODO: define type\n\n @ReactiveProperty({type: Object})\n declare currentCollection: Collection;\n\n @ReactiveProperty()\n declare currentCollectionId: string;\n\n @ReactiveProperty({type: Array})\n declare currentCollectionItems: Array;\n\n @ReactiveProperty({type: String})\n declare currentScreenId: string;\n\n @ReactiveProperty({type: Number})\n declare currentSearchGravity: number;\n\n @ReactiveProperty({type: String})\n declare currentSearchString: string;\n\n @ReactiveProperty({type: Object})\n declare highlightedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare highlightedItemId: string;\n\n @ReactiveProperty({type: Array})\n declare highlightedItemIds: Array;\n\n @ReactiveProperty({type: Array})\n declare magnets: Array; // TODO: define type\n\n @ReactiveProperty({type: Array})\n declare screenIds: Array;\n\n @ReactiveProperty({type: Object})\n declare selectedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare selectedItemId: string; // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: Array})\n declare selectedItemIds: Array;\n\n @ReactiveProperty({type: String})\n declare lastUpdated: string;\n\n @ReactiveProperty({type: Array})\n declare tags: Array;\n\n @ReactiveProperty({type: Number})\n declare updateCounter: number;\n\n update(state: SimulatorState) {\n this.setProperties({\n clientId: state.clientId || '',\n clientName: state.clientName || '',\n clientType: state.clientType || '',\n connectedClients: state.connectedClients || [],\n currentCollection: state.currentCollection || {},\n currentCollectionId: state.currentCollectionId || '',\n currentCollectionItems: state.currentCollectionItems || [],\n currentScreenId: state.currentScreenId || '',\n currentSearchGravity: state.currentSearchGravity || 0,\n currentSearchString: state.currentSearchString || '',\n highlightedItem: state.highlightedItem || null,\n highlightedItemId: state.highlightedItemId || '',\n highlightedItemIds: state.highlightedItemIds || [],\n magnets: state.magnets || [],\n screenIds: state.screenIds || [],\n selectedItem: state.selectedItem || null,\n selectedItemId: state.selectedItemId || '',\n selectedItemIds: state.selectedItemIds || [],\n lastUpdated: state.lastUpdated,\n tags: state.tags || [],\n updateCounter: state.updateCounter,\n });\n }\n\n}"]} \ No newline at end of file +{"version":3,"file":"SimulatorState.js","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,gBAAgB,EAAa,MAAM,QAAQ,CAAC;AAW9D,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,IAAI;IA2DpC,MAAM,CAAC,KAAqB;QACxB,IAAI,CAAC,aAAa,CAAC;YACf,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,EAAE;YAC9B,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE;YAClC,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE;YAClC,gBAAgB,EAAE,KAAK,CAAC,gBAAgB,IAAI,EAAE;YAC9C,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,IAAI,EAAE;YAChD,mBAAmB,EAAE,KAAK,CAAC,mBAAmB,IAAI,EAAE;YACpD,sBAAsB,EAAE,KAAK,CAAC,sBAAsB,IAAI,EAAE;YAC1D,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,EAAE;YAC5C,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,IAAI;YAC9C,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,IAAI,EAAE;YAChD,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,IAAI,EAAE;YAClD,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,EAAE;YAC5B,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,EAAE;YAChC,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,IAAI;YACxC,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,EAAE;YAC1C,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,EAAE;YAC5C,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;YACtB,aAAa,EAAE,KAAK,CAAC,aAAa;SACrC,CAAC,CAAC;IACP,CAAC;CAEJ,CAAA;AAhFW;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;gDACR;AAGjB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;kDACN;AAGnB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;kDACN;AAGnB;IADP,gBAAgB,EAAE;wDACkB;AAG7B;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;yDACK;AAG9B;IADP,gBAAgB,EAAE;2DACiB;AAG5B;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,KAAK,EAAC,CAAC;8DACc;AAGtC;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;uDACD;AAGxB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;uDACJ;AAGrB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;yDACC;AAG1B;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,KAAK,EAAC,CAAC;0DACU;AAGlC;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,KAAK,EAAC,CAAC;+CACJ;AAGpB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,KAAK,EAAC,CAAC;iDACC;AAGzB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;oDACP;AAGlB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;sDACF;AAGvB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,KAAK,EAAC,CAAC;uDACO;AAG/B;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;mDACL;AAGpB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,KAAK,EAAC,CAAC;4CACJ;AAGpB;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;qDACH;AAzDrB,cAAc;IAD1B,QAAQ;GACI,cAAc,CAmF1B","sourcesContent":["import { Node, Register, ReactiveProperty, NodeProps } from 'io-gui';\n\nexport type Collection = {\n description: string;\n id: string;\n}\n\nexport type SimulatorStateProps = NodeProps & {\n};\n\n@Register\nexport class SimulatorState extends Node {\n\n @ReactiveProperty({type: String})\n declare clientId: string;\n\n @ReactiveProperty({type: String})\n declare clientName: string;\n\n @ReactiveProperty({type: String})\n declare clientType: string;\n\n @ReactiveProperty()\n declare connectedClients: Array; // TODO: define type\n\n @ReactiveProperty({type: Object})\n declare currentCollection: Collection;\n\n @ReactiveProperty()\n declare currentCollectionId: string;\n\n @ReactiveProperty({type: Array})\n declare currentCollectionItems: Array;\n\n @ReactiveProperty({type: String})\n declare currentScreenId: string;\n\n @ReactiveProperty({type: Object})\n declare highlightedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare highlightedItemId: string;\n\n @ReactiveProperty({type: Array})\n declare highlightedItemIds: Array;\n\n @ReactiveProperty({type: Array})\n declare magnets: Array; // TODO: define type\n\n @ReactiveProperty({type: Array})\n declare screenIds: Array;\n\n @ReactiveProperty({type: Object})\n declare selectedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare selectedItemId: string; // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: Array})\n declare selectedItemIds: Array;\n\n @ReactiveProperty({type: String})\n declare lastUpdated: string;\n\n @ReactiveProperty({type: Array})\n declare tags: Array;\n\n @ReactiveProperty({type: Number})\n declare updateCounter: number;\n\n update(state: SimulatorState) {\n this.setProperties({\n clientId: state.clientId || '',\n clientName: state.clientName || '',\n clientType: state.clientType || '',\n connectedClients: state.connectedClients || [],\n currentCollection: state.currentCollection || {},\n currentCollectionId: state.currentCollectionId || '',\n currentCollectionItems: state.currentCollectionItems || [],\n currentScreenId: state.currentScreenId || '',\n highlightedItem: state.highlightedItem || null,\n highlightedItemId: state.highlightedItemId || '',\n highlightedItemIds: state.highlightedItemIds || [],\n magnets: state.magnets || [],\n screenIds: state.screenIds || [],\n selectedItem: state.selectedItem || null,\n selectedItemId: state.selectedItemId || '',\n selectedItemIds: state.selectedItemIds || [],\n lastUpdated: state.lastUpdated,\n tags: state.tags || [],\n updateCounter: state.updateCounter,\n });\n }\n\n}"]} \ No newline at end of file diff --git a/WebSites/controller/build/SpacetimeController.d.ts b/WebSites/controller/build/SpacetimeController.d.ts index add4d00d..12521133 100644 --- a/WebSites/controller/build/SpacetimeController.d.ts +++ b/WebSites/controller/build/SpacetimeController.d.ts @@ -46,8 +46,10 @@ export declare class SpacetimeController extends IoElement { clientChannel: any; clientConnected: boolean; currentSimulatorId: string | null; + currentSimulators: Map; magnetViewMetadata: Array; simulatorState: SimulatorState; + simulatorRosterTick: number; constructor(props: IoElementProps); connect(): void; ready(): void; @@ -61,8 +63,9 @@ export declare class SpacetimeController extends IoElement { sendEventToSimulator(eventType: string, data: any): void; setupPresenceHandlers(): void; subscribeToChannel(): void; + setCurrentSimulator(simId: string): void; updatePresenceState(): Promise; - findLatestSimulator(presenceState: PresenceState): SimulatorPresence | null; + findSimulator(presenceState: PresenceState): SimulatorPresence | null; } export {}; //# sourceMappingURL=SpacetimeController.d.ts.map \ No newline at end of file diff --git a/WebSites/controller/build/SpacetimeController.d.ts.map b/WebSites/controller/build/SpacetimeController.d.ts.map index 9295a56f..5680318e 100644 --- a/WebSites/controller/build/SpacetimeController.d.ts.map +++ b/WebSites/controller/build/SpacetimeController.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"SpacetimeController.d.ts","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAiF,cAAc,EAAkB,MAAM,QAAQ,CAAC;AAMlJ,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAa7C,KAAK,aAAa,GAAG;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;CAC3B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAQD,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,GAAG,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAA;AAED,qBACa,mBAAoB,SAAQ,SAAS;IAC9C,MAAM,KAAK,KAAK,WAaf;IAED,MAAM,CAAC,WAAW,SAA8C;IAChE,MAAM,CAAC,eAAe,SAAsN;IAC5O,MAAM,CAAC,iBAAiB,SAAgB;IACxC,MAAM,CAAC,UAAU,SAAgB;IAEzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,GAAG,CAAC;IACpB,aAAa,EAAE,GAAG,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAElC,kBAAkB,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAGxC,cAAc,EAAE,cAAc,CAAC;gBAE3B,KAAK,EAAE,cAAc;IAejC,OAAO;IAmBP,KAAK;IA+BL,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAI3C,aAAa,CAAC,SAAS,EAAE,MAAM;IAI/B,eAAe,CAAC,MAAM,EAAE,MAAM;IAI9B,qBAAqB,CAAC,UAAU,EAAE,MAAM;IASxC,qBAAqB,CAAC,UAAU,EAAE,MAAM;IAIxC,qBAAqB,CAAC,QAAQ,EAAE,MAAM;IAItC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAIpE,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IA6BjD,qBAAqB;IAiBrB,kBAAkB;IAcZ,mBAAmB;IAezB,mBAAmB,CAAC,aAAa,EAAE,aAAa,GAAG,iBAAiB,GAAG,IAAI;CAa9E"} \ No newline at end of file +{"version":3,"file":"SpacetimeController.d.ts","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAiF,cAAc,EAAkB,MAAM,QAAQ,CAAC;AAMlJ,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAa7C,KAAK,aAAa,GAAG;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;CAC3B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAQD,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,GAAG,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAA;AAED,qBACa,mBAAoB,SAAQ,SAAS;IAC9C,MAAM,KAAK,KAAK,WAaf;IAED,MAAM,CAAC,WAAW,SAA8C;IAChE,MAAM,CAAC,eAAe,SAAsN;IAC5O,MAAM,CAAC,iBAAiB,SAAgB;IACxC,MAAM,CAAC,UAAU,SAAgB;IAEzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,GAAG,CAAC;IACpB,aAAa,EAAE,GAAG,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAElD,kBAAkB,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAGxC,cAAc,EAAE,cAAc,CAAC;IAG/B,mBAAmB,EAAE,MAAM,CAAC;gBAExB,KAAK,EAAE,cAAc;IAiBjC,OAAO;IA0BP,KAAK;IA+BL,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAI3C,aAAa,CAAC,SAAS,EAAE,MAAM;IAI/B,eAAe,CAAC,MAAM,EAAE,MAAM;IAI9B,qBAAqB,CAAC,UAAU,EAAE,MAAM;IASxC,qBAAqB,CAAC,UAAU,EAAE,MAAM;IAIxC,qBAAqB,CAAC,QAAQ,EAAE,MAAM;IAItC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAIpE,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IA6BjD,qBAAqB;IAoDrB,kBAAkB;IAclB,mBAAmB,CAAC,KAAK,EAAE,MAAM;IA2B3B,mBAAmB;IAezB,aAAa,CAAC,aAAa,EAAE,aAAa,GAAG,iBAAiB,GAAG,IAAI;CAkDxE"} \ No newline at end of file diff --git a/WebSites/controller/build/SpacetimeController.js b/WebSites/controller/build/SpacetimeController.js index 7fd41622..35c0c19b 100644 --- a/WebSites/controller/build/SpacetimeController.js +++ b/WebSites/controller/build/SpacetimeController.js @@ -51,7 +51,9 @@ let SpacetimeController = class SpacetimeController extends IoElement { this.clientChannel = null; this.clientConnected = false; this.currentSimulatorId = null; + this.currentSimulators = new Map(); this.magnetViewMetadata = []; + this.simulatorRosterTick = 0; this.connect(); } connect() { @@ -60,13 +62,19 @@ let SpacetimeController = class SpacetimeController extends IoElement { return; } try { - const channelName = new URLSearchParams(window.location.search).get('channel') || SpacetimeController_1.clientChannelName; + const params = new URLSearchParams(window.location.search); + const channelName = params.get('channel') || SpacetimeController_1.clientChannelName; this.supabaseClient = supabase.createClient(SpacetimeController_1.supabaseUrl, SpacetimeController_1.supabaseAnonKey); this.clientChannel = this.supabaseClient.channel(channelName, { config: { presence: { key: this.clientId } } }); this.setupPresenceHandlers(); this.subscribeToChannel(); + // Only honor simulatorIndex param (no legacy) + const simIndexFromUrl = params.get('simulatorIndex'); + if (simIndexFromUrl) { + this._preselectSimulatorIndex = parseInt(simIndexFromUrl, 10); + } } catch (error) { console.error('Controller connection failed:', error); @@ -158,15 +166,52 @@ let SpacetimeController = class SpacetimeController extends IoElement { this.clientChannel .on('presence', { event: 'sync' }, () => { const presenceState = this.clientChannel.presenceState(); - const simulator = this.findLatestSimulator(presenceState); + try { + const raw = Object.values(presenceState || {}).flat(); + const sims = raw.filter((p) => p && p.clientType === 'simulator'); + console.log('[Controller][presence:sync] presences:', raw.length, 'simulators:', sims.length); + console.log('[Controller][presence:sync] simulators raw:', sims.map((p) => ({ + clientId: p.clientId, + nameTop: p.clientName, + nameShared: p.shared && p.shared.clientName, + idxTop: typeof p.simulatorIndex === 'number' ? p.simulatorIndex : 0, + idxShared: p.shared && typeof p.shared.simulatorIndex === 'number' ? p.shared.simulatorIndex : 0 + }))); + } + catch { } + const simulator = this.findSimulator(presenceState); if (simulator) { + // If preselect by index requested, override with match when available + if (this._preselectSimulatorIndex) { + for (const sim of this.currentSimulators.values()) { + const idx = sim.simulatorIndex || (sim.shared && sim.shared.simulatorIndex); + if (idx === this._preselectSimulatorIndex) { + this.currentSimulatorId = sim.clientId; + break; + } + } + this._preselectSimulatorIndex = null; + } this.magnetViewMetadata = simulator.shared.unityMetaData?.MagnetView || []; - this.currentSimulatorId = simulator.clientId; + this.currentSimulatorId = this.currentSimulatorId || simulator.clientId; this.simulatorState.update(simulator.shared); + // bump tick so UI re-renders simulator menus + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; + try { + const list = Array.from(this.currentSimulators.values()).map((s) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] roster updated (tick', this.simulatorRosterTick, '):', list); + console.log('[Controller] currentSimulatorId:', this.currentSimulatorId); + } + catch { } + } + else { + // No simulator selected yet; still bump tick to refresh menu state + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; } - }) - .on('broadcast', { event: 'simulatorTakeover' }, (payload) => { - this.currentSimulatorId = payload.newSimulatorId; }); } subscribeToChannel() { @@ -182,6 +227,35 @@ let SpacetimeController = class SpacetimeController extends IoElement { } }); } + setCurrentSimulator(simId) { + this.currentSimulatorId = simId; + if (!this.clientChannel) + return; + // Pull fresh presence state from Supabase and switch to the selected simulator's state + const presenceState = this.clientChannel.presenceState(); + const sim = this.findSimulator(presenceState); + if (sim) { + this.magnetViewMetadata = sim.shared.unityMetaData?.MagnetView || []; + this.simulatorState.update(sim.shared); + } + try { + const sel = this.currentSimulators.get(simId); + console.log('[Controller] setCurrentSimulator:', simId, 'name=', sel && (sel.clientName || (sel.shared && sel.shared.clientName)), 'index=', sel && (sel.simulatorIndex || (sel.shared && sel.shared.simulatorIndex))); + } + catch { } + // Persist simulatorIndex in URL + try { + const selected = this.currentSimulators.get(simId); + const simIndex = selected && (selected.simulatorIndex || (selected.shared && selected.shared.simulatorIndex)); + if (simIndex) { + const url = new URL(window.location.href); + url.searchParams.set('simulatorIndex', String(simIndex)); + window.history.replaceState({}, '', url.toString()); + console.log('[Controller] URL simulatorIndex set to', simIndex); + } + } + catch { } + } async updatePresenceState() { if (this.clientConnected && this.clientChannel) { try { @@ -197,23 +271,65 @@ let SpacetimeController = class SpacetimeController extends IoElement { } } } - findLatestSimulator(presenceState) { - let latestSimulator = null; - let latestStartTime = 0; - Object.values(presenceState).forEach(presences => { - presences.forEach((presence) => { - if (presence.clientType === 'simulator' && presence.startTime > latestStartTime) { - latestSimulator = presence; - latestStartTime = presence.startTime; + findSimulator(presenceState) { + let lastSimulator = null; + let simulator = null; + const values = Object.values(presenceState); + this.currentSimulators = new Map(); + for (const presences of values) { + for (const presence of presences) { + // Only count fully-initialized simulators (index assigned) + const meta = presence; + const simIndexTop = typeof meta.simulatorIndex === 'number' ? meta.simulatorIndex : 0; + const simIndexShared = meta.shared && typeof meta.shared.simulatorIndex === 'number' ? meta.shared.simulatorIndex : 0; + const simIndex = simIndexTop || simIndexShared; + if (meta.clientType === 'simulator' && simIndex > 0) { + // Prefer shared view of fields if available + const merged = { + ...meta, + clientName: (meta.shared && meta.shared.clientName) ? meta.shared.clientName : meta.clientName, + simulatorIndex: simIndex, + shared: meta.shared || {} + }; + lastSimulator = merged; + this.currentSimulators.set(meta.clientId, merged); + if (meta.clientId == this.currentSimulatorId) { + simulator = lastSimulator; + } } - }); - }); - return latestSimulator; + else if (meta.clientType === 'simulator') { + console.log('[Controller] ignoring simulator without index yet:', { + clientId: meta.clientId, + nameTop: meta.clientName, + nameShared: meta.shared && meta.shared.clientName, + idxTop: simIndexTop, + idxShared: simIndexShared + }); + } + } + } + if (!simulator) { + simulator = lastSimulator; + } + this.currentSimulatorId = simulator?.clientId || null; + try { + const list = Array.from(this.currentSimulators.values()).map((s) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] currentSimulators:', list); + } + catch { } + return simulator; } }; __decorate([ ReactiveProperty({ type: SimulatorState, init: null }) ], SpacetimeController.prototype, "simulatorState", void 0); +__decorate([ + ReactiveProperty({ type: Number }) +], SpacetimeController.prototype, "simulatorRosterTick", void 0); SpacetimeController = SpacetimeController_1 = __decorate([ Register ], SpacetimeController); diff --git a/WebSites/controller/build/SpacetimeController.js.map b/WebSites/controller/build/SpacetimeController.js.map index 36302db4..6a287712 100644 --- a/WebSites/controller/build/SpacetimeController.js.map +++ b/WebSites/controller/build/SpacetimeController.js.map @@ -1 +1 @@ -{"version":3,"file":"SpacetimeController.js","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":"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","sourcesContent":["import { IoElement, Register, ioNavigator, MenuOption, Storage as $, ioMarkdown, ReactiveProperty, IoElementProps, ThemeSingleton } from 'io-gui';\nimport { tabNavigate } from './TabNavigate.js';\nimport { tabSelect } from './TabSelect.js';\nimport { tabInspect } from './TabInspect.js';\nimport { tabMagnet } from './TabMagnet.js';\nimport { tabAdjust } from './TabAdjust.js';\nimport { SimulatorState } from './SimulatorState.js';\nimport type { Magnet } from './types/Magnet';\n\nThemeSingleton.themeID = 'dark';\n\nfunction generateClientId() {\n return 'controller-' + Math.random().toString(36).substr(2, 9);\n}\nfunction generateClientName() {\n const now = new Date();\n const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);\n return `Controller-${timestamp}`;\n}\n\ntype PresenceState = {\n [key: string]: Presence[];\n};\n\ntype Presence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n startTime: number;\n}\n\ntype SimulatorPresence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n shared: SimulatorState;\n startTime: number;\n}\n\ntype SimulatorTakeoverPayload = {\n newSimulatorId: string;\n newSimulatorName: string;\n startTime: number;\n}\n\nexport type ViewMetadata = {\n canWrite: boolean;\n category: string;\n component: string;\n defaultValue: any;\n description: string;\n displayName: string;\n name: string;\n path: string;\n type: 'bool' | 'float' | 'string';\n unityType: string;\n min?: number,\n max?: number,\n step?: number,\n}\n\n@Register\nexport class SpacetimeController extends IoElement {\n static get Style() {\n return /* css */`\n :host {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n :host > io-navigator {\n flex: 1 1 auto;\n overflow: hidden;\n }\n `;\n }\n\n static supabaseUrl = 'https://gwodhwyvuftyrvbymmvc.supabase.co';\n static supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd3b2Rod3l2dWZ0eXJ2YnltbXZjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIzNDkyMDMsImV4cCI6MjA1NzkyNTIwM30.APVpyOupY84gQ7c0vBZkY-GqoJRPhb4oD4Lcj9CEzlc';\n static clientChannelName = 'spacecraft';\n static clientType = 'controller';\n\n declare clientId: string;\n declare clientName: string;\n declare supabaseClient: any;\n declare clientChannel: any;\n declare clientConnected: boolean;\n declare currentSimulatorId: string | null;\n\n declare magnetViewMetadata: Array;\n\n @ReactiveProperty({type: SimulatorState, init: null})\n declare simulatorState: SimulatorState;\n\n constructor(props: IoElementProps) {\n super(props);\n // Client Identity\n this.clientId = generateClientId();\n this.clientName = generateClientName();\n\n // Connection State\n this.supabaseClient = null;\n this.clientChannel = null;\n this.clientConnected = false;\n this.currentSimulatorId = null;\n this.magnetViewMetadata = [];\n this.connect();\n }\n\n connect() {\n if (typeof supabase === 'undefined') {\n console.error('Supabase library missing!');\n return;\n }\n try {\n const channelName = new URLSearchParams(window.location.search).get('channel') || SpacetimeController.clientChannelName;\n this.supabaseClient = supabase.createClient(SpacetimeController.supabaseUrl, SpacetimeController.supabaseAnonKey);\n this.clientChannel = this.supabaseClient.channel(channelName, {\n config: { presence: { key: this.clientId } }\n });\n this.setupPresenceHandlers();\n this.subscribeToChannel();\n } catch (error) {\n console.error('Controller connection failed:', error);\n console.error('[Controller] Connection failed:', error);\n }\n }\n\n ready() {\n this.render([\n ioNavigator({\n menu: 'top',\n caching: 'proactive',\n option: new MenuOption({\n id: 'root',\n options: [\n {id: 'About', icon: '📖'},\n {id: 'Navigate', icon: '🧭'},\n {id: 'Select', icon: '👆'},\n {id: 'Inspect', icon: '🔍'},\n {id: 'Magnet', icon: '🧲'},\n {id: 'Adjust', icon: '⚙️'},\n ],\n selectedID: $({key: 'page', storage: 'hash', value: 'About'})\n }),\n elements: [\n ioMarkdown({id: 'About', src: './docs/About.md'}),\n tabNavigate({id: 'Navigate', controller: this, simulatorState: this.simulatorState}),\n tabSelect({id: 'Select', controller: this, simulatorState: this.simulatorState}),\n tabInspect({id: 'Inspect', controller: this, simulatorState: this.simulatorState}),\n tabMagnet({id: 'Magnet', controller: this, simulatorState: this.simulatorState}),\n tabAdjust({id: 'Adjust', controller: this, simulatorState: this.simulatorState}),\n ]\n })\n ]);\n }\n\n // === UNITY COMMUNICATION ===\n\n sendPanEvent(deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pan', { panXDelta: deltaX, panYDelta: deltaY });\n }\n\n sendZoomEvent(zoomDelta: number) {\n this.sendEventToSimulator('zoom', { zoomDelta });\n }\n\n sendSelectEvent(action: string) {\n this.sendEventToSimulator('select', { action });\n }\n\n sendCreateMagnetEvent(magnetData: Magnet) {\n try {\n console.log('[Controller] sendCreateMagnetEvent magnetData:', JSON.parse(JSON.stringify(magnetData)));\n } catch (e) {\n console.log('[Controller] sendCreateMagnetEvent magnetData (raw):', magnetData);\n }\n this.sendEventToSimulator('createMagnet', { magnetData });\n }\n\n sendUpdateMagnetEvent(magnetData: Magnet) {\n this.sendEventToSimulator('updateMagnet', { magnetData });\n }\n\n sendDeleteMagnetEvent(magnetId: string) {\n this.sendEventToSimulator('deleteMagnet', { magnetId });\n }\n\n sendPushMagnetEvent(magnetId: string, deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pushMagnet', { magnetId, deltaX, deltaY });\n }\n\n sendEventToSimulator(eventType: string, data: any) {\n if (!this.clientChannel) {\n console.error('[Controller] Cannot send event - no client channel');\n return;\n }\n\n if (!this.currentSimulatorId) {\n console.error('[Controller] Cannot send event - no current simulator ID');\n return;\n }\n\n const payload = {\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n screenId: 'main',\n targetSimulatorId: this.currentSimulatorId,\n ...data\n };\n\n this.clientChannel.send({\n type: 'broadcast',\n event: eventType,\n payload: payload\n }).catch((err: any) => {\n console.error(`[Controller] Send '${eventType}' failed:`, err);\n });\n }\n\n setupPresenceHandlers() {\n this.clientChannel\n .on('presence', { event: 'sync' }, () => {\n const presenceState = this.clientChannel.presenceState();\n const simulator = this.findLatestSimulator(presenceState);\n \n if (simulator) {\n this.magnetViewMetadata = (simulator.shared as any).unityMetaData?.MagnetView || [];\n this.currentSimulatorId = simulator.clientId;\n this.simulatorState.update(simulator.shared);\n }\n })\n .on('broadcast', { event: 'simulatorTakeover' }, (payload: SimulatorTakeoverPayload) => {\n this.currentSimulatorId = payload.newSimulatorId;\n });\n }\n\n subscribeToChannel() {\n this.clientChannel.subscribe(async (status: string) => {\n if (status === 'SUBSCRIBED') {\n this.clientConnected = true;\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n }\n });\n }\n\n async updatePresenceState() {\n if (this.clientConnected && this.clientChannel) {\n try {\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n } catch (error) {\n console.error('[Connection] Failed to update presence:', error);\n }\n }\n }\n\n findLatestSimulator(presenceState: PresenceState): SimulatorPresence | null {\n let latestSimulator = null;\n let latestStartTime = 0;\n Object.values(presenceState).forEach(presences => {\n presences.forEach((presence: Presence) => {\n if (presence.clientType === 'simulator' && presence.startTime > latestStartTime) {\n latestSimulator = presence;\n latestStartTime = presence.startTime;\n }\n });\n });\n return latestSimulator;\n }\n}"]} \ No newline at end of file +{"version":3,"file":"SpacetimeController.js","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":"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","sourcesContent":["import { IoElement, Register, ioNavigator, MenuOption, Storage as $, ioMarkdown, ReactiveProperty, IoElementProps, ThemeSingleton } from 'io-gui';\nimport { tabNavigate } from './TabNavigate.js';\nimport { tabSelect } from './TabSelect.js';\nimport { tabInspect } from './TabInspect.js';\nimport { tabMagnet } from './TabMagnet.js';\nimport { tabAdjust } from './TabAdjust.js';\nimport { SimulatorState } from './SimulatorState.js';\nimport type { Magnet } from './types/Magnet';\n\nThemeSingleton.themeID = 'dark';\n\nfunction generateClientId() {\n return 'controller-' + Math.random().toString(36).substr(2, 9);\n}\nfunction generateClientName() {\n const now = new Date();\n const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);\n return `Controller-${timestamp}`;\n}\n\ntype PresenceState = {\n [key: string]: Presence[];\n};\n\ntype Presence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n startTime: number;\n}\n\ntype SimulatorPresence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n shared: SimulatorState;\n startTime: number;\n}\n\ntype SimulatorTakeoverPayload = {\n newSimulatorId: string;\n newSimulatorName: string;\n startTime: number;\n}\n\nexport type ViewMetadata = {\n canWrite: boolean;\n category: string;\n component: string;\n defaultValue: any;\n description: string;\n displayName: string;\n name: string;\n path: string;\n type: 'bool' | 'float' | 'string';\n unityType: string;\n min?: number,\n max?: number,\n step?: number,\n}\n\n@Register\nexport class SpacetimeController extends IoElement {\n static get Style() {\n return /* css */`\n :host {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n :host > io-navigator {\n flex: 1 1 auto;\n overflow: hidden;\n }\n `;\n }\n\n static supabaseUrl = 'https://gwodhwyvuftyrvbymmvc.supabase.co';\n static supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd3b2Rod3l2dWZ0eXJ2YnltbXZjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIzNDkyMDMsImV4cCI6MjA1NzkyNTIwM30.APVpyOupY84gQ7c0vBZkY-GqoJRPhb4oD4Lcj9CEzlc';\n static clientChannelName = 'spacecraft';\n static clientType = 'controller';\n\n declare clientId: string;\n declare clientName: string;\n declare supabaseClient: any;\n declare clientChannel: any;\n declare clientConnected: boolean;\n declare currentSimulatorId: string | null;\n declare currentSimulators: Map;\n\n declare magnetViewMetadata: Array;\n\n @ReactiveProperty({type: SimulatorState, init: null})\n declare simulatorState: SimulatorState;\n\n @ReactiveProperty({type: Number})\n declare simulatorRosterTick: number;\n\n constructor(props: IoElementProps) {\n super(props);\n // Client Identity\n this.clientId = generateClientId();\n this.clientName = generateClientName();\n\n // Connection State\n this.supabaseClient = null;\n this.clientChannel = null;\n this.clientConnected = false;\n this.currentSimulatorId = null;\n this.currentSimulators = new Map();\n this.magnetViewMetadata = [];\n this.simulatorRosterTick = 0;\n this.connect();\n }\n\n connect() {\n if (typeof supabase === 'undefined') {\n console.error('Supabase library missing!');\n return;\n }\n try {\n const params = new URLSearchParams(window.location.search);\n const channelName = params.get('channel') || SpacetimeController.clientChannelName;\n this.supabaseClient = supabase.createClient(SpacetimeController.supabaseUrl, SpacetimeController.supabaseAnonKey);\n this.clientChannel = this.supabaseClient.channel(channelName, {\n config: { presence: { key: this.clientId } }\n });\n this.setupPresenceHandlers();\n this.subscribeToChannel();\n // Only honor simulatorIndex param (no legacy)\n const simIndexFromUrl = params.get('simulatorIndex');\n if (simIndexFromUrl) {\n (this as any)._preselectSimulatorIndex = parseInt(simIndexFromUrl, 10);\n }\n } catch (error) {\n console.error('Controller connection failed:', error);\n console.error('[Controller] Connection failed:', error);\n }\n }\n\n \n ready() {\n this.render([\n ioNavigator({\n menu: 'top',\n caching: 'proactive',\n option: new MenuOption({\n id: 'root',\n options: [\n {id: 'About', icon: '📖'},\n {id: 'Navigate', icon: '🧭'},\n {id: 'Select', icon: '👆'},\n {id: 'Inspect', icon: '🔍'},\n {id: 'Magnet', icon: '🧲'},\n {id: 'Adjust', icon: '⚙️'},\n ],\n selectedID: $({key: 'page', storage: 'hash', value: 'About'})\n }),\n elements: [\n ioMarkdown({id: 'About', src: './docs/About.md'}),\n tabNavigate({id: 'Navigate', controller: this, simulatorState: this.simulatorState}),\n tabSelect({id: 'Select', controller: this, simulatorState: this.simulatorState}),\n tabInspect({id: 'Inspect', controller: this, simulatorState: this.simulatorState}),\n tabMagnet({id: 'Magnet', controller: this, simulatorState: this.simulatorState}),\n tabAdjust({id: 'Adjust', controller: this, simulatorState: this.simulatorState}),\n ]\n })\n ]);\n }\n\n // === UNITY COMMUNICATION ===\n\n sendPanEvent(deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pan', { panXDelta: deltaX, panYDelta: deltaY });\n }\n\n sendZoomEvent(zoomDelta: number) {\n this.sendEventToSimulator('zoom', { zoomDelta });\n }\n\n sendSelectEvent(action: string) {\n this.sendEventToSimulator('select', { action });\n }\n\n sendCreateMagnetEvent(magnetData: Magnet) {\n try {\n console.log('[Controller] sendCreateMagnetEvent magnetData:', JSON.parse(JSON.stringify(magnetData)));\n } catch (e) {\n console.log('[Controller] sendCreateMagnetEvent magnetData (raw):', magnetData);\n }\n this.sendEventToSimulator('createMagnet', { magnetData });\n }\n\n sendUpdateMagnetEvent(magnetData: Magnet) {\n this.sendEventToSimulator('updateMagnet', { magnetData });\n }\n\n sendDeleteMagnetEvent(magnetId: string) {\n this.sendEventToSimulator('deleteMagnet', { magnetId });\n }\n\n sendPushMagnetEvent(magnetId: string, deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pushMagnet', { magnetId, deltaX, deltaY });\n }\n\n sendEventToSimulator(eventType: string, data: any) {\n if (!this.clientChannel) {\n console.error('[Controller] Cannot send event - no client channel');\n return;\n }\n\n if (!this.currentSimulatorId) {\n console.error('[Controller] Cannot send event - no current simulator ID');\n return;\n }\n\n const payload = {\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n screenId: 'main',\n targetSimulatorId: this.currentSimulatorId,\n ...data\n };\n\n this.clientChannel.send({\n type: 'broadcast',\n event: eventType,\n payload: payload\n }).catch((err: any) => {\n console.error(`[Controller] Send '${eventType}' failed:`, err);\n });\n }\n\n setupPresenceHandlers() {\n this.clientChannel\n .on('presence', { event: 'sync' }, () => {\n const presenceState = this.clientChannel.presenceState();\n try {\n const raw = Object.values(presenceState || {}).flat();\n const sims = raw.filter((p: any) => p && p.clientType === 'simulator');\n console.log('[Controller][presence:sync] presences:', raw.length, 'simulators:', sims.length);\n console.log('[Controller][presence:sync] simulators raw:', sims.map((p: any) => ({\n clientId: p.clientId,\n nameTop: p.clientName,\n nameShared: p.shared && p.shared.clientName,\n idxTop: typeof p.simulatorIndex === 'number' ? p.simulatorIndex : 0,\n idxShared: p.shared && typeof p.shared.simulatorIndex === 'number' ? p.shared.simulatorIndex : 0\n })));\n } catch {}\n\n const simulator = this.findSimulator(presenceState);\n \n if (simulator) {\n // If preselect by index requested, override with match when available\n if ((this as any)._preselectSimulatorIndex) {\n for (const sim of this.currentSimulators.values()) {\n const idx = (sim as any).simulatorIndex || ((sim as any).shared && (sim as any).shared.simulatorIndex);\n if (idx === (this as any)._preselectSimulatorIndex) {\n this.currentSimulatorId = (sim as any).clientId;\n break;\n }\n }\n (this as any)._preselectSimulatorIndex = null;\n }\n this.magnetViewMetadata = (simulator.shared as any).unityMetaData?.MagnetView || [];\n this.currentSimulatorId = this.currentSimulatorId || simulator.clientId;\n this.simulatorState.update(simulator.shared);\n // bump tick so UI re-renders simulator menus\n this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1;\n try {\n const list = Array.from(this.currentSimulators.values()).map((s: any) => ({\n clientId: s.clientId,\n clientName: s.clientName || (s.shared && s.shared.clientName),\n simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex)\n }));\n console.log('[Controller] roster updated (tick', this.simulatorRosterTick, '):', list);\n console.log('[Controller] currentSimulatorId:', this.currentSimulatorId);\n } catch {}\n } else {\n // No simulator selected yet; still bump tick to refresh menu state\n this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1;\n }\n });\n }\n\n subscribeToChannel() {\n this.clientChannel.subscribe(async (status: string) => {\n if (status === 'SUBSCRIBED') {\n this.clientConnected = true;\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n }\n });\n }\n\n setCurrentSimulator(simId: string) {\n this.currentSimulatorId = simId;\n if (!this.clientChannel) return;\n // Pull fresh presence state from Supabase and switch to the selected simulator's state\n const presenceState = this.clientChannel.presenceState();\n const sim = this.findSimulator(presenceState);\n if (sim) {\n this.magnetViewMetadata = (sim.shared as any).unityMetaData?.MagnetView || [];\n this.simulatorState.update(sim.shared);\n }\n try {\n const sel = this.currentSimulators.get(simId) as any;\n console.log('[Controller] setCurrentSimulator:', simId, 'name=', sel && (sel.clientName || (sel.shared && sel.shared.clientName)), 'index=', sel && (sel.simulatorIndex || (sel.shared && sel.shared.simulatorIndex)));\n } catch {}\n // Persist simulatorIndex in URL\n try {\n const selected = this.currentSimulators.get(simId) as any;\n const simIndex = selected && (selected.simulatorIndex || (selected.shared && selected.shared.simulatorIndex));\n if (simIndex) {\n const url = new URL(window.location.href);\n url.searchParams.set('simulatorIndex', String(simIndex));\n window.history.replaceState({}, '', url.toString());\n console.log('[Controller] URL simulatorIndex set to', simIndex);\n }\n } catch {}\n }\n\n async updatePresenceState() {\n if (this.clientConnected && this.clientChannel) {\n try {\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n } catch (error) {\n console.error('[Connection] Failed to update presence:', error);\n }\n }\n }\n\n findSimulator(presenceState: PresenceState): SimulatorPresence | null {\n let lastSimulator: SimulatorPresence | null = null;\n let simulator: SimulatorPresence | null = null;\n const values = Object.values(presenceState);\n this.currentSimulators = new Map();\n for (const presences of values) {\n for (const presence of presences as Presence[]) {\n // Only count fully-initialized simulators (index assigned)\n const meta: any = presence as any;\n const simIndexTop = typeof meta.simulatorIndex === 'number' ? meta.simulatorIndex : 0;\n const simIndexShared = meta.shared && typeof meta.shared.simulatorIndex === 'number' ? meta.shared.simulatorIndex : 0;\n const simIndex = simIndexTop || simIndexShared;\n if (meta.clientType === 'simulator' && simIndex > 0) {\n // Prefer shared view of fields if available\n const merged: any = {\n ...meta,\n clientName: (meta.shared && meta.shared.clientName) ? meta.shared.clientName : meta.clientName,\n simulatorIndex: simIndex,\n shared: meta.shared || {}\n };\n lastSimulator = merged as SimulatorPresence;\n this.currentSimulators.set(meta.clientId, merged as SimulatorPresence);\n if (meta.clientId == this.currentSimulatorId) {\n simulator = lastSimulator;\n }\n } else if (meta.clientType === 'simulator') {\n console.log('[Controller] ignoring simulator without index yet:', {\n clientId: meta.clientId,\n nameTop: meta.clientName,\n nameShared: meta.shared && meta.shared.clientName,\n idxTop: simIndexTop,\n idxShared: simIndexShared\n });\n }\n }\n }\n if (!simulator) {\n simulator = lastSimulator;\n }\n this.currentSimulatorId = simulator?.clientId || null;\n try {\n const list = Array.from(this.currentSimulators.values()).map((s: any) => ({\n clientId: s.clientId,\n clientName: s.clientName || (s.shared && s.shared.clientName),\n simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex)\n }));\n console.log('[Controller] currentSimulators:', list);\n } catch {}\n return simulator;\n }\n}\n"]} \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.d.ts b/WebSites/controller/build/TabNavigate.d.ts index e18bcac9..a0577834 100644 --- a/WebSites/controller/build/TabNavigate.d.ts +++ b/WebSites/controller/build/TabNavigate.d.ts @@ -3,6 +3,7 @@ export declare class TabNavigate extends TabBase { static get Style(): string; onPointermove(event: PointerEvent): void; changed(): void; + onSimulatorChange(event: CustomEvent): void; } export declare const tabNavigate: (arg0: TabBaseProps) => import("io-gui").VDOMElement; //# sourceMappingURL=TabNavigate.d.ts.map \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.d.ts.map b/WebSites/controller/build/TabNavigate.d.ts.map index 61909f88..49df209a 100644 --- a/WebSites/controller/build/TabNavigate.d.ts.map +++ b/WebSites/controller/build/TabNavigate.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"TabNavigate.d.ts","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,qBACa,WAAY,SAAQ,OAAO;IACpC,MAAM,KAAK,KAAK,WAaf;IAED,aAAa,CAAC,KAAK,EAAE,YAAY;IAUjC,OAAO;CAKV;AAED,eAAO,MAAM,WAAW,GAAY,MAAM,YAAY,iCAErD,CAAC"} \ No newline at end of file +{"version":3,"file":"TabNavigate.d.ts","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,qBACa,WAAY,SAAQ,OAAO;IACpC,MAAM,KAAK,KAAK,WAaf;IAED,aAAa,CAAC,KAAK,EAAE,YAAY;IAUjC,OAAO;IAwCP,iBAAiB,CAAC,KAAK,EAAE,WAAW;CAMvC;AAED,eAAO,MAAM,WAAW,GAAY,MAAM,YAAY,iCAErD,CAAC"} \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.js b/WebSites/controller/build/TabNavigate.js index 04b5709e..7aaefea0 100644 --- a/WebSites/controller/build/TabNavigate.js +++ b/WebSites/controller/build/TabNavigate.js @@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; -import { h2, Register } from 'io-gui'; +import { h2, Register, IoOptionSelect, MenuOption, br } from 'io-gui'; import { TabBase } from './TabBase.js'; let TabNavigate = class TabNavigate extends TabBase { static get Style() { @@ -28,10 +28,48 @@ let TabNavigate = class TabNavigate extends TabBase { } } changed() { + // Force rerender when simulator list changes by reading controller.simulatorRosterTick + void this.controller.simulatorRosterTick; + const simulators = Array.from(this.controller.currentSimulators?.values() || []); + const hasSimulators = simulators.length > 0; + const headerRow = hasSimulators + ? [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: this.controller.currentSimulatorId || '', + option: new MenuOption({ + options: simulators + .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId })) + .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' })) + }), + '@value-input': (e) => this.onSimulatorChange(e) + }) + ] + : [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: '(none)', + option: new MenuOption({ + options: [{ id: '(none)', value: '(none)' }], + disabled: true, + }), + // no handler; locked selector + 'disabled': true + }) + ]; this.render([ - h2('DRAG to pan • SCROLL to zoom'), + ...headerRow, + h2('DRAG to pan • PINCH to zoom'), ]); } + onSimulatorChange(event) { + const newId = event.detail?.value; + if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') { + this.controller.setCurrentSimulator?.(newId); + } + } }; TabNavigate = __decorate([ Register diff --git a/WebSites/controller/build/TabNavigate.js.map b/WebSites/controller/build/TabNavigate.js.map index ea357805..df0d5887 100644 --- a/WebSites/controller/build/TabNavigate.js.map +++ b/WebSites/controller/build/TabNavigate.js.map @@ -1 +1 @@ -{"version":3,"file":"TabNavigate.js","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAG9C,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,OAAO;IACpC,MAAM,KAAK,KAAK;QACZ,OAAO,SAAS,CAAA;;;;;;;;;;;SAWf,CAAC;IACN,CAAC;IAED,aAAa,CAAC,KAAmB;QAC7B,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,CAAC,YAAY,CACxB,KAAK,CAAC,SAAS,GAAG,IAAI,EACtB,KAAK,CAAC,SAAS,GAAG,IAAI,CACzB,CAAC;QACN,CAAC;IACL,CAAC;IAED,OAAO;QACH,IAAI,CAAC,MAAM,CAAC;YACR,EAAE,CAAC,8BAA8B,CAAC;SACrC,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AA/BY,WAAW;IADvB,QAAQ;GACI,WAAW,CA+BvB;;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,UAAS,IAAkB;IAClD,OAAO,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AAC1C,CAAC,CAAC","sourcesContent":["import { h2, Register } from 'io-gui';\nimport { TabBase, TabBaseProps } from './TabBase.js';\n\n@Register\nexport class TabNavigate extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n justify-content: center;\n }\n :host > h2 {\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n }\n `;\n }\n\n onPointermove(event: PointerEvent) {\n super.onPointermove(event);\n if (event.movementX || event.movementY) {\n this.controller.sendPanEvent(\n event.movementX * 0.03,\n event.movementY * 0.03\n );\n }\n }\n\n changed() {\n this.render([\n h2('DRAG to pan • SCROLL to zoom'),\n ]);\n }\n}\n\nexport const tabNavigate = function(arg0: TabBaseProps) {\n return TabNavigate.vConstructor(arg0);\n};"]} \ No newline at end of file +{"version":3,"file":"TabNavigate.js","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"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","sourcesContent":["import { h2, Register, IoOptionSelect, MenuOption, IoOptionSelectProps, br } from 'io-gui';\nimport { TabBase, TabBaseProps } from './TabBase.js';\n\n@Register\nexport class TabNavigate extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n justify-content: center;\n }\n :host > h2 {\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n }\n `;\n }\n\n onPointermove(event: PointerEvent) {\n super.onPointermove(event);\n if (event.movementX || event.movementY) {\n this.controller.sendPanEvent(\n event.movementX * 0.03,\n event.movementY * 0.03\n );\n }\n }\n\n changed() {\n // Force rerender when simulator list changes by reading controller.simulatorRosterTick\n void (this.controller as any).simulatorRosterTick;\n const simulators = Array.from(this.controller.currentSimulators?.values() || []);\n const hasSimulators = simulators.length > 0;\n\n const headerRow = hasSimulators\n ? [\n h2('Simulator:'),\n br(),\n IoOptionSelect.vConstructor({\n value: this.controller.currentSimulatorId || '',\n option: new MenuOption({\n options: simulators\n .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId }))\n .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' }))\n }),\n '@value-input': (e: CustomEvent) => this.onSimulatorChange(e)\n } as IoOptionSelectProps)\n ]\n : [\n h2('Simulator:'),\n br(),\n IoOptionSelect.vConstructor({\n value: '(none)',\n option: new MenuOption({\n options: [{ id: '(none)', value: '(none)' }],\n disabled: true,\n }),\n // no handler; locked selector\n 'disabled': true\n } as IoOptionSelectProps)\n ];\n\n this.render([\n ...headerRow,\n h2('DRAG to pan • PINCH to zoom'),\n ]);\n }\n\n onSimulatorChange(event: CustomEvent) {\n const newId = event.detail?.value;\n if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') {\n (this.controller as any).setCurrentSimulator?.(newId);\n }\n }\n}\n\nexport const tabNavigate = function(arg0: TabBaseProps) {\n return TabNavigate.vConstructor(arg0);\n};"]} \ No newline at end of file diff --git a/WebSites/controller/src/SimulatorState.ts b/WebSites/controller/src/SimulatorState.ts index a60748d3..493079fc 100644 --- a/WebSites/controller/src/SimulatorState.ts +++ b/WebSites/controller/src/SimulatorState.ts @@ -35,12 +35,6 @@ export class SimulatorState extends Node { @ReactiveProperty({type: String}) declare currentScreenId: string; - @ReactiveProperty({type: Number}) - declare currentSearchGravity: number; - - @ReactiveProperty({type: String}) - declare currentSearchString: string; - @ReactiveProperty({type: Object}) declare highlightedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …} @@ -84,8 +78,6 @@ export class SimulatorState extends Node { currentCollectionId: state.currentCollectionId || '', currentCollectionItems: state.currentCollectionItems || [], currentScreenId: state.currentScreenId || '', - currentSearchGravity: state.currentSearchGravity || 0, - currentSearchString: state.currentSearchString || '', highlightedItem: state.highlightedItem || null, highlightedItemId: state.highlightedItemId || '', highlightedItemIds: state.highlightedItemIds || [], diff --git a/WebSites/controller/src/SpacetimeController.ts b/WebSites/controller/src/SpacetimeController.ts index 64dd2b33..86641c50 100644 --- a/WebSites/controller/src/SpacetimeController.ts +++ b/WebSites/controller/src/SpacetimeController.ts @@ -89,12 +89,16 @@ export class SpacetimeController extends IoElement { declare clientChannel: any; declare clientConnected: boolean; declare currentSimulatorId: string | null; + declare currentSimulators: Map; declare magnetViewMetadata: Array; @ReactiveProperty({type: SimulatorState, init: null}) declare simulatorState: SimulatorState; + @ReactiveProperty({type: Number}) + declare simulatorRosterTick: number; + constructor(props: IoElementProps) { super(props); // Client Identity @@ -106,7 +110,9 @@ export class SpacetimeController extends IoElement { this.clientChannel = null; this.clientConnected = false; this.currentSimulatorId = null; + this.currentSimulators = new Map(); this.magnetViewMetadata = []; + this.simulatorRosterTick = 0; this.connect(); } @@ -116,19 +122,26 @@ export class SpacetimeController extends IoElement { return; } try { - const channelName = new URLSearchParams(window.location.search).get('channel') || SpacetimeController.clientChannelName; + const params = new URLSearchParams(window.location.search); + const channelName = params.get('channel') || SpacetimeController.clientChannelName; this.supabaseClient = supabase.createClient(SpacetimeController.supabaseUrl, SpacetimeController.supabaseAnonKey); this.clientChannel = this.supabaseClient.channel(channelName, { config: { presence: { key: this.clientId } } }); this.setupPresenceHandlers(); this.subscribeToChannel(); + // Only honor simulatorIndex param (no legacy) + const simIndexFromUrl = params.get('simulatorIndex'); + if (simIndexFromUrl) { + (this as any)._preselectSimulatorIndex = parseInt(simIndexFromUrl, 10); + } } catch (error) { console.error('Controller connection failed:', error); console.error('[Controller] Connection failed:', error); } } + ready() { this.render([ ioNavigator({ @@ -226,16 +239,51 @@ export class SpacetimeController extends IoElement { this.clientChannel .on('presence', { event: 'sync' }, () => { const presenceState = this.clientChannel.presenceState(); - const simulator = this.findLatestSimulator(presenceState); + try { + const raw = Object.values(presenceState || {}).flat(); + const sims = raw.filter((p: any) => p && p.clientType === 'simulator'); + console.log('[Controller][presence:sync] presences:', raw.length, 'simulators:', sims.length); + console.log('[Controller][presence:sync] simulators raw:', sims.map((p: any) => ({ + clientId: p.clientId, + nameTop: p.clientName, + nameShared: p.shared && p.shared.clientName, + idxTop: typeof p.simulatorIndex === 'number' ? p.simulatorIndex : 0, + idxShared: p.shared && typeof p.shared.simulatorIndex === 'number' ? p.shared.simulatorIndex : 0 + }))); + } catch {} + + const simulator = this.findSimulator(presenceState); if (simulator) { + // If preselect by index requested, override with match when available + if ((this as any)._preselectSimulatorIndex) { + for (const sim of this.currentSimulators.values()) { + const idx = (sim as any).simulatorIndex || ((sim as any).shared && (sim as any).shared.simulatorIndex); + if (idx === (this as any)._preselectSimulatorIndex) { + this.currentSimulatorId = (sim as any).clientId; + break; + } + } + (this as any)._preselectSimulatorIndex = null; + } this.magnetViewMetadata = (simulator.shared as any).unityMetaData?.MagnetView || []; - this.currentSimulatorId = simulator.clientId; + this.currentSimulatorId = this.currentSimulatorId || simulator.clientId; this.simulatorState.update(simulator.shared); + // bump tick so UI re-renders simulator menus + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; + try { + const list = Array.from(this.currentSimulators.values()).map((s: any) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] roster updated (tick', this.simulatorRosterTick, '):', list); + console.log('[Controller] currentSimulatorId:', this.currentSimulatorId); + } catch {} + } else { + // No simulator selected yet; still bump tick to refresh menu state + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; } - }) - .on('broadcast', { event: 'simulatorTakeover' }, (payload: SimulatorTakeoverPayload) => { - this.currentSimulatorId = payload.newSimulatorId; }); } @@ -253,6 +301,33 @@ export class SpacetimeController extends IoElement { }); } + setCurrentSimulator(simId: string) { + this.currentSimulatorId = simId; + if (!this.clientChannel) return; + // Pull fresh presence state from Supabase and switch to the selected simulator's state + const presenceState = this.clientChannel.presenceState(); + const sim = this.findSimulator(presenceState); + if (sim) { + this.magnetViewMetadata = (sim.shared as any).unityMetaData?.MagnetView || []; + this.simulatorState.update(sim.shared); + } + try { + const sel = this.currentSimulators.get(simId) as any; + console.log('[Controller] setCurrentSimulator:', simId, 'name=', sel && (sel.clientName || (sel.shared && sel.shared.clientName)), 'index=', sel && (sel.simulatorIndex || (sel.shared && sel.shared.simulatorIndex))); + } catch {} + // Persist simulatorIndex in URL + try { + const selected = this.currentSimulators.get(simId) as any; + const simIndex = selected && (selected.simulatorIndex || (selected.shared && selected.shared.simulatorIndex)); + if (simIndex) { + const url = new URL(window.location.href); + url.searchParams.set('simulatorIndex', String(simIndex)); + window.history.replaceState({}, '', url.toString()); + console.log('[Controller] URL simulatorIndex set to', simIndex); + } + } catch {} + } + async updatePresenceState() { if (this.clientConnected && this.clientChannel) { try { @@ -268,17 +343,54 @@ export class SpacetimeController extends IoElement { } } - findLatestSimulator(presenceState: PresenceState): SimulatorPresence | null { - let latestSimulator = null; - let latestStartTime = 0; - Object.values(presenceState).forEach(presences => { - presences.forEach((presence: Presence) => { - if (presence.clientType === 'simulator' && presence.startTime > latestStartTime) { - latestSimulator = presence; - latestStartTime = presence.startTime; + findSimulator(presenceState: PresenceState): SimulatorPresence | null { + let lastSimulator: SimulatorPresence | null = null; + let simulator: SimulatorPresence | null = null; + const values = Object.values(presenceState); + this.currentSimulators = new Map(); + for (const presences of values) { + for (const presence of presences as Presence[]) { + // Only count fully-initialized simulators (index assigned) + const meta: any = presence as any; + const simIndexTop = typeof meta.simulatorIndex === 'number' ? meta.simulatorIndex : 0; + const simIndexShared = meta.shared && typeof meta.shared.simulatorIndex === 'number' ? meta.shared.simulatorIndex : 0; + const simIndex = simIndexTop || simIndexShared; + if (meta.clientType === 'simulator' && simIndex > 0) { + // Prefer shared view of fields if available + const merged: any = { + ...meta, + clientName: (meta.shared && meta.shared.clientName) ? meta.shared.clientName : meta.clientName, + simulatorIndex: simIndex, + shared: meta.shared || {} + }; + lastSimulator = merged as SimulatorPresence; + this.currentSimulators.set(meta.clientId, merged as SimulatorPresence); + if (meta.clientId == this.currentSimulatorId) { + simulator = lastSimulator; + } + } else if (meta.clientType === 'simulator') { + console.log('[Controller] ignoring simulator without index yet:', { + clientId: meta.clientId, + nameTop: meta.clientName, + nameShared: meta.shared && meta.shared.clientName, + idxTop: simIndexTop, + idxShared: simIndexShared + }); } - }); - }); - return latestSimulator; + } + } + if (!simulator) { + simulator = lastSimulator; + } + this.currentSimulatorId = simulator?.clientId || null; + try { + const list = Array.from(this.currentSimulators.values()).map((s: any) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] currentSimulators:', list); + } catch {} + return simulator; } -} \ No newline at end of file +} diff --git a/WebSites/controller/src/TabNavigate.ts b/WebSites/controller/src/TabNavigate.ts index 602369d4..cebecd50 100644 --- a/WebSites/controller/src/TabNavigate.ts +++ b/WebSites/controller/src/TabNavigate.ts @@ -1,4 +1,4 @@ -import { h2, Register } from 'io-gui'; +import { h2, Register, IoOptionSelect, MenuOption, IoOptionSelectProps, br } from 'io-gui'; import { TabBase, TabBaseProps } from './TabBase.js'; @Register @@ -29,10 +29,51 @@ export class TabNavigate extends TabBase { } changed() { + // Force rerender when simulator list changes by reading controller.simulatorRosterTick + void (this.controller as any).simulatorRosterTick; + const simulators = Array.from(this.controller.currentSimulators?.values() || []); + const hasSimulators = simulators.length > 0; + + const headerRow = hasSimulators + ? [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: this.controller.currentSimulatorId || '', + option: new MenuOption({ + options: simulators + .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId })) + .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' })) + }), + '@value-input': (e: CustomEvent) => this.onSimulatorChange(e) + } as IoOptionSelectProps) + ] + : [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: '(none)', + option: new MenuOption({ + options: [{ id: '(none)', value: '(none)' }], + disabled: true, + }), + // no handler; locked selector + 'disabled': true + } as IoOptionSelectProps) + ]; + this.render([ - h2('DRAG to pan • SCROLL to zoom'), + ...headerRow, + h2('DRAG to pan • PINCH to zoom'), ]); } + + onSimulatorChange(event: CustomEvent) { + const newId = event.detail?.value; + if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') { + (this.controller as any).setCurrentSimulator?.(newId); + } + } } export const tabNavigate = function(arg0: TabBaseProps) {