diff --git a/docs/architecture.md b/docs/architecture.md index 9817167..9b102a9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -103,6 +103,8 @@ Derived State: - Blender JSON debug exports are inspection/import probes, not mirror authority. - Unity and Blender sync UI fields are command/edit affordances; the `brokkr.sync.*` documents are the shared sync policy surface. +- Ad hoc syncvar editors write `brokkr.sync.var.v0` policy records only; they + do not make an unsupported kind executable inside the daemon. - Editor selection and scene summaries are observations until command receipts confirm an accepted mutation. - Daemon stdout reports are telemetry, not durable sync state. The durable state diff --git a/docs/provider-contract.md b/docs/provider-contract.md index a7e71ff..af33808 100644 --- a/docs/provider-contract.md +++ b/docs/provider-contract.md @@ -62,6 +62,10 @@ Primary sync documents: - `brokkr.sync.var.v0`: per-lane sync options such as transform, material, component property, custom property, timeline frame, camera lens, or Cinemachine virtual camera. + Editor surfaces may also publish ad hoc sync vars with an explicit kind, + paths, authority, enabled flag, and interpolation. The daemon executes + implemented kinds and preserves unsupported kinds as typed sync policy for + external consumers. - `brokkr.sync.timeline_binding.v0`: Blender scene/action timeline to Unity Timeline/Cinemachine correspondence. - `brokkr.sync.receipt.v0`: daemon sync pass receipts. diff --git a/surfaces/blender/brokkr_bridge/__init__.py b/surfaces/blender/brokkr_bridge/__init__.py index 9ec20c4..bfe37c4 100644 --- a/surfaces/blender/brokkr_bridge/__init__.py +++ b/surfaces/blender/brokkr_bridge/__init__.py @@ -190,6 +190,54 @@ class BrokkrPreferences(bpy.types.AddonPreferences): description="Synchronize Blender camera animation into Unity Cinemachine lanes", ) + ad_hoc_sync_var_binding_id: bpy.props.StringProperty( + name="Sync Var Binding", + default="", + description="Optional binding id for an ad hoc Brokkr sync var", + ) + + ad_hoc_sync_var_kind: bpy.props.StringProperty( + name="Sync Var Kind", + default="custom-property", + description="Brokkr sync var kind", + ) + + ad_hoc_sync_var_display_name: bpy.props.StringProperty( + name="Sync Var Name", + default="Custom Property", + description="Human-readable sync var label", + ) + + ad_hoc_sync_var_unity_path: bpy.props.StringProperty( + name="Unity Path", + default="", + description="Unity property path for the ad hoc sync var", + ) + + ad_hoc_sync_var_blender_path: bpy.props.StringProperty( + name="Blender Path", + default="", + description="Blender property path for the ad hoc sync var", + ) + + ad_hoc_sync_var_authority: bpy.props.StringProperty( + name="Authority", + default="blender-to-unity", + description="Sync authority, for example blender-to-unity or unity-to-blender", + ) + + ad_hoc_sync_var_interpolation: bpy.props.StringProperty( + name="Interpolation", + default="step", + description="Sync interpolation mode", + ) + + ad_hoc_sync_var_enabled: bpy.props.BoolProperty( + name="Sync Var Enabled", + default=True, + description="Whether the ad hoc sync var is enabled", + ) + def draw(self, context): layout = self.layout layout.prop(self, "broker_uri") @@ -217,6 +265,14 @@ class BrokkrPreferences(bpy.types.AddonPreferences): layout.prop(self, "blender_action_name") layout.prop(self, "sync_timeline_frame") layout.prop(self, "sync_cinemachine_camera") + layout.prop(self, "ad_hoc_sync_var_binding_id") + layout.prop(self, "ad_hoc_sync_var_kind") + layout.prop(self, "ad_hoc_sync_var_display_name") + layout.prop(self, "ad_hoc_sync_var_unity_path") + layout.prop(self, "ad_hoc_sync_var_blender_path") + layout.prop(self, "ad_hoc_sync_var_authority") + layout.prop(self, "ad_hoc_sync_var_interpolation") + layout.prop(self, "ad_hoc_sync_var_enabled") class BROKKR_PT_status(bpy.types.Panel): @@ -251,6 +307,18 @@ class BROKKR_PT_status(bpy.types.Panel): sync_row.operator("brokkr.publish_timeline_sync", icon="TIME") sync_row.operator("brokkr.refresh_sync_receipt", icon="FILE_REFRESH") + var_box = layout.box() + var_box.label(text="Ad Hoc Sync Var") + var_box.prop(prefs, "ad_hoc_sync_var_binding_id") + var_box.prop(prefs, "ad_hoc_sync_var_kind") + var_box.prop(prefs, "ad_hoc_sync_var_display_name") + var_box.prop(prefs, "ad_hoc_sync_var_unity_path") + var_box.prop(prefs, "ad_hoc_sync_var_blender_path") + var_box.prop(prefs, "ad_hoc_sync_var_authority") + var_box.prop(prefs, "ad_hoc_sync_var_interpolation") + var_box.prop(prefs, "ad_hoc_sync_var_enabled") + var_box.operator("brokkr.publish_sync_var", icon="DRIVER") + if adapter.last_snapshot: layout.separator() layout.label(text=f"Objects: {adapter.last_snapshot.get('objectCount', 0)}") @@ -401,6 +469,31 @@ class BROKKR_OT_refresh_sync_receipt(bpy.types.Operator): return {"FINISHED"} +class BROKKR_OT_publish_sync_var(bpy.types.Operator): + bl_idname = "brokkr.publish_sync_var" + bl_label = "Publish Sync Var" + bl_description = "Publish an ad hoc Brokkr sync var through the Blender mirror" + + def execute(self, context): + prefs = context.preferences.addons[__name__].preferences + sync_var = target().publish_sync_var( + prefs.cultmesh_cache_path, + prefs.cultlib_py_src, + prefs.sync_session_id, + prefs.sync_display_name, + prefs.ad_hoc_sync_var_display_name, + prefs.ad_hoc_sync_var_binding_id, + prefs.ad_hoc_sync_var_kind, + prefs.ad_hoc_sync_var_unity_path, + prefs.ad_hoc_sync_var_blender_path, + prefs.ad_hoc_sync_var_authority, + prefs.ad_hoc_sync_var_enabled, + prefs.ad_hoc_sync_var_interpolation, + ) + self.report({"INFO"}, f"Brokkr sync var: {sync_var['displayName']}") + return {"FINISHED"} + + def _auto_capture(scene, depsgraph): context = bpy.context prefs = context.preferences.addons.get(__name__) @@ -424,6 +517,7 @@ classes = ( BROKKR_OT_publish_object_sync, BROKKR_OT_publish_timeline_sync, BROKKR_OT_refresh_sync_receipt, + BROKKR_OT_publish_sync_var, ) diff --git a/surfaces/blender/brokkr_bridge/blender_target.py b/surfaces/blender/brokkr_bridge/blender_target.py index e5778ac..c28928c 100644 --- a/surfaces/blender/brokkr_bridge/blender_target.py +++ b/surfaces/blender/brokkr_bridge/blender_target.py @@ -232,6 +232,52 @@ class BrokkrBlenderTarget: self._put_sync_var(node, documents, normalized_session_id, binding_id, "cinemachine-virtual-camera", "Cinemachine Camera", "CinemachineVirtualCamera", "camera", "blender-to-unity", sync_camera, "linear", now) return binding + def publish_sync_var( + self, + cache_path: str, + cultlib_py_src: str, + session_id: str, + session_display_name: str, + sync_var_display_name: str, + binding_id: str, + kind: str, + unity_property_path: str, + blender_property_path: str, + authority: str, + enabled: bool, + interpolation: str, + ) -> dict[str, Any]: + now = _now() + node, documents = self._open_node(cache_path, cultlib_py_src) + normalized_session_id = session_id or "default" + normalized_kind = kind or "custom-property" + normalized_binding_id = binding_id or _stable_id( + "adhoc-binding", + normalized_session_id, + normalized_kind, + unity_property_path, + blender_property_path, + ) + session = _sync_session(normalized_session_id, session_display_name, now) + sync_var_id = _stable_id("var", normalized_session_id, normalized_binding_id, normalized_kind) + sync_var = { + "schema": SYNC_VAR_SCHEMA, + "syncVarId": sync_var_id, + "sessionId": normalized_session_id, + "bindingId": normalized_binding_id, + "displayName": sync_var_display_name or normalized_kind, + "kind": normalized_kind, + "unityPropertyPath": unity_property_path, + "blenderPropertyPath": blender_property_path, + "authority": authority or "blender-to-unity", + "enabled": bool(enabled), + "interpolation": interpolation or "step", + "updatedAt": now, + } + node.database.put(documents["sync_session"], f"sync/sessions/{normalized_session_id}", session) + node.database.put(documents["sync_var"], f"sync/vars/{sync_var_id}", sync_var) + return sync_var + def start_server( self, cache_path: str, diff --git a/surfaces/blender/docs/install.md b/surfaces/blender/docs/install.md index 4f84c28..9abc889 100644 --- a/surfaces/blender/docs/install.md +++ b/surfaces/blender/docs/install.md @@ -51,6 +51,8 @@ owns the mirror state. 12. After running `brokkr-daemon sync-once` or `sync-loop`, click `Refresh Sync Receipt` to show the latest daemon sync pass observed in the Blender mirror. +13. To publish an ad hoc sync variable, fill the `Ad Hoc Sync Var` fields in + the sidebar or add-on preferences and click `Publish Sync Var`. Admitted command actions: diff --git a/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrSettings.cs b/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrSettings.cs index 2513751..8676897 100644 --- a/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrSettings.cs +++ b/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrSettings.cs @@ -151,5 +151,53 @@ namespace GameCult.Brokkr.Editor get => EditorPrefs.GetString("GameCult.Brokkr.PrefabVariantPath", "Assets/BrokkrPrefabVariant.prefab"); set => EditorPrefs.SetString("GameCult.Brokkr.PrefabVariantPath", value); } + + internal static string AdHocSyncVarBindingId + { + get => EditorPrefs.GetString("GameCult.Brokkr.AdHocSyncVarBindingId", ""); + set => EditorPrefs.SetString("GameCult.Brokkr.AdHocSyncVarBindingId", value); + } + + internal static string AdHocSyncVarKind + { + get => EditorPrefs.GetString("GameCult.Brokkr.AdHocSyncVarKind", "custom-property"); + set => EditorPrefs.SetString("GameCult.Brokkr.AdHocSyncVarKind", value); + } + + internal static string AdHocSyncVarDisplayName + { + get => EditorPrefs.GetString("GameCult.Brokkr.AdHocSyncVarDisplayName", "Custom Property"); + set => EditorPrefs.SetString("GameCult.Brokkr.AdHocSyncVarDisplayName", value); + } + + internal static string AdHocSyncVarUnityPath + { + get => EditorPrefs.GetString("GameCult.Brokkr.AdHocSyncVarUnityPath", ""); + set => EditorPrefs.SetString("GameCult.Brokkr.AdHocSyncVarUnityPath", value); + } + + internal static string AdHocSyncVarBlenderPath + { + get => EditorPrefs.GetString("GameCult.Brokkr.AdHocSyncVarBlenderPath", ""); + set => EditorPrefs.SetString("GameCult.Brokkr.AdHocSyncVarBlenderPath", value); + } + + internal static string AdHocSyncVarAuthority + { + get => EditorPrefs.GetString("GameCult.Brokkr.AdHocSyncVarAuthority", "blender-to-unity"); + set => EditorPrefs.SetString("GameCult.Brokkr.AdHocSyncVarAuthority", value); + } + + internal static string AdHocSyncVarInterpolation + { + get => EditorPrefs.GetString("GameCult.Brokkr.AdHocSyncVarInterpolation", "step"); + set => EditorPrefs.SetString("GameCult.Brokkr.AdHocSyncVarInterpolation", value); + } + + internal static bool AdHocSyncVarEnabled + { + get => EditorPrefs.GetBool("GameCult.Brokkr.AdHocSyncVarEnabled", true); + set => EditorPrefs.SetBool("GameCult.Brokkr.AdHocSyncVarEnabled", value); + } } } diff --git a/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrWindow.cs b/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrWindow.cs index 389caba..ea50555 100644 --- a/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrWindow.cs +++ b/surfaces/unity/Packages/com.gamecult.brokkr/Editor/BrokkrWindow.cs @@ -46,6 +46,14 @@ namespace GameCult.Brokkr.Editor private string prefabAssetPath = ""; private string prefabInstanceName = ""; private string prefabVariantPath = "Assets/BrokkrPrefabVariant.prefab"; + private string adHocSyncVarBindingId = ""; + private string adHocSyncVarKind = "custom-property"; + private string adHocSyncVarDisplayName = "Custom Property"; + private string adHocSyncVarUnityPath = ""; + private string adHocSyncVarBlenderPath = ""; + private string adHocSyncVarAuthority = "blender-to-unity"; + private string adHocSyncVarInterpolation = "step"; + private bool adHocSyncVarEnabled = true; [MenuItem("GameCult/Brokkr")] public static void Open() @@ -79,6 +87,14 @@ namespace GameCult.Brokkr.Editor prefabAssetPath = BrokkrSettings.PrefabAssetPath; prefabInstanceName = BrokkrSettings.PrefabInstanceName; prefabVariantPath = BrokkrSettings.PrefabVariantPath; + adHocSyncVarBindingId = BrokkrSettings.AdHocSyncVarBindingId; + adHocSyncVarKind = BrokkrSettings.AdHocSyncVarKind; + adHocSyncVarDisplayName = BrokkrSettings.AdHocSyncVarDisplayName; + adHocSyncVarUnityPath = BrokkrSettings.AdHocSyncVarUnityPath; + adHocSyncVarBlenderPath = BrokkrSettings.AdHocSyncVarBlenderPath; + adHocSyncVarAuthority = BrokkrSettings.AdHocSyncVarAuthority; + adHocSyncVarInterpolation = BrokkrSettings.AdHocSyncVarInterpolation; + adHocSyncVarEnabled = BrokkrSettings.AdHocSyncVarEnabled; Selection.selectionChanged += Repaint; EditorSceneManagerBridge.SceneDirtied += OnEditorSignal; EditorApplication.playModeStateChanged += OnPlayModeChanged; @@ -130,6 +146,14 @@ namespace GameCult.Brokkr.Editor BrokkrSettings.PrefabAssetPath = prefabAssetPath; BrokkrSettings.PrefabInstanceName = prefabInstanceName; BrokkrSettings.PrefabVariantPath = prefabVariantPath; + BrokkrSettings.AdHocSyncVarBindingId = adHocSyncVarBindingId; + BrokkrSettings.AdHocSyncVarKind = adHocSyncVarKind; + BrokkrSettings.AdHocSyncVarDisplayName = adHocSyncVarDisplayName; + BrokkrSettings.AdHocSyncVarUnityPath = adHocSyncVarUnityPath; + BrokkrSettings.AdHocSyncVarBlenderPath = adHocSyncVarBlenderPath; + BrokkrSettings.AdHocSyncVarAuthority = adHocSyncVarAuthority; + BrokkrSettings.AdHocSyncVarInterpolation = adHocSyncVarInterpolation; + BrokkrSettings.AdHocSyncVarEnabled = adHocSyncVarEnabled; SetStatus("Settings saved.", MessageType.Info); } @@ -291,6 +315,22 @@ namespace GameCult.Brokkr.Editor unityCinemachineObjectId = selectedId; } } + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Ad Hoc Sync Var", EditorStyles.boldLabel); + adHocSyncVarBindingId = EditorGUILayout.TextField("Binding Id", adHocSyncVarBindingId); + adHocSyncVarKind = EditorGUILayout.TextField("Kind", adHocSyncVarKind); + adHocSyncVarDisplayName = EditorGUILayout.TextField("Display Name", adHocSyncVarDisplayName); + adHocSyncVarUnityPath = EditorGUILayout.TextField("Unity Path", adHocSyncVarUnityPath); + adHocSyncVarBlenderPath = EditorGUILayout.TextField("Blender Path", adHocSyncVarBlenderPath); + adHocSyncVarAuthority = EditorGUILayout.TextField("Authority", adHocSyncVarAuthority); + adHocSyncVarInterpolation = EditorGUILayout.TextField("Interpolation", adHocSyncVarInterpolation); + adHocSyncVarEnabled = EditorGUILayout.Toggle("Enabled", adHocSyncVarEnabled); + + if (GUILayout.Button("Publish Sync Var")) + { + PublishAdHocSyncVar(); + } } private void CaptureSnapshot() @@ -580,6 +620,36 @@ namespace GameCult.Brokkr.Editor } } + private void PublishAdHocSyncVar() + { + try + { + RequireMirror(); + var now = System.DateTimeOffset.UtcNow.ToString("O"); + var bindingId = string.IsNullOrWhiteSpace(adHocSyncVarBindingId) + ? StableId("adhoc-binding", NormalizedSyncSessionId(), adHocSyncVarKind, adHocSyncVarUnityPath, adHocSyncVarBlenderPath) + : adHocSyncVarBindingId.Trim(); + var session = BuildSyncSession(now); + + mirror.PublishSyncSessionAsync(session).GetAwaiter().GetResult(); + PublishSyncVar( + bindingId, + string.IsNullOrWhiteSpace(adHocSyncVarKind) ? "custom-property" : adHocSyncVarKind.Trim(), + string.IsNullOrWhiteSpace(adHocSyncVarDisplayName) ? "Custom Property" : adHocSyncVarDisplayName.Trim(), + adHocSyncVarUnityPath.Trim(), + adHocSyncVarBlenderPath.Trim(), + adHocSyncVarEnabled, + string.IsNullOrWhiteSpace(adHocSyncVarInterpolation) ? "step" : adHocSyncVarInterpolation.Trim(), + now, + string.IsNullOrWhiteSpace(adHocSyncVarAuthority) ? "blender-to-unity" : adHocSyncVarAuthority.Trim()); + SetStatus($"Published Brokkr sync var: {adHocSyncVarDisplayName}", MessageType.Info); + } + catch (System.Exception error) + { + SetStatus(error.Message, MessageType.Error); + } + } + private BrokkrSyncSession BuildSyncSession(string observedAt) { return new BrokkrSyncSession @@ -601,8 +671,12 @@ namespace GameCult.Brokkr.Editor string blenderPropertyPath, bool enabled, string interpolation, - string updatedAt) + string updatedAt, + string authority = "") { + var resolvedAuthority = string.IsNullOrWhiteSpace(authority) + ? DefaultAuthorityForKind(kind) + : authority; var syncVar = new BrokkrSyncVar { syncVarId = StableId("var", NormalizedSyncSessionId(), bindingId, kind), @@ -612,9 +686,7 @@ namespace GameCult.Brokkr.Editor kind = kind, unityPropertyPath = unityPropertyPath, blenderPropertyPath = blenderPropertyPath, - authority = kind.StartsWith("timeline", StringComparison.Ordinal) || kind == "custom-property" - ? "blender-to-unity" - : "unity-to-blender", + authority = resolvedAuthority, enabled = enabled, interpolation = interpolation, updatedAt = updatedAt @@ -622,6 +694,13 @@ namespace GameCult.Brokkr.Editor mirror.PublishSyncVarAsync(syncVar).GetAwaiter().GetResult(); } + private static string DefaultAuthorityForKind(string kind) + { + return kind.StartsWith("timeline", StringComparison.Ordinal) || kind == "custom-property" + ? "blender-to-unity" + : "unity-to-blender"; + } + private static string BuildTransformPath(Transform transform) { var path = transform.name; diff --git a/surfaces/unity/README.md b/surfaces/unity/README.md index 6e4c9bb..7615231 100644 --- a/surfaces/unity/README.md +++ b/surfaces/unity/README.md @@ -38,6 +38,9 @@ In Unity: 9. With the mirror running, run `brokkr-daemon sync-once` or `sync-loop`, then click `Poll Sync Receipt` or leave the window open to show the latest daemon sync pass receipt. +10. To publish an ad hoc sync variable, fill `Ad Hoc Sync Var` with a binding id + or leave it blank for a generated id, set kind/paths/authority/interpolation, + then click `Publish Sync Var`. The Unity adapter writes the latest editor snapshot to: @@ -51,6 +54,12 @@ Brokkr daemon sync receipts are read from: sync/receipts/{receiptId} ``` +Ad hoc sync variables are written to: + +```text +sync/vars/{syncVarId} +``` + Verse-side command clients write `brokkr.unity.command_intent.v0` documents to: ```text