using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Native
{
// Panel protocol: extract complex UI panels (Grid Settings, Snap Increment, Draw Mode)
// and render them interactively in the browser frontend.
internal static partial class NativeWindowBridgeHost
{
// =================== Grid Settings Panel ===================
// Reflection cache for SceneView.sceneViewGrids (SceneViewGrid).
private static bool s_GridReflectionDone;
private static PropertyInfo s_SceneViewGridsProp;
private static PropertyInfo s_GridAxisProp;
private static PropertyInfo s_GridOpacityProp;
private static MethodInfo s_SetAllGridsPivotMethod;
private static MethodInfo s_ResetPivotMethod;
private static PropertyInfo s_GridSettingsSizeProp;
private static System.Type s_GridRenderAxisType;
private static void EnsureGridReflection()
{
if (s_GridReflectionDone) return;
s_GridReflectionDone = true;
try
{
var asm = typeof(EditorWindow).Assembly;
s_SceneViewGridsProp = typeof(SceneView).GetProperty("sceneViewGrids",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
System.Type svgType = asm.GetType("UnityEditor.SceneViewGrid");
if (svgType != null)
{
s_GridAxisProp = svgType.GetProperty("gridAxis",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
s_GridOpacityProp = svgType.GetProperty("gridOpacity",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
s_SetAllGridsPivotMethod = svgType.GetMethod("SetAllGridsPivot",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
null, new System.Type[] { typeof(Vector3) }, null);
s_ResetPivotMethod = svgType.GetMethod("ResetPivot",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
s_GridRenderAxisType = svgType.GetNestedType("GridRenderAxis",
BindingFlags.NonPublic | BindingFlags.Public);
}
System.Type gsType = asm.GetType("UnityEditor.GridSettings");
if (gsType != null)
s_GridSettingsSizeProp = gsType.GetProperty("size",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
CodelyLogger.Log($"[NWB-GridPanel] Reflection OK: sceneViewGrids={s_SceneViewGridsProp != null}" +
$" gridAxis={s_GridAxisProp != null} gridOpacity={s_GridOpacityProp != null}" +
$" SetAllGridsPivot={s_SetAllGridsPivotMethod != null} ResetPivot={s_ResetPivotMethod != null}" +
$" GridSettings.size={s_GridSettingsSizeProp != null} GridRenderAxis={s_GridRenderAxisType != null}");
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-GridPanel] Reflection init error: {ex.Message}");
}
}
///
/// Extract Grid Settings panel data from the active SceneView and send
/// a panel_show message to the browser frontend.
///
private static bool TryExtractGridSettingsPanel(float relX, float relY)
{
EnsureGridReflection();
if (s_SceneViewGridsProp == null || s_GridAxisProp == null || s_GridOpacityProp == null)
{
CodelyLogger.LogWarning("[NWB-GridPanel] Required reflection targets not available");
return false;
}
try
{
SceneView sv = s_OffscreenTarget as SceneView;
if (sv == null) sv = SceneView.lastActiveSceneView;
if (sv == null)
{
CodelyLogger.LogWarning("[NWB-GridPanel] No active SceneView found");
return false;
}
object grids = s_SceneViewGridsProp.GetValue(sv);
if (grids == null)
{
CodelyLogger.LogWarning("[NWB-GridPanel] sceneViewGrids is null");
return false;
}
object axisObj = s_GridAxisProp.GetValue(grids);
int axisIndex = (int)axisObj;
float opacity = (float)s_GridOpacityProp.GetValue(grids);
s_FrontendPopupIdCounter++;
s_CurrentFrontendPopupId = "panel_" + s_FrontendPopupIdCounter;
s_PopupSourceType = "grid_settings";
var sb = new StringBuilder(1024);
sb.Append("{\"type\":\"panel_show\",\"id\":\"");
sb.Append(s_CurrentFrontendPopupId);
sb.Append("\",\"x\":");
sb.Append(Mathf.RoundToInt(relX));
sb.Append(",\"y\":");
sb.Append(Mathf.RoundToInt(relY));
AppendToolbarAnchorRectJson(sb);
sb.Append(",\"title\":\"Grid Visual\",\"sourceType\":\"grid_settings\",\"controls\":[");
sb.Append("{\"id\":\"gridPlane\",\"type\":\"button_strip\",\"label\":\"Grid Plane\",\"tooltip\":\"Choose which axis-aligned plane is visible in Scene view.\",");
sb.Append("\"options\":[\"X\",\"Y\",\"Z\"],\"value\":");
sb.Append(axisIndex);
sb.Append("},");
sb.Append("{\"id\":\"gridOpacity\",\"type\":\"slider\",\"label\":\"Opacity\",\"tooltip\":\"Adjust Scene grid opacity.\",");
sb.Append("\"min\":0,\"max\":1,\"step\":0.01,\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F2}", opacity);
sb.Append("},");
sb.Append("{\"id\":\"moveTo\",\"type\":\"button_row\",\"label\":\"Move To\",\"tooltip\":\"Move the Scene grid pivot.\",");
sb.Append("\"buttons\":[");
sb.Append("{\"id\":\"toHandle\",\"label\":\"To Handle\"},");
sb.Append("{\"id\":\"toOrigin\",\"label\":\"To Origin\"}");
sb.Append("]}");
sb.Append("]}");
string json = sb.ToString();
bool sent = SendDataChannelMessage(json);
s_FrontendPopupSent = sent;
if (sent)
CodelyLogger.Log($"[NWB-GridPanel] Sent grid panel: id={s_CurrentFrontendPopupId} axis={axisIndex} opacity={opacity:F2}");
else
CodelyLogger.LogWarning("[NWB-GridPanel] Failed to send grid panel via DataChannel");
return sent;
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-GridPanel] Error extracting grid settings: {ex.Message}\n{ex.StackTrace}");
return false;
}
}
private static void HandleGridPanelChange(string json, string controlId)
{
float value = ExtractJsonFloat(json, "value");
SceneView sv = s_OffscreenTarget as SceneView;
if (sv == null) sv = SceneView.lastActiveSceneView;
if (sv == null) return;
object grids = s_SceneViewGridsProp?.GetValue(sv);
if (grids == null) return;
switch (controlId)
{
case "gridPlane":
if (s_GridAxisProp != null && s_GridRenderAxisType != null)
{
object enumVal = System.Enum.ToObject(s_GridRenderAxisType, (int)value);
s_GridAxisProp.SetValue(grids, enumVal);
CodelyLogger.Log($"[NWB-GridPanel] Set gridAxis to {enumVal}");
}
break;
case "gridOpacity":
if (s_GridOpacityProp != null)
{
s_GridOpacityProp.SetValue(grids, value);
CodelyLogger.Log($"[NWB-GridPanel] Set gridOpacity to {value:F2}");
}
break;
case "toHandle":
if (s_SetAllGridsPivotMethod != null)
{
Vector3 handlePos = UnityEditor.Tools.handlePosition;
Vector3 gridSize = Vector3.one;
if (s_GridSettingsSizeProp != null)
gridSize = (Vector3)s_GridSettingsSizeProp.GetValue(null);
Vector3 snapped = Snapping.Snap(handlePos, gridSize);
s_SetAllGridsPivotMethod.Invoke(grids, new object[] { snapped });
CodelyLogger.Log($"[NWB-GridPanel] SetAllGridsPivot({snapped})");
}
break;
case "toOrigin":
if (s_ResetPivotMethod != null && s_GridRenderAxisType != null)
{
object allVal = System.Enum.ToObject(s_GridRenderAxisType, 3);
s_ResetPivotMethod.Invoke(grids, new object[] { allVal });
CodelyLogger.Log("[NWB-GridPanel] ResetPivot(All)");
}
break;
default:
CodelyLogger.LogWarning($"[NWB-GridPanel] Unknown controlId: {controlId}");
break;
}
SceneView.RepaintAll();
}
// =================== Snap Increment Panel ===================
///
/// Extract Snap Increment settings and send a panel_show message.
/// Uses public EditorSnapSettings API — no reflection needed.
///
private static bool TryExtractSnapIncrementPanel(float relX, float relY)
{
try
{
Vector3 moveVal = EditorSnapSettings.move;
float rotateVal = EditorSnapSettings.rotate;
float scaleVal = EditorSnapSettings.scale;
bool linked = Mathf.Approximately(moveVal.x, moveVal.y) &&
Mathf.Approximately(moveVal.y, moveVal.z);
s_FrontendPopupIdCounter++;
s_CurrentFrontendPopupId = "panel_" + s_FrontendPopupIdCounter;
s_PopupSourceType = "snap_increment";
var sb = new StringBuilder(512);
sb.Append("{\"type\":\"panel_show\",\"id\":\"");
sb.Append(s_CurrentFrontendPopupId);
sb.Append("\",\"x\":");
sb.Append(Mathf.RoundToInt(relX));
sb.Append(",\"y\":");
sb.Append(Mathf.RoundToInt(relY));
AppendToolbarAnchorRectJson(sb);
sb.Append(",\"title\":\"Increment Snapping\",\"sourceType\":\"snap_increment\",\"controls\":[");
sb.Append("{\"id\":\"snapMove\",\"type\":\"linked_vector3\",\"label\":\"Move\",\"tooltip\":\"Set move snap increments for X/Y/Z.\",");
sb.Append("\"linked\":");
sb.Append(linked ? "true" : "false");
sb.Append(",\"value\":[");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture,
"{0},{1},{2}", moveVal.x, moveVal.y, moveVal.z);
sb.Append("]},");
sb.Append("{\"id\":\"snapRotate\",\"type\":\"float_field\",\"label\":\"Rotate\",\"tooltip\":\"Set rotate snap increment in degrees.\",\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}", rotateVal);
sb.Append(",\"step\":1},");
sb.Append("{\"id\":\"snapScale\",\"type\":\"float_field\",\"label\":\"Scale\",\"tooltip\":\"Set scale snap increment.\",\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0}", scaleVal);
sb.Append(",\"step\":0.01}");
sb.Append("]}");
string json = sb.ToString();
bool sent = SendDataChannelMessage(json);
s_FrontendPopupSent = sent;
if (sent)
CodelyLogger.Log($"[NWB-SnapPanel] Sent snap increment panel: id={s_CurrentFrontendPopupId} move=({moveVal.x},{moveVal.y},{moveVal.z}) rotate={rotateVal} scale={scaleVal}");
else
CodelyLogger.LogWarning("[NWB-SnapPanel] Failed to send snap increment panel via DataChannel");
return sent;
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-SnapPanel] Error extracting snap settings: {ex.Message}\n{ex.StackTrace}");
return false;
}
}
private static void HandleSnapPanelChange(string json, string controlId)
{
switch (controlId)
{
case "snapMove":
{
string valStr = ExtractJsonString(json, "value");
if (string.IsNullOrEmpty(valStr))
{
float x = ExtractJsonArrayFloat(json, "value", 0);
float y = ExtractJsonArrayFloat(json, "value", 1);
float z = ExtractJsonArrayFloat(json, "value", 2);
EditorSnapSettings.move = new Vector3(x, y, z);
CodelyLogger.Log($"[NWB-SnapPanel] Set move to ({x},{y},{z})");
}
break;
}
case "snapMove_link":
break;
case "snapRotate":
{
float value = ExtractJsonFloat(json, "value");
EditorSnapSettings.rotate = value;
CodelyLogger.Log($"[NWB-SnapPanel] Set rotate to {value}");
break;
}
case "snapScale":
{
float value = ExtractJsonFloat(json, "value");
EditorSnapSettings.scale = value;
CodelyLogger.Log($"[NWB-SnapPanel] Set scale to {value}");
break;
}
default:
CodelyLogger.LogWarning($"[NWB-SnapPanel] Unknown controlId: {controlId}");
break;
}
if (s_OffscreenTarget != null) s_OffscreenTarget.Repaint();
}
// =================== Snap Settings (Grid Snapping) Panel ===================
// Reflection cache: EditorSnapSettings.gridSize exists in Tuanjie engine
// but not in standard Unity. Fall back to EditorSnapSettings.move.
private static bool s_SnapGridSizeReflectionDone;
private static PropertyInfo s_SnapGridSizeProp;
private static Vector3 GetSnapGridSize()
{
if (!s_SnapGridSizeReflectionDone)
{
s_SnapGridSizeReflectionDone = true;
s_SnapGridSizeProp = typeof(EditorSnapSettings).GetProperty("gridSize",
BindingFlags.Public | BindingFlags.Static);
}
if (s_SnapGridSizeProp != null)
return (Vector3)s_SnapGridSizeProp.GetValue(null);
return EditorSnapSettings.move;
}
private static void SetSnapGridSize(Vector3 value)
{
if (!s_SnapGridSizeReflectionDone)
{
s_SnapGridSizeReflectionDone = true;
s_SnapGridSizeProp = typeof(EditorSnapSettings).GetProperty("gridSize",
BindingFlags.Public | BindingFlags.Static);
}
if (s_SnapGridSizeProp != null && s_SnapGridSizeProp.CanWrite)
{
s_SnapGridSizeProp.SetValue(null, value);
return;
}
EditorSnapSettings.move = value;
}
///
/// Extract Grid Snapping settings and send a panel_show message.
/// Uses EditorSnapSettings.gridSize (Tuanjie) or .move (standard Unity).
///
private static bool TryExtractSnapSettingsPanel(float relX, float relY)
{
try
{
Vector3 gridSize = GetSnapGridSize();
// Check if axes are linked.
bool linked = Mathf.Approximately(gridSize.x, gridSize.y) &&
Mathf.Approximately(gridSize.y, gridSize.z);
s_FrontendPopupIdCounter++;
s_CurrentFrontendPopupId = "panel_" + s_FrontendPopupIdCounter;
s_PopupSourceType = "snap_settings";
var sb = new StringBuilder(512);
sb.Append("{\"type\":\"panel_show\",\"id\":\"");
sb.Append(s_CurrentFrontendPopupId);
sb.Append("\",\"x\":");
sb.Append(Mathf.RoundToInt(relX));
sb.Append(",\"y\":");
sb.Append(Mathf.RoundToInt(relY));
AppendToolbarAnchorRectJson(sb);
sb.Append(",\"title\":\"Grid Snapping\",\"sourceType\":\"snap_settings\",\"controls\":[");
// Grid Size (linked X/Y/Z)
sb.Append("{\"id\":\"gridSize\",\"type\":\"linked_vector3\",\"label\":\"Grid Size\",\"tooltip\":\"Set grid snapping size for X/Y/Z.\",");
sb.Append("\"linked\":");
sb.Append(linked ? "true" : "false");
sb.Append(",\"value\":[");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture,
"{0},{1},{2}", gridSize.x, gridSize.y, gridSize.z);
sb.Append("]},");
// Align Selected (button row)
sb.Append("{\"id\":\"alignSelected\",\"type\":\"button_row\",\"label\":\"Align Selected\",\"tooltip\":\"Snap selected objects to the grid.\",");
sb.Append("\"buttons\":[");
sb.Append("{\"id\":\"alignAll\",\"label\":\"All Axes\"},");
sb.Append("{\"id\":\"alignX\",\"label\":\"X\"},");
sb.Append("{\"id\":\"alignY\",\"label\":\"Y\"},");
sb.Append("{\"id\":\"alignZ\",\"label\":\"Z\"}");
sb.Append("]}");
sb.Append("]}");
string json = sb.ToString();
bool sent = SendDataChannelMessage(json);
s_FrontendPopupSent = sent;
if (sent)
CodelyLogger.Log($"[NWB-SnapSettingsPanel] Sent grid snapping panel: id={s_CurrentFrontendPopupId} gridSize=({gridSize.x},{gridSize.y},{gridSize.z})");
else
CodelyLogger.LogWarning("[NWB-SnapSettingsPanel] Failed to send grid snapping panel via DataChannel");
return sent;
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-SnapSettingsPanel] Error extracting snap settings: {ex.Message}\n{ex.StackTrace}");
return false;
}
}
private static void HandleSnapSettingsPanelChange(string json, string controlId)
{
switch (controlId)
{
case "gridSize":
{
float x = ExtractJsonArrayFloat(json, "value", 0);
float y = ExtractJsonArrayFloat(json, "value", 1);
float z = ExtractJsonArrayFloat(json, "value", 2);
SetSnapGridSize(new Vector3(x, y, z));
CodelyLogger.Log($"[NWB-SnapSettingsPanel] Set gridSize to ({x},{y},{z})");
break;
}
case "gridSize_link":
break;
case "alignAll":
case "alignX":
case "alignY":
case "alignZ":
{
var selections = Selection.transforms;
if (selections != null && selections.Length > 0)
{
Undo.RecordObjects(selections, "Snap to Grid");
SnapAxis axis = SnapAxis.All;
if (controlId == "alignX") axis = SnapAxis.X;
else if (controlId == "alignY") axis = SnapAxis.Y;
else if (controlId == "alignZ") axis = SnapAxis.Z;
Handles.SnapToGrid(selections, axis);
CodelyLogger.Log($"[NWB-SnapSettingsPanel] SnapToGrid axis={axis}");
}
else
{
CodelyLogger.Log("[NWB-SnapSettingsPanel] No selection for SnapToGrid");
}
break;
}
default:
CodelyLogger.LogWarning($"[NWB-SnapSettingsPanel] Unknown controlId: {controlId}");
break;
}
if (s_OffscreenTarget != null) s_OffscreenTarget.Repaint();
}
private static void HandleSceneViewCameraPanelChange(string json, string controlId)
{
SceneView sv = s_OffscreenTarget as SceneView;
if (sv == null) sv = SceneView.lastActiveSceneView;
if (sv == null) return;
object settings = sv.cameraSettings;
System.Type settingsType = settings.GetType();
bool SetMember(string name, object memberValue)
{
FieldInfo f = settingsType.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (f != null)
{
try { f.SetValue(settings, memberValue); return true; } catch { }
}
PropertyInfo p = settingsType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (p != null && p.CanWrite)
{
try { p.SetValue(settings, memberValue); return true; } catch { }
}
return false;
}
float value = ExtractJsonFloat(json, "value");
bool boolValue = ExtractJsonBool(json, "value");
bool dirty = false;
switch (controlId)
{
case "cameraFov":
dirty = SetMember("fieldOfView", Mathf.Clamp(value, 4f, 120f));
break;
case "dynamicClip":
dirty = SetMember("dynamicClip", boolValue || value >= 0.5f);
break;
case "nearClip":
dirty = SetMember("nearClip", Mathf.Max(0.0001f, value));
break;
case "farClip":
dirty = SetMember("farClip", Mathf.Max(0.001f, value));
break;
case "occlusionCulling":
dirty = SetMember("occlusionCulling", boolValue || value >= 0.5f);
break;
case "backfaceCulling":
dirty = SetMember("backfaceCulling", boolValue || value >= 0.5f);
break;
case "cameraEasing":
dirty = SetMember("easingEnabled", boolValue || value >= 0.5f);
break;
case "cameraAccel":
dirty = SetMember("accelerationEnabled", boolValue || value >= 0.5f);
break;
case "cameraSpeed":
dirty = SetMember("speed", Mathf.Max(0.0001f, value));
break;
case "speedMin":
case "speedMax":
{
float speedMin = 0.01f;
float speedMax = 2f;
FieldInfo minFld = settingsType.GetField("speedMin", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo maxFld = settingsType.GetField("speedMax", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (minFld != null && minFld.FieldType == typeof(float)) speedMin = (float)minFld.GetValue(settings);
if (maxFld != null && maxFld.FieldType == typeof(float)) speedMax = (float)maxFld.GetValue(settings);
if (controlId == "speedMin") speedMin = Mathf.Max(0.0001f, value);
else speedMax = Mathf.Max(0.0001f, value);
if (speedMax <= speedMin) speedMax = speedMin + 0.001f;
MethodInfo setMinMaxMethod = settingsType.GetMethod("SetSpeedMinMax",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
null, new[] { typeof(float), typeof(float) }, null);
if (setMinMaxMethod != null)
{
try
{
setMinMaxMethod.Invoke(settings, new object[] { speedMin, speedMax });
dirty = true;
}
catch { }
}
if (!dirty)
{
bool setMin = SetMember("speedMin", speedMin);
bool setMax = SetMember("speedMax", speedMax);
dirty = setMin || setMax;
}
break;
}
}
if (dirty)
{
PropertyInfo cameraSettingsProp = typeof(SceneView).GetProperty("cameraSettings",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
cameraSettingsProp?.SetValue(sv, settings);
sv.Repaint();
}
}
// ExtractJsonArrayFloat moved to NativeWindowBridgeHost.Json.cs
// =================== Draw Mode Popup ===================
private static string GetPopupWindowContentTypeName(object containerWindow)
{
try
{
object wc = GetPopupWindowContentObject(containerWindow);
return wc?.GetType().Name ?? "";
}
catch { return ""; }
}
private static object GetPopupWindowContentObject(object containerWindow)
{
if (containerWindow == null) return null;
System.Type cwType = containerWindow.GetType();
PropertyInfo rootViewProp = cwType.GetProperty("rootView",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
object rootView = rootViewProp?.GetValue(containerWindow);
if (rootView == null) return null;
PropertyInfo avProp = rootView.GetType().GetProperty("actualView",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
object popupWindow = avProp?.GetValue(rootView);
if (popupWindow == null) return null;
FieldInfo wcField = popupWindow.GetType().GetField("m_WindowContent",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (wcField == null) return null;
return wcField.GetValue(popupWindow);
}
private static bool TryExtractSceneFXPopup(float relX, float relY)
{
try
{
SceneView sv = s_OffscreenTarget as SceneView;
if (sv == null) sv = SceneView.lastActiveSceneView;
if (sv == null) return false;
object state = sv.sceneViewState;
if (state == null) return false;
System.Type stateType = state.GetType();
bool ReadBool(string name, bool fallback)
{
PropertyInfo p = stateType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (p != null && p.PropertyType == typeof(bool))
{
try { return (bool)p.GetValue(state); } catch { return fallback; }
}
FieldInfo f = stateType.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (f != null && f.FieldType == typeof(bool))
{
try { return (bool)f.GetValue(state); } catch { return fallback; }
}
return fallback;
}
var items = new (string label, string key)[]
{
("Skybox", "showSkybox"),
("Fog", "showFog"),
("Flares", "showFlares"),
("Always Refresh", "alwaysRefresh"),
("Post Processing", "showImageEffects"),
("Particle Systems", "showParticleSystems"),
("Visual Effect Graphs", "showVisualEffectGraphs")
};
bool vfxActive = false;
try
{
System.Type vfxMgrType = Type.GetType("UnityEngine.VFX.VFXManager, UnityEngine.VFXModule");
PropertyInfo activateProp = vfxMgrType?.GetProperty("activateVFX", BindingFlags.Static | BindingFlags.Public);
if (activateProp != null && activateProp.PropertyType == typeof(bool))
vfxActive = (bool)activateProp.GetValue(null);
}
catch { }
s_FrontendPopupIdCounter++;
s_CurrentFrontendPopupId = "popup_" + s_FrontendPopupIdCounter;
s_PopupSourceType = "scene_fx";
s_LastFrontendPopupX = relX;
s_LastFrontendPopupY = relY;
s_CachedPopupItemPaths = new string[items.Length];
var sb = new StringBuilder(2048);
sb.Append("{\"type\":\"popup_show\",\"id\":\"");
sb.Append(s_CurrentFrontendPopupId);
sb.Append("\",\"x\":");
sb.Append(Mathf.RoundToInt(relX));
sb.Append(",\"y\":");
sb.Append(Mathf.RoundToInt(relY));
sb.Append(",\"sourceType\":\"scene_fx\",\"items\":[");
bool first = true;
int validCount = 0;
for (int i = 0; i < items.Length; i++)
{
if (items[i].key == "showVisualEffectGraphs" && !vfxActive)
continue;
if (!first) sb.Append(',');
bool checkedVal = ReadBool(items[i].key, true);
string path = "scenefx|" + items[i].key;
s_CachedPopupItemPaths[validCount] = path;
sb.Append("{\"index\":");
sb.Append(validCount);
sb.Append(",\"label\":\"");
sb.Append(EscapeJsonString(items[i].label));
sb.Append("\",\"tooltip\":\"");
sb.Append(EscapeJsonString("Toggle " + items[i].label + " in Scene view"));
sb.Append("\",\"path\":\"");
sb.Append(path);
sb.Append("\",\"enabled\":true,\"checked\":");
sb.Append(checkedVal ? "true" : "false");
sb.Append(",\"separator\":false,\"keepOpen\":true}");
first = false;
validCount++;
}
sb.Append("]}");
bool sent = SendDataChannelMessage(sb.ToString());
s_FrontendPopupSent = sent;
if (sent) CodelyLogger.Log($"[NWB-SceneFX] Sent popup: id={s_CurrentFrontendPopupId} items={validCount}");
return sent;
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-SceneFX] Error extracting popup: {ex.Message}");
return false;
}
}
private static bool HandleSceneFXToggle(string path)
{
if (string.IsNullOrEmpty(path) || !path.StartsWith("scenefx|", StringComparison.Ordinal))
return false;
SceneView sv = s_OffscreenTarget as SceneView;
if (sv == null) sv = SceneView.lastActiveSceneView;
if (sv == null) return false;
object state = sv.sceneViewState;
if (state == null) return false;
string key = path.Substring("scenefx|".Length);
System.Type stateType = state.GetType();
bool current = false;
bool found = false;
PropertyInfo p = stateType.GetProperty(key, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (p != null && p.PropertyType == typeof(bool) && p.CanRead && p.CanWrite)
{
current = (bool)p.GetValue(state);
p.SetValue(state, !current);
found = true;
}
else
{
FieldInfo f = stateType.GetField(key, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (f != null && f.FieldType == typeof(bool))
{
current = (bool)f.GetValue(state);
f.SetValue(state, !current);
found = true;
}
}
if (!found) return false;
sv.Repaint();
return true;
}
private static bool TryExtractSceneViewCameraPanel(float relX, float relY)
{
try
{
SceneView sv = s_OffscreenTarget as SceneView;
if (sv == null) sv = SceneView.lastActiveSceneView;
if (sv == null) return false;
object settings = sv.cameraSettings;
System.Type settingsType = settings.GetType();
float GetFloat(string name, float fallback)
{
FieldInfo f = settingsType.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (f != null && f.FieldType == typeof(float))
{
try { return (float)f.GetValue(settings); } catch { return fallback; }
}
PropertyInfo p = settingsType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (p != null && p.PropertyType == typeof(float))
{
try { return (float)p.GetValue(settings); } catch { return fallback; }
}
return fallback;
}
bool GetBool(string name, bool fallback)
{
FieldInfo f = settingsType.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (f != null && f.FieldType == typeof(bool))
{
try { return (bool)f.GetValue(settings); } catch { return fallback; }
}
PropertyInfo p = settingsType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (p != null && p.PropertyType == typeof(bool))
{
try { return (bool)p.GetValue(settings); } catch { return fallback; }
}
return fallback;
}
float speedMin = GetFloat("speedMin", 0.01f);
float speedMax = GetFloat("speedMax", 2f);
float speed = GetFloat("speed", speedMin);
if (speedMax <= speedMin) speedMax = speedMin + 0.01f;
speed = Mathf.Clamp(speed, speedMin, speedMax);
s_FrontendPopupIdCounter++;
s_CurrentFrontendPopupId = "panel_" + s_FrontendPopupIdCounter;
s_PopupSourceType = "scene_camera";
s_LastFrontendPopupX = relX;
s_LastFrontendPopupY = relY;
var sb = new StringBuilder(3072);
sb.Append("{\"type\":\"panel_show\",\"id\":\"");
sb.Append(s_CurrentFrontendPopupId);
sb.Append("\",\"x\":");
sb.Append(Mathf.RoundToInt(relX));
sb.Append(",\"y\":");
sb.Append(Mathf.RoundToInt(relY));
sb.Append(",\"title\":\"Scene Camera\",\"sourceType\":\"scene_camera\",\"controls\":[");
bool dynamicClip = GetBool("dynamicClip", true);
bool isOrthographic = sv.orthographic;
bool backfaceEnabled = true;
try
{
// Use reflection to stay compatible with editor versions without GPU-driven pipeline APIs.
System.Type graphicsSettingsType = typeof(UnityEngine.Rendering.GraphicsSettings);
PropertyInfo enableGdrpProp = graphicsSettingsType.GetProperty("enableGDRP",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (enableGdrpProp != null && enableGdrpProp.PropertyType == typeof(bool))
backfaceEnabled = (bool)enableGdrpProp.GetValue(null);
}
catch { }
sb.Append("{\"id\":\"cameraFov\",\"type\":\"slider\",\"label\":\"Field of View\",\"tooltip\":\"The height of the camera's view angle. Measured in degrees vertically, or along the local Y axis.\",\"min\":4,\"max\":120,\"step\":0.1,\"disabled\":");
sb.Append(isOrthographic ? "true" : "false");
sb.Append(",\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F2}", GetFloat("fieldOfView", 60f));
sb.Append("},");
sb.Append("{\"id\":\"dynamicClip\",\"type\":\"checkbox\",\"label\":\"Dynamic Clipping\",\"tooltip\":\"Check this to enable camera's near and far clipping planes to be calculated relative to the viewport size of the Scene.\",\"value\":");
sb.Append(dynamicClip ? "true" : "false");
sb.Append("},");
sb.Append("{\"id\":\"sectionClipPlanes\",\"type\":\"section_header\",\"label\":\"Clipping Planes\"},");
sb.Append("{\"id\":\"nearClip\",\"type\":\"float_field\",\"label\":\"Near\",\"tooltip\":\"Near clipping plane.\",\"subField\":true,\"disabled\":");
sb.Append(dynamicClip ? "true" : "false");
sb.Append(",\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F4}", GetFloat("nearClip", 0.03f));
sb.Append("},");
sb.Append("{\"id\":\"farClip\",\"type\":\"float_field\",\"label\":\"Far\",\"tooltip\":\"Far clipping plane.\",\"subField\":true,\"disabled\":");
sb.Append(dynamicClip ? "true" : "false");
sb.Append(",\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F2}", GetFloat("farClip", 10000f));
sb.Append("},");
sb.Append("{\"id\":\"occlusionCulling\",\"type\":\"checkbox\",\"label\":\"Occlusion Culling\",\"tooltip\":\"Check this to enable occlusion culling in the Scene view.\",\"value\":");
sb.Append(GetBool("occlusionCulling", false) ? "true" : "false");
sb.Append("},");
sb.Append("{\"id\":\"backfaceCulling\",\"type\":\"checkbox\",\"label\":\"Backface Culling\",\"tooltip\":\"Check this to enable backface culling in the Scene View.\",\"disabled\":");
sb.Append(backfaceEnabled ? "false" : "true");
sb.Append(",\"value\":");
sb.Append(GetBool("backfaceCulling", false) ? "true" : "false");
sb.Append("},");
sb.Append("{\"id\":\"sectionNavigation\",\"type\":\"section_header\",\"label\":\"Navigation\"},");
sb.Append("{\"id\":\"cameraEasing\",\"type\":\"checkbox\",\"label\":\"Camera Easing\",\"tooltip\":\"Check this to enable camera movement easing.\",\"value\":");
sb.Append(GetBool("easingEnabled", false) ? "true" : "false");
sb.Append("},");
sb.Append("{\"id\":\"cameraAccel\",\"type\":\"checkbox\",\"label\":\"Camera Acceleration\",\"tooltip\":\"Check this to enable acceleration when moving the camera.\",\"value\":");
sb.Append(GetBool("accelerationEnabled", true) ? "true" : "false");
sb.Append("},");
sb.Append("{\"id\":\"cameraSpeed\",\"type\":\"slider\",\"label\":\"Camera Speed\",\"tooltip\":\"The current speed of the camera in the Scene view.\",\"min\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F4}", speedMin);
sb.Append(",\"max\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F4}", speedMax);
sb.Append(",\"step\":0.01,\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F4}", speed);
sb.Append("},");
sb.Append("{\"id\":\"speedMin\",\"type\":\"float_field\",\"label\":\"Min\",\"tooltip\":\"The minimum speed of the camera in the Scene view.\",\"subField\":true,\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F4}", speedMin);
sb.Append("},");
sb.Append("{\"id\":\"speedMax\",\"type\":\"float_field\",\"label\":\"Max\",\"tooltip\":\"The maximum speed of the camera in the Scene view.\",\"subField\":true,\"value\":");
sb.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "{0:F4}", speedMax);
sb.Append("}");
sb.Append("]}");
bool sent = SendDataChannelMessage(sb.ToString());
s_FrontendPopupSent = sent;
if (sent) CodelyLogger.Log($"[NWB-SceneCamera] Sent panel: id={s_CurrentFrontendPopupId}");
return sent;
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-SceneCamera] Error extracting panel: {ex.Message}");
return false;
}
}
private static bool s_DrawModeReflectionDone;
private static object[] s_BuiltinCameraModes;
private static FieldInfo s_CameraModeDrawMode;
private static FieldInfo s_CameraModeName;
private static FieldInfo s_CameraModeSection;
private static MethodInfo s_IsCameraDrawModeSupportedMethod;
private static void EnsureDrawModeReflection()
{
if (s_DrawModeReflectionDone) return;
s_DrawModeReflectionDone = true;
try
{
System.Type srmType = typeof(EditorWindow).Assembly.GetType("UnityEditor.SceneRenderModeWindow");
if (srmType == null) { CodelyLogger.LogWarning("[NWB-DrawMode] SceneRenderModeWindow type not found"); return; }
System.Type stylesType = srmType.GetNestedType("Styles", BindingFlags.NonPublic | BindingFlags.Public);
if (stylesType == null) { CodelyLogger.LogWarning("[NWB-DrawMode] Styles nested type not found"); return; }
FieldInfo modesField = stylesType.GetField("sBuiltinCameraModes",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (modesField == null) { CodelyLogger.LogWarning("[NWB-DrawMode] sBuiltinCameraModes field not found"); return; }
System.Array modesArray = modesField.GetValue(null) as System.Array;
if (modesArray == null || modesArray.Length == 0) return;
s_BuiltinCameraModes = new object[modesArray.Length];
for (int i = 0; i < modesArray.Length; i++)
s_BuiltinCameraModes[i] = modesArray.GetValue(i);
System.Type cmType = modesArray.GetValue(0).GetType();
s_CameraModeDrawMode = cmType.GetField("drawMode", BindingFlags.Instance | BindingFlags.Public);
s_CameraModeName = cmType.GetField("name", BindingFlags.Instance | BindingFlags.Public);
s_CameraModeSection = cmType.GetField("section", BindingFlags.Instance | BindingFlags.Public);
// SceneView.IsCameraDrawModeSupported is internal in the Tuanjie engine
// (public in standard Unity), so resolve it via reflection.
s_IsCameraDrawModeSupportedMethod = typeof(SceneView).GetMethod(
"IsCameraDrawModeSupported",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
null, new System.Type[] { cmType }, null);
CodelyLogger.Log($"[NWB-DrawMode] Reflection OK: modes={s_BuiltinCameraModes.Length} " +
$"drawMode={s_CameraModeDrawMode != null} name={s_CameraModeName != null} section={s_CameraModeSection != null} " +
$"isSupported={s_IsCameraDrawModeSupportedMethod != null}");
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-DrawMode] Reflection init error: {ex.Message}");
}
}
///
/// Calls SceneView.IsCameraDrawModeSupported via reflection (internal in
/// the Tuanjie engine). Assumes the mode is supported when the API is unavailable,
/// so modes are shown rather than silently hidden.
///
private static bool IsCameraDrawModeSupportedReflected(SceneView sv, SceneView.CameraMode cameraMode)
{
if (s_IsCameraDrawModeSupportedMethod == null) return true;
try { return (bool)s_IsCameraDrawModeSupportedMethod.Invoke(sv, new object[] { cameraMode }); }
catch { return true; }
}
///
/// Extract draw mode (Shading Mode) popup items and send as popup_show.
///
private static bool TryExtractDrawModePopup(float relX, float relY)
{
EnsureDrawModeReflection();
if (s_BuiltinCameraModes == null || s_CameraModeName == null) return false;
try
{
SceneView sv = s_OffscreenTarget as SceneView;
if (sv == null) sv = SceneView.lastActiveSceneView;
if (sv == null) return false;
var currentMode = sv.cameraMode;
s_FrontendPopupIdCounter++;
s_CurrentFrontendPopupId = "popup_" + s_FrontendPopupIdCounter;
s_PopupSourceType = "draw_mode";
var sb = new StringBuilder(4096);
sb.Append("{\"type\":\"popup_show\",\"id\":\"");
sb.Append(s_CurrentFrontendPopupId);
sb.Append("\",\"x\":");
sb.Append(Mathf.RoundToInt(relX));
sb.Append(",\"y\":");
sb.Append(Mathf.RoundToInt(relY));
sb.Append(",\"items\":[");
string lastSection = null;
int validCount = 0;
var pathList = new List();
for (int i = 0; i < s_BuiltinCameraModes.Length; i++)
{
object cm = s_BuiltinCameraModes[i];
string name = s_CameraModeName.GetValue(cm) as string ?? "";
string section = s_CameraModeSection?.GetValue(cm) as string ?? "";
int drawModeInt = s_CameraModeDrawMode != null ? (int)s_CameraModeDrawMode.GetValue(cm) : -1;
// Construct a public CameraMode struct to call public SceneView methods.
var cameraMode = new SceneView.CameraMode
{
drawMode = (DrawCameraMode)drawModeInt,
name = name,
section = section
};
// Skip modes not supported by the current rendering pipeline.
if (!IsCameraDrawModeSupportedReflected(sv, cameraMode))
continue;
// Check if the mode is enabled (e.g. deferred modes on non-deferred path).
bool modeEnabled = sv.IsCameraDrawModeEnabled(cameraMode);
if (section != lastSection && !string.IsNullOrEmpty(section))
{
if (lastSection != null)
{
if (validCount > 0) sb.Append(',');
sb.Append("{\"separator\":true}");
validCount++;
}
if (validCount > 0) sb.Append(',');
sb.Append("{\"index\":-1,\"label\":\"");
sb.Append(EscapeJsonString(section));
sb.Append("\",\"path\":\"\",\"enabled\":false,\"checked\":false,\"separator\":false}");
validCount++;
lastSection = section;
}
bool isChecked = (DrawCameraMode)drawModeInt == currentMode.drawMode;
string pathKey = $"drawmode|{drawModeInt}|{name}";
pathList.Add(pathKey);
if (validCount > 0) sb.Append(',');
sb.Append("{\"index\":");
sb.Append(pathList.Count - 1);
sb.Append(",\"label\":\"");
sb.Append(EscapeJsonString(name));
sb.Append("\",\"path\":\"");
sb.Append(EscapeJsonString(pathKey));
sb.Append("\",\"enabled\":");
sb.Append(modeEnabled ? "true" : "false");
sb.Append(",\"checked\":");
sb.Append(isChecked ? "true" : "false");
sb.Append(",\"separator\":false}");
validCount++;
}
// Separator before Show Lightmap Resolution.
if (validCount > 0) sb.Append(',');
sb.Append("{\"separator\":true}");
// Show Lightmap Resolution toggle — disabled when current mode < RealtimeCharting(12)
// or the current mode is not enabled, matching Unity's SceneRenderModeWindow logic.
bool showRes = GetLightmapShowResolution();
bool lightmapToggleEnabled = (int)currentMode.drawMode >= 12 &&
sv.IsCameraDrawModeEnabled(currentMode);
string resPath = "drawmode_lightmap_resolution";
pathList.Add(resPath);
sb.Append(",{\"index\":");
sb.Append(pathList.Count - 1);
sb.Append(",\"label\":\"Show Lightmap Resolution\",\"path\":\"");
sb.Append(resPath);
sb.Append("\",\"enabled\":");
sb.Append(lightmapToggleEnabled ? "true" : "false");
sb.Append(",\"checked\":");
sb.Append(showRes ? "true" : "false");
sb.Append(",\"separator\":false}");
sb.Append("]}");
s_CachedPopupItemPaths = pathList.ToArray();
string json = sb.ToString();
bool sent = SendDataChannelMessage(json);
s_FrontendPopupSent = sent;
if (sent)
CodelyLogger.Log($"[NWB-DrawMode] Sent draw mode popup: id={s_CurrentFrontendPopupId} items={pathList.Count}");
return sent;
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-DrawMode] Error extracting draw modes: {ex.Message}\n{ex.StackTrace}");
return false;
}
}
// =================== Panel Protocol Dispatch ===================
///
/// Handle a panel_change message from the browser. Dispatches to
/// the appropriate handler based on the panel's sourceType.
///
private static void HandlePanelChange(string json)
{
try
{
string panelId = ExtractJsonString(json, "id");
string controlId = ExtractJsonString(json, "controlId");
CodelyLogger.Log($"[NWB-Panel] panel_change: id={panelId} control={controlId} source={s_PopupSourceType}");
if (panelId != s_CurrentFrontendPopupId)
{
CodelyLogger.LogWarning($"[NWB-Panel] Mismatched panel ID: got {panelId}, expected {s_CurrentFrontendPopupId}");
return;
}
if (s_PopupSourceType == "grid_settings")
HandleGridPanelChange(json, controlId);
else if (s_PopupSourceType == "snap_increment")
HandleSnapPanelChange(json, controlId);
else if (s_PopupSourceType == "snap_settings")
HandleSnapSettingsPanelChange(json, controlId);
else if (s_PopupSourceType == "scene_camera")
HandleSceneViewCameraPanelChange(json, controlId);
else if (s_PopupSourceType == "gameview_size_add")
HandleGameViewSizeAddPanelChange(json, controlId);
else
CodelyLogger.LogWarning($"[NWB-Panel] Unknown panel source: {s_PopupSourceType}");
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-Panel] Error handling panel_change: {ex.Message}\n{ex.StackTrace}");
}
}
// =================== GameView Size Add Panel ===================
///
/// Send a panel_show message for the GameView "Add Resolution" form.
/// Uses text_input, dropdown, int2_field, and button_row control types.
///
private static void TryShowGameViewSizeAddPanel()
{
try
{
s_FrontendPopupIdCounter++;
s_CurrentFrontendPopupId = "panel_" + s_FrontendPopupIdCounter;
s_PopupSourceType = "gameview_size_add";
var sb = new StringBuilder(1024);
sb.Append("{\"type\":\"panel_show\",\"id\":\"");
sb.Append(s_CurrentFrontendPopupId);
sb.Append("\",\"title\":\"Add\",\"controls\":[");
// Label text input.
sb.Append("{\"type\":\"text_input\",\"id\":\"label\",\"label\":\"Label\",\"value\":\"\"},");
// Type dropdown: 0=Aspect Ratio, 1=Fixed Resolution (default).
sb.Append("{\"type\":\"dropdown\",\"id\":\"sizeType\",\"label\":\"Type\",");
sb.Append("\"options\":[\"Aspect Ratio\",\"Fixed Resolution\"],\"value\":1},");
// Width & Height integer inputs.
sb.Append("{\"type\":\"int2_field\",\"id\":\"widthHeight\",\"label\":\"Width & Height\",");
sb.Append("\"labels\":[\"X\",\"Y\"],\"value\":[10,10]},");
// Cancel / OK buttons.
sb.Append("{\"type\":\"button_row\",\"id\":\"actions\",\"label\":\"\",");
sb.Append("\"buttons\":[{\"id\":\"cancel\",\"label\":\"Cancel\"},{\"id\":\"ok\",\"label\":\"OK\"}]}");
sb.Append("]}");
string json = sb.ToString();
bool sent = SendDataChannelMessage(json);
s_FrontendPopupSent = sent;
if (sent)
CodelyLogger.Log($"[NWB-Panel] Sent GameViewSize add panel: id={s_CurrentFrontendPopupId}");
else
CodelyLogger.LogWarning("[NWB-Panel] Failed to send GameViewSize add panel");
}
catch (Exception ex)
{
CodelyLogger.LogWarning($"[NWB-Panel] Error showing add panel: {ex.Message}");
}
}
///
/// Handle panel_change messages from the GameView size add panel.
/// Uses the cached IFlexibleMenuItemProvider.Create()/Add() pattern
/// which is the same path FlexibleMenu uses internally.
///
private static void HandleGameViewSizeAddPanelChange(string json, string controlId)
{
try
{
// Only act on the OK/Cancel button clicks.
if (controlId != "actions") return;
string buttonId = ExtractJsonString(json, "buttonId");
if (buttonId == "cancel")
{
CodelyLogger.Log("[NWB-Panel] GameViewSize add panel cancelled");
SendDataChannelMessage("{\"type\":\"panel_close\"}");
ClearFrontendPopupState();
return;
}
if (buttonId != "ok") return;
// Extract form fields from the combined JSON payload.
string label = ExtractJsonString(json, "formLabel");
int sizeType = ExtractJsonInt(json, "formSizeType");
int width = ExtractJsonInt(json, "formWidth");
int height = ExtractJsonInt(json, "formHeight");
CodelyLogger.Log($"[NWB-Panel] GameViewSize add: label={label} type={sizeType} w={width} h={height}");
if (width <= 0 || height <= 0)
{
CodelyLogger.LogWarning("[NWB-Panel] Invalid size: width and height must be > 0");
return;
}
// Use the cached IFlexibleMenuItemProvider from the popup session.
// Create() returns a default GameViewSize; we then set properties and Add().
object provider = s_CachedFlexibleMenuItemProvider;
if (provider == null)
{
CodelyLogger.LogWarning("[NWB-Panel] No cached FlexibleMenuItemProvider for add");
return;
}
System.Type provType = provider.GetType();
MethodInfo createMethod = provType.GetMethod("Create",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (createMethod == null)
{
CodelyLogger.LogWarning("[NWB-Panel] Create() method not found on provider");
return;
}
object newSize = createMethod.Invoke(provider, null);
if (newSize == null)
{
CodelyLogger.LogWarning("[NWB-Panel] Create() returned null");
return;
}
// Set properties: sizeType, width, height, baseText.
System.Type sizeObjType = newSize.GetType();
PropertyInfo stProp = sizeObjType.GetProperty("sizeType",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
PropertyInfo wProp = sizeObjType.GetProperty("width",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
PropertyInfo hProp = sizeObjType.GetProperty("height",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
PropertyInfo btProp = sizeObjType.GetProperty("baseText",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (stProp != null)
{
object enumVal = System.Enum.ToObject(stProp.PropertyType, sizeType);
stProp.SetValue(newSize, enumVal);
}
wProp?.SetValue(newSize, width);
hProp?.SetValue(newSize, height);
btProp?.SetValue(newSize, label ?? "");
MethodInfo addMethod = provType.GetMethod("Add",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (addMethod == null)
{
CodelyLogger.LogWarning("[NWB-Panel] Add() method not found on provider");
return;
}
int newIndex = (int)addMethod.Invoke(provider, new object[] { newSize });
CodelyLogger.Log($"[NWB-Panel] Added GameViewSize at index {newIndex}: {label} ({width}x{height}) type={sizeType}");
SendDataChannelMessage("{\"type\":\"panel_close\"}");
ClearFrontendPopupState();
if (s_OffscreenTarget != null) s_OffscreenTarget.Repaint();
}
catch (Exception ex)
{
string inner = ex.InnerException != null ? ex.InnerException.Message : "";
CodelyLogger.LogWarning($"[NWB-Panel] Error adding GameViewSize: {ex.Message} inner={inner}\n{ex.StackTrace}");
}
}
private static void HandlePanelClose(string json)
{
CodelyLogger.Log("[NWB-Panel] panel_close received");
ClearFrontendPopupState();
}
}
}