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":"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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: 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) {