2026-04-08 10:28:31 +02:00
import bpy
import json
# --- 1. SCHEMA ---
SCHEMA = {
2026-04-21 15:27:41 +02:00
" title " : " XR Fragments " ,
" type " : " object " ,
" $version " : 2 ,
" flatten " : True ,
" $defs " : {
" xrf " : {
" title " : " XR Fragments " ,
" description " : " XR Fragments (level2) promotes embedding clickable hyperlinks \n in 3D files (via the href attribute). \n Make sure to check ' custom attributes ' in your export dialog. " ,
" url " : " https://xrfragment.org/#How %20i t % 20works " ,
" properties " : {
" _xrf-type " : { " title " : " type " , " type " : " string " , " default " : " xrf " , " description " : " needed for blender UI " } ,
" href " : {
" type " : " object.name " ,
" description " : " teleport to / href "
}
}
} ,
" three " : {
" oneOf " : [
{ " $ref " : " #/$defs/three_material " } ,
{ " $ref " : " #/$defs/three_object " }
2026-04-08 10:28:31 +02:00
]
2026-04-21 15:27:41 +02:00
} ,
" aframe " : {
" oneOf " : [
{ " $ref " : " #/$defs/three_material " } ,
{ " $ref " : " #/$defs/three_object " }
2026-04-08 10:28:31 +02:00
]
2026-04-21 15:27:41 +02:00
} ,
" three_material " : {
" title " : " Material " ,
" description " : " Material is THREE ' s abstract base material class " ,
" url " : " https://threejs.org/docs/?q=material#Material " ,
" properties " : {
" _three-type " : { " title " : " type " , " type " : " string " , " default " : " material " , " description " : " needed for blender UI " } ,
" -three-material.blending " : {
" type " : " string " ,
" enum " : [ " THREE.NormalBlending " , " THREE.NoBlending " , " THREE.AdditiveBlending " , " THREE.SubtractiveBlending " , " THREE.MultiplyBlending " ]
} ,
" -three-material.sides " : {
" type " : " string " ,
" description " : " culling: which sides to render (less increases fps) " ,
" enum " : [ " THREE.FrontSide " , " THREE.BackSide " , " THREE.DoubleSide " ]
} ,
" -three-material.map.offset.x+ " : {
" type " : " number " ,
" description " : " texture scrolling speed " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
} ,
" -three-material.map.offset.yx+ " : {
" type " : " number " ,
" description " : " texture scrolling speed " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
}
}
2026-04-08 10:28:31 +02:00
} ,
2026-04-21 15:27:41 +02:00
" three_object " : {
" title " : " Object3D " ,
" description " : " Object3D is THREE ' s abstract base entity class " ,
" url " : " https://threejs.org/docs/?q=Object3D#Object3D " ,
" properties " : {
" _three-type " : { " title " : " type " , " type " : " string " , " default " : " object3D " , " description " : " needed for blender UI " } ,
" -three-renderOrder " : {
" type " : " integer " ,
" minimum " : 0 ,
" maximum " : 100
}
}
} ,
" janus " : {
" oneOf " : [
{ " $ref " : " #/$defs/janus_image " } ,
{ " $ref " : " #/$defs/janus_video " } ,
{ " $ref " : " #/$defs/janus_link " }
]
} ,
" janus_image " : {
" description " : " The <image> tag allows displaying images via url, relative file, or data-uri ' s " ,
" url " : " https://janusvr.com/docs/build/roomtag/index.html#Link " ,
" title " : " <image> " ,
" properties " : {
" -janus-tag " : { " type " : " string " , " default " : " image " } ,
" -janus-id " : { " type " : " object.name " } ,
" -janus-col " : { " $ref " : " #/$defs/-janus-col " } ,
" -janus-lighting " : { " $ref " : " #/$defs/-janus-lighting " } ,
" -janus-fwd " : { " $ref " : " #/$defs/-janus-fwd " } ,
" -janus-scale " : { " $ref " : " #/$defs/-janus-scale " } ,
" -janus-rotation " : { " $ref " : " #/$defs/-janus-rotation " } ,
" -janus-locked " : { " $ref " : " #/$defs/-janus-locked " } ,
" -janus-draw-layer " : { " $ref " : " #/$defs/-janus-draw_layer " } ,
}
} ,
" janus_video " : {
" title " : " <video> " ,
" description " : " The <video> tag allows displaying videos " ,
" url " : " https://janusvr.com/docs/build/roomtag/index.html#Video " ,
" properties " : {
" -janus-tag " : { " type " : " string " , " default " : " video " } ,
" -janus-id " : { " type " : " object.name " } ,
" -janus-thumb_id " : { " $ref " : " #/$defs/-janus-thumb_id " } ,
" -janus-col " : { " $ref " : " #/$defs/-janus-col " } ,
" -janus-lighting " : { " $ref " : " #/$defs/-janus-lighting " } ,
" -janus-fwd " : { " $ref " : " #/$defs/-janus-fwd " } ,
" -janus-scale " : { " $ref " : " #/$defs/-janus-scale " } ,
" -janus-rotation " : { " $ref " : " #/$defs/-janus-rotation " } ,
" -janus-locked " : { " $ref " : " #/$defs/-janus-locked " } ,
" -janus-draw-layer " : { " $ref " : " #/$defs/-janus-draw_layer " }
" -janus-gain " : { " type " : " float " , " minimum " : 0 , " maximum " : 1 }
}
} ,
" janus_link " : {
" title " : " <link> " ,
" description " : " The <link> tag allows creating portals to other scenes " ,
" url " : " https://janusvr.com/docs/build/roomtag/index.html#Link " ,
" properties " : {
" -janus-tag " : { " type " : " string " , " default " : " link " } ,
" -janus-url " : { " type " : " string " , " default " : " https://... " } ,
" -janus-title " : { " type " : " string " , " default " : " my title " } ,
" -janus-thumb_id " : { " $ref " : " #/$defs/-janus-thumb_id " } ,
" -janus-draw_glow " : { " type " : " boolean " , " default " : True } ,
" -janus-draw_text " : { " type " : " boolean " , " default " : True } ,
" -janus-auto_load " : { " type " : " boolean " , " default " : False } ,
" -janus-mirror " : { " type " : " boolean " , " default " : False } ,
" -janus-col " : { " $ref " : " #/$defs/-janus-col " } ,
" -janus-lighting " : { " $ref " : " #/$defs/-janus-lighting " } ,
" -janus-fwd " : { " $ref " : " #/$defs/-janus-fwd " } ,
" -janus-scale " : { " $ref " : " #/$defs/-janus-scale " } ,
" -janus-rotation " : { " $ref " : " #/$defs/-janus-rotation " } ,
" -janus-locked " : { " $ref " : " #/$defs/-janus-locked " } ,
" -janus-draw-layer " : { " $ref " : " #/$defs/-janus-draw_layer " }
}
} ,
" janus_object " : {
" title " : " <object> " ,
" description " : " The <object> tag allows displaying 3D files " ,
" url " : " https://janusvr.com/docs/build/roomtag/index.html#Object " ,
" properties " : {
" -janus-tag " : { " type " : " string " , " default " : " object " } ,
" -janus-col " : { " $ref " : " #/$defs/-janus-col " } ,
" -janus-fwd " : { " $ref " : " #/$defs/-janus-fwd " } ,
" -janus-scale " : { " $ref " : " #/$defs/-janus-scale " } ,
" -janus-rotation " : { " $ref " : " #/$defs/-janus-rotation " } ,
" -janus-locked " : { " $ref " : " #/$defs/-janus-locked " } ,
" -janus-lighting " : { " $ref " : " #/$defs/-janus-lighting " } ,
" -janus-draw-layer " : { " $ref " : " #/$defs/-janus-draw_layer " }
}
} ,
" -janus-thumb_id " : { " type " : " object.name " } ,
" -janus-lighting " : { " type " : " boolean " , " default " : False } ,
" -janus-col " : { " type " : " color " , " default " : " #FFFFFF " } ,
" -janus-fwd " : { " type " : " string " , " default " : " 0 0 1 " } ,
" -janus-scale " : { " type " : " string " , " default " : " 1 1 1 " } ,
" -janus-rotation " : { " type " : " string " , " default " : " 0 0 0 " } ,
" -janus-locked " : { " type " : " boolean " , " default " : False } ,
" -janus-draw_layer " : { " type " : " integer " , " default " : 0 , " minimum " : 0 , " maximum " : 100 } ,
" godot3 " : {
" oneOf " : [
{ " $ref " : " #/$defs/godot3_material " } ,
{ " $ref " : " #/$defs/godot3_spatial " }
]
} ,
" godot3_material " : {
" title " : " SpatialMaterial " ,
" description " : " The default Pmaterial for Godot 3. " ,
" url " : " https://docs.godotengine.org/en/3.0/tutorials/3d/spatial_material.html " ,
" properties " : {
" _godot3-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " SpatialMaterial "
} ,
" -godot3-params_blend_mode " : {
" type " : " string " ,
" description " : " mat.params_blend_mode = SpatialMaterial.BLEND_MODE_MIX " ,
" enum " : [
" SpatialMaterial.BLEND_MODE_MIX " ,
" SpatialMaterial.BLEND_MODE_ADD " ,
" SpatialMaterial.BLEND_MODE_SUB " ,
" SpatialMaterial.BLEND_MODE_MUL "
]
} ,
" -godot3-params_cull_mode " : {
" type " : " string " ,
" description " : " mat.params_cull_mode = SpatialMaterial.CULL_BACK " ,
" enum " : [
" SpatialMaterial.CULL_BACK " ,
" SpatialMaterial.CULL_FRONT " ,
" SpatialMaterial.CULL_DISABLED "
]
} ,
" -godot3-uv1_offset.x+ " : {
" type " : " number " ,
" description " : " mat.uv1_offset.x += speed * delta " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
} ,
" -godot3-uv1_offset.y+ " : {
" type " : " number " ,
" description " : " mat.uv1_offset.y += speed * delta " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
}
}
} ,
" godot3_spatial " : {
" title " : " Object " ,
" properties " : {
" _godot3-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " Object "
} ,
" -godot3-render_priority " : {
" type " : " integer " ,
" description " : " mat.render_priority = value " ,
" minimum " : - 128 ,
" maximum " : 127
}
}
} ,
" godot4 " : {
" oneOf " : [ { " $ref " : " #/$defs/godot4_material " } , { " $ref " : " #/$defs/godot4_node3d " } ]
} ,
" godot4_material " : {
" title " : " StandardMaterial3D " ,
" description " : " The default PBR material for Godot 4. " ,
" url " : " https://docs.godotengine.org/en/stable/classes/class_standardmaterial3d.html " ,
" properties " : {
" _godot4-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " StandardMaterial3D "
} ,
" -gotod4-blend_mode " : {
" type " : " string " ,
" description " : " mat.blend_mode = BaseMaterial3D.BLEND_MODE_MIX " ,
" enum " : [
" BaseMaterial3D.BLEND_MODE_MIX " ,
" BaseMaterial3D.BLEND_MODE_ADD " ,
" BaseMaterial3D.BLEND_MODE_SUB " ,
" BaseMaterial3D.BLEND_MODE_MUL "
]
} ,
" -gotod4-cull_mode " : {
" type " : " string " ,
" description " : " mat.cull_mode = BaseMaterial3D.CULL_BACK " ,
" enum " : [
" BaseMaterial3D.CULL_BACK " ,
" BaseMaterial3D.CULL_FRONT " ,
" BaseMaterial3D.CULL_DISABLED "
]
} ,
" -gotod4-uv1_offset.x+ " : {
" type " : " number " ,
" description " : " mat.uv1_offset.x += speed * delta " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
} ,
" -gotod4-uv1_offset.y+ " : {
" type " : " number " ,
" description " : " mat.uv1_offset.y += speed * delta " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
}
}
} ,
" godot4_node3d " : {
" title " : " Node3D " ,
" properties " : {
" _godot4-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " Node3D "
} ,
" -godot4-render_priority " : {
" type " : " integer " ,
" description " : " mat.render_priority = value " ,
" minimum " : - 128 ,
" maximum " : 127
}
}
} ,
" playcanvas " : {
" oneOf " : [
{
" $ref " : " #/$defs/playcanvas_material "
} ,
{
" $ref " : " #/$defs/playcanvas_entity "
}
]
} ,
" playcanvas_material " : {
" title " : " StandardMaterial " ,
" properties " : {
" _playcanvas-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " material " ,
" description " : " var mat = new pc.StandardMaterial() "
} ,
" -playcanvas-material.blendType " : {
" type " : " string " ,
" description " : " mat.blendType = pc.BLEND_NORMAL " ,
" enum " : [
" pc.BLEND_NORMAL " ,
" pc.BLEND_NONE " ,
" pc.BLEND_ADDITIVE " ,
" pc.BLEND_SUBTRACTIVE " ,
" pc.BLEND_MULTIPLICATIVE "
]
} ,
" -playcanvas-material.cull " : {
" type " : " string " ,
" description " : " mat.cull = pc.CULL_BACK " ,
" enum " : [
" pc.CULL_BACK " ,
" pc.CULL_FRONT " ,
" pc.CULL_NONE "
]
} ,
" -playcanvas-material.diffuseMapOffset.x " : {
" type " : " number " ,
" description " : " mat.diffuseMapOffset.x += speed * dt " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
} ,
" -playcanvas-material.diffuseMapOffset.y " : {
" type " : " number " ,
" description " : " mat.diffuseMapOffset.y += speed * dt " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
}
}
} ,
" playcanvas_entity " : {
" title " : " Entity " ,
" properties " : {
" _playcanvas-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " entity "
} ,
" -playcanvas-renderOrder " : {
" type " : " integer " ,
" description " : " entity.render.layers = [layerID] // Custom sorting via layers or drawOrder " ,
" minimum " : 0 ,
" maximum " : 100
}
}
} ,
" babylon " : {
" oneOf " : [
{
" $ref " : " #/$defs/babylon_material "
} ,
{
" $ref " : " #/$defs/babylon_node "
}
]
} ,
" babylon_material " : {
" title " : " StandardMaterial " ,
" properties " : {
" _babylon-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " StandardMaterial "
} ,
" -babylon-material.alphaMode " : {
" type " : " string " ,
" description " : " mat.alphaMode = BABYLON.Engine.ALPHA_COMBINE " ,
" enum " : [
" BABYLON.Engine.ALPHA_COMBINE " ,
" BABYLON.Engine.ALPHA_DISABLE " ,
" BABYLON.Engine.ALPHA_ADD " ,
" BABYLON.Engine.ALPHA_SUBTRACT " ,
" BABYLON.Engine.ALPHA_MULTIPLY "
]
} ,
" -babylon-material.sideOrientation " : {
" type " : " string " ,
" description " : " mat.backFaceCulling = true // or mat.sideOrientation " ,
" enum " : [
" BABYLON.Material.ClockWiseSideOrientation " ,
" BABYLON.Material.CounterClockWiseSideOrientation " ,
" BABYLON.Material.TwoSidedLighting "
]
} ,
" -babylon-material.diffuseTexture.uOffset " : {
" type " : " number " ,
" description " : " mat.diffuseTexture.uOffset += speed * scene.getAnimationRatio() " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
} ,
" -babylon-material.diffuseTexture.vOffset " : {
" type " : " number " ,
" description " : " mat.diffuseTexture.vOffset += speed * scene.getAnimationRatio() " ,
" default " : 0.005 ,
" minimum " : 0.000001 ,
" maximum " : 1
}
}
} ,
" babylon_node " : {
" title " : " AbstractMesh " ,
" properties " : {
" _babylon-type " : {
" title " : " type " ,
" type " : " string " ,
" default " : " mesh " ,
" description " : " var mesh = new BABYLON.Mesh( ' mesh ' , scene) "
} ,
" -babylon-renderingGroupId " : {
" type " : " integer " ,
" description " : " mesh.renderingGroupId = value // 0 to 3 by default " ,
" minimum " : 0 ,
" maximum " : 100
}
}
}
2026-04-08 10:28:31 +02:00
} ,
2026-04-21 15:27:41 +02:00
" properties " : {
" engine " : {
" type " : " enum " ,
" enum " : [
( " NONE " , " -- Select -- " , " " ) ,
( " XRF " , " generic: hyperlinking " , " " ) ,
( " JANUSXR " , " JanusXR " , " " ) ,
( " THREEJS " , " Three.js " , " " ) ,
( " GODOT3 " , " Godot 3 " , " " ) ,
( " GODOT4 " , " Godot 4 " , " " ) ,
( " AFRAME " , " AFRAME " , " " ) ,
( " BABYLON " , " Babylon.js " , " " ) ,
( " PLAYCANVAS " , " PlayCanvas " , " " )
] ,
" default " : " NONE "
} ,
" engine_settings " : {
" oneOf " : [
{ " engine_val " : " XRF " , " $ref " : " #/$defs/xrf " , " title " : " XR Fragment " } ,
{ " engine_val " : " JANUSXR " , " $ref " : " #/$defs/janus " , " title " : " JanusXR " } ,
{ " engine_val " : " THREEJS " , " $ref " : " #/$defs/three " , " title " : " THREE.js " } ,
{ " engine_val " : " GODOT3 " , " $ref " : " #/$defs/godot3 " , " title " : " Godot3 (beta) " } ,
{ " engine_val " : " GODOT4 " , " $ref " : " #/$defs/godot4 " , " title " : " Godot4 (beta) " } ,
{ " engine_val " : " AFRAME " , " $ref " : " #/$defs/aframe " , " title " : " AFRAME (beta) " } ,
{ " engine_val " : " BABYLON " , " $ref " : " #/$defs/babylon " , " title " : " Babylon.js (beta) " } ,
{ " engine_val " : " PLAYCANVAS " , " $ref " : " #/$defs/playcanvas " , " title " : " PlayCanvas (beta) " } ,
]
}
}
2026-04-08 10:28:31 +02:00
}
# --- 2. HELPERS ---
def resolve_schema_node ( node , schema_root ) :
if " $ref " in node :
ref_path = node [ " $ref " ] . split ( ' / ' )
resolved = schema_root
for part in ref_path [ 1 : ] :
resolved = resolved . get ( part , { } )
return resolved
return node
def get_scene_object_items ( self , context ) :
2026-04-21 15:27:41 +02:00
items = [ ( " NONE " , " -- select target / asset -- " , " " ) ]
if context is None :
return items
obj_list = [ ]
2026-04-08 10:28:31 +02:00
for obj in context . scene . objects :
2026-04-21 15:27:41 +02:00
obj_list . append ( ( obj . name , obj . name , f " Object: { obj . name } " ) )
# Sort objects alphabetically by name
obj_list . sort ( key = lambda x : x [ 1 ] . lower ( ) )
items . extend ( obj_list )
2026-04-08 10:28:31 +02:00
return items
def tell_if_const ( p_name , p_info ) :
2026-04-21 15:27:41 +02:00
return ( p_info . get ( " type " ) == " string " and " default " in p_info and len ( p_info ) == 2 )
2026-04-08 10:28:31 +02:00
def rgb_to_hex ( rgb ) :
""" Converts linear RGB floats to sRGB Hex string. """
# Gamma correction: Linear to sRGB
srgb = [ pow ( c , 1 / 2.2 ) if c > 0 else 0 for c in rgb ]
return " # %02x %02x %02x " % tuple ( int ( max ( 0 , min ( 255 , c * 255 ) ) ) for c in srgb )
# --- 3. BAKE OPERATOR ---
class OBJECT_OT_bake_schema ( bpy . types . Operator ) :
bl_idname = " object.bake_schema "
bl_label = " Apply "
def execute ( self , context ) :
obj = context . object
active_engine = obj . engine
if active_engine == " NONE " :
self . report ( { ' WARNING ' } , " No engine selected. " )
return { ' CANCELLED ' }
engine_opt = next ( ( opt for opt in SCHEMA [ " properties " ] [ " engine_settings " ] [ " oneOf " ]
if opt [ " engine_val " ] == active_engine ) , None )
if not engine_opt :
return { ' FINISHED ' }
baked_data = { }
root_node = resolve_schema_node ( engine_opt , SCHEMA )
self . collect_visible_props ( obj , root_node , baked_data )
if SCHEMA . get ( " flatten " , True ) :
for k , v in baked_data . items ( ) :
obj [ k ] = v
else :
obj [ " JSON " ] = json . dumps ( baked_data , indent = 2 , sort_keys = True )
self . report ( { ' INFO ' } , f " Baked { active_engine } configuration. " )
return { ' FINISHED ' }
def collect_visible_props ( self , obj , node , data ) :
# 1. Handle choice-based routing
if " oneOf " in node :
options = node [ " oneOf " ]
if len ( options ) > 1 :
# Multiple options: standard discriminator logic
first_opt = resolve_schema_node ( options [ 0 ] , SCHEMA )
tag_name = list ( first_opt . get ( " properties " , { } ) . keys ( ) ) [ 0 ]
if hasattr ( obj , f " ui_ { tag_name } " ) :
current_tag_val = getattr ( obj , f " ui_ { tag_name } " )
for option in options :
branch = resolve_schema_node ( option , SCHEMA )
tag_default = branch . get ( " properties " , { } ) . get ( tag_name , { } ) . get ( " default " )
if tag_default == current_tag_val :
self . collect_visible_props ( obj , branch , data )
elif len ( options ) == 1 :
# Single option: Just dive in
branch = resolve_schema_node ( options [ 0 ] , SCHEMA )
self . collect_visible_props ( obj , branch , data )
# Note: We return or skip here so we don't process
# the same properties twice if they are defined in the branches.
if len ( options ) > 0 :
# Only return if the oneOf successfully navigated
# (Optional: check for shared properties below)
pass
# 2. Collect actual property values in this node
properties = node . get ( " properties " , { } )
for p_name , p_info in properties . items ( ) :
attr_name = f " ui_ { p_name } "
is_const = tell_if_const ( p_name , p_info )
2026-04-21 15:27:41 +02:00
if not p_name . startswith ( " _ " ) :
if is_const :
data [ p_name ] = p_info [ " default " ]
elif hasattr ( obj , attr_name ) :
val = getattr ( obj , attr_name )
# Check if this specific property is defined as a color in the schema
resolved_info = resolve_schema_node ( p_info , SCHEMA )
if resolved_info . get ( " type " ) == " color " :
# Convert the FloatVector (RGB) to "#RRGGBB"
data [ p_name ] = rgb_to_hex ( val )
else :
# Handle normal strings, ints, floats, etc.
data [ p_name ] = val
self . xrf_compose ( data , p_name )
# 3. compose correct XR Fragment hrefs
def xrf_compose ( self , data , p_name ) :
if p_name == " href " :
data [ p_name ] = " # " + data [ p_name ]
2026-04-08 10:28:31 +02:00
# --- 4. PANEL ---
class VIEW3D_PT_json_schema_props ( bpy . types . Panel ) :
bl_label = SCHEMA [ " title " ]
bl_idname = " VIEW3D_PT_json_schema_props "
bl_space_type = ' PROPERTIES '
bl_region_type = ' WINDOW '
bl_context = " object "
def draw ( self , context ) :
layout = self . layout
obj = context . object
layout . prop ( obj , " engine " )
active_engine = obj . engine
if active_engine == " NONE " : return
engine_opt = next ( ( opt for opt in SCHEMA [ " properties " ] [ " engine_settings " ] [ " oneOf " ]
if opt [ " engine_val " ] == active_engine ) , None )
if engine_opt :
2026-04-21 15:27:41 +02:00
layout . separator ( )
box = layout . column ( align = True )
2026-04-08 10:28:31 +02:00
resolved_node = resolve_schema_node ( engine_opt , SCHEMA )
self . draw_schema_node ( box , obj , resolved_node )
2026-04-21 15:27:41 +02:00
layout . separator ( )
2026-04-08 10:28:31 +02:00
layout . operator ( " object.bake_schema " )
def draw_schema_node ( self , layout , obj , node ) :
""" Recursively renders the schema UI with support for conditional branching. """
if " description " in node or " url " in node :
2026-04-21 15:27:41 +02:00
help_box = layout . column ( align = True )
2026-04-08 10:28:31 +02:00
if " url " in node :
2026-04-21 15:27:41 +02:00
op = help_box . operator ( " wm.url_open " , text = " Documentation " , icon = ' URL ' )
2026-04-08 10:28:31 +02:00
op . url = node [ " url " ]
if " description " in node :
2026-04-21 15:27:41 +02:00
description_text = node [ " description " ]
for line in description_text . split ( " \n " ) :
help_box . label ( text = line )
layout . separator ( ) # Add space before properties
2026-04-08 10:28:31 +02:00
layout . separator ( )
# 1. Handle oneOf Routing
if " oneOf " in node :
options = node [ " oneOf " ]
if len ( options ) > 1 :
# Multiple options: We need a selector (e.g., -janus-tag)
first_option = resolve_schema_node ( options [ 0 ] , SCHEMA )
tag_name = list ( first_option . get ( " properties " , { } ) . keys ( ) ) [ 0 ]
# Draw the dropdown selector
layout . prop ( obj , f " ui_ { tag_name } " )
# Match current selection to a branch
current_val = getattr ( obj , f " ui_ { tag_name } " )
for option in options :
branch = resolve_schema_node ( option , SCHEMA )
tag_default = branch . get ( " properties " , { } ) . get ( tag_name , { } ) . get ( " default " )
if tag_default == current_val :
# Recurse into the matching branch contents
self . draw_schema_node ( layout , obj , branch )
return # Exit after processing oneOf to prevent double-drawing parent props
elif len ( options ) == 1 :
# Single option wrapper: Dive straight in without drawing a selector
branch = resolve_schema_node ( options [ 0 ] , SCHEMA )
self . draw_schema_node ( layout , obj , branch )
# After the branch is drawn, we continue to draw any local properties
# (like -janus-lighting) defined at this level.
# 2. Draw standard properties for the current node
properties = node . get ( " properties " , { } )
for p_name , p_info in properties . items ( ) :
2026-04-21 15:27:41 +02:00
# Skip underscore properties + (Internal tags that shouldn't be edited)
2026-04-08 10:28:31 +02:00
is_const = tell_if_const ( p_name , p_info )
2026-04-21 15:27:41 +02:00
if is_const or p_name . startswith ( " _ " ) :
2026-04-08 10:28:31 +02:00
continue
attr_name = f " ui_ { p_name } "
if hasattr ( obj , attr_name ) :
# Safety check: If this property was already drawn as a 'oneOf' router
# in a parent call, we skip it here to avoid duplicates.
if " oneOf " in node :
first_opt = resolve_schema_node ( node [ " oneOf " ] [ 0 ] , SCHEMA )
if p_name == list ( first_opt . get ( " properties " , { } ) . keys ( ) ) [ 0 ] :
continue
layout . prop ( obj , attr_name )
# --- 5. REGISTRATION ---
def register ( ) :
# 1. Register top-level engine selector
bpy . types . Object . engine = bpy . props . EnumProperty (
2026-04-21 15:27:41 +02:00
items = SCHEMA [ " properties " ] [ " engine " ] [ " enum " ] ,
2026-04-08 10:28:31 +02:00
default = " NONE "
)
def create_and_set_prop ( prop_name , schema_node , override_enum_items = None ) :
""" Unified helper to map JSON schema nodes to Blender properties. """
attr_name = f " ui_ { prop_name } "
if hasattr ( bpy . types . Object , attr_name ) :
return
info = resolve_schema_node ( schema_node , SCHEMA )
label = prop_name # prop_name.replace("-", " ").strip().title()
if info . get ( " title " ) :
label = info . get ( " title " )
prop_type = info . get ( " type " )
common = { " description " : info . get ( " description " , " " ) }
if info . get ( " minimum " ) != None :
common [ " min " ] = info . get ( " minimum " )
if info . get ( " maximum " ) != None :
common [ " max " ] = info . get ( " maximum " )
if override_enum_items :
prop = bpy . props . EnumProperty ( * * common , name = label , items = override_enum_items )
elif " enum " in info :
items = [ ( v , v , " " ) for v in info [ " enum " ] ]
# Set default if it exists in the schema
default_val = info . get ( " default " , items [ 0 ] [ 0 ] )
prop = bpy . props . EnumProperty ( * * common , name = label , items = items , default = default_val )
elif prop_type == " string " :
prop = bpy . props . StringProperty ( * * common , name = label , default = info . get ( " default " , " " ) )
elif prop_type == " integer " :
prop = bpy . props . IntProperty ( * * common , name = label , default = info . get ( " default " , 0 ) )
elif prop_type == " boolean " :
prop = bpy . props . BoolProperty ( * * common , name = label , default = info . get ( " default " , False ) )
elif prop_type == " number " :
# Blender uses FloatProperty for floating point numbers
prop = bpy . props . FloatProperty (
* * common ,
name = label ,
default = info . get ( " default " , 0.0 ) ,
precision = 6 # Optional: controls how many decimals show in UI
)
elif prop_type == " color " :
# Convert hex default to RGB float tuple for Blender UI
def_hex = info . get ( " default " , " #FFFFFF " ) . lstrip ( ' # ' )
def_rgb = tuple ( int ( def_hex [ i : i + 2 ] , 16 ) / 255 for i in ( 0 , 2 , 4 ) )
prop = bpy . props . FloatVectorProperty (
* * common ,
name = label ,
subtype = ' COLOR ' ,
default = def_rgb ,
size = 3 ,
min = 0.0 , max = 1.0
)
# custom types
elif prop_type == " object.name " :
prop = bpy . props . EnumProperty ( * * common , name = label , items = get_scene_object_items )
else :
return
setattr ( bpy . types . Object , attr_name , prop )
# 2. Iterate through definitions
for def_name , def_content in SCHEMA [ " $defs " ] . items ( ) :
# If the definition IS the property (a leaf node like -janus-thumb_id)
if " type " in def_content :
create_and_set_prop ( def_name , def_content )
# Handle Object-style definitions (standard properties)
props = def_content . get ( " properties " , { } )
for p_name , p_node in props . items ( ) :
create_and_set_prop ( p_name , p_node )
# Method A: Handle direct oneOf (The Router/Discriminator)
# CHANGE: Only treat as a router if there are MULTIPLE branches.
if " oneOf " in def_content and len ( def_content [ " oneOf " ] ) > 1 :
first_branch = resolve_schema_node ( def_content [ " oneOf " ] [ 0 ] , SCHEMA )
# Ensure properties exists before grabbing key
branch_props = first_branch . get ( " properties " , { } )
if branch_props :
tag_name = list ( branch_props . keys ( ) ) [ 0 ]
branch_items = [ ]
for opt in def_content [ " oneOf " ] :
res = resolve_schema_node ( opt , SCHEMA )
# Use .get() to avoid KeyError if 'default' is missing
val = res . get ( " properties " , { } ) . get ( tag_name , { } ) . get ( " default " , " " )
name = res . get ( " title " , val . title ( ) if val else " Option " )
branch_items . append ( ( val , name , " " ) )
create_and_set_prop ( tag_name , branch_props [ tag_name ] , override_enum_items = branch_items )
# Method B: Handle standard properties
props = def_content . get ( " properties " , { } )
for p_name , p_node in props . items ( ) :
create_and_set_prop ( p_name , p_node )
# Finalize registration
bpy . utils . register_class ( OBJECT_OT_bake_schema )
bpy . utils . register_class ( VIEW3D_PT_json_schema_props )
if __name__ == " __main__ " :
register ( )
2026-04-21 15:27:41 +02:00