Add ad hoc syncvar editor publishing

This commit is contained in:
Meta Vens 2026-06-15 02:37:03 +01:00
commit bb192a28c0
8 changed files with 288 additions and 4 deletions

View file

@ -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

View file

@ -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.

View file

@ -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,
)

View file

@ -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,

View file

@ -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:

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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