added engine prefixes schema + blender plugin

This commit is contained in:
Leon van Kammen 2026-04-08 10:28:31 +02:00
parent b1043733d4
commit 17bc4bb9ea
3 changed files with 1302 additions and 0 deletions

View file

@ -0,0 +1,21 @@
#!/usr/bin/env -S awk -f
BEGIN {
REPLACE=1
state=0
while( (getline < "./tool/blender-xrfragments.py") > 0 ){
if( $1 ~ /^}/ && state == REPLACE ) state=0
print $0
if( state == REPLACE ){
while( (getline < "./xrfragment-engine-prefixes.json") > 0 ){
gsub("true","True",$0)
gsub("false","False",$0)
if( $1 == "\"$version\":" ) $2 = ($2+1)"," # bump version
if( $0 !~ /^[{}]/ ) print $0
}
}
if( $1 == "SCHEMA" ) state=REPLACE
}
}

View file

@ -0,0 +1,812 @@
import bpy
import json
# --- 1. SCHEMA ---
SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://xrfragment.org/schema/xrfragment-engine-prefixes.json",
"$version": 35,
"title": "XR Fragments Engine Prefixes",
"type": "object",
"flatten": True,
"properties": {
"engine": {
"type": "enum",
"enum": [
["NONE", "-- Select --", ""],
["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": "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)"}
]
}
},
"$defs": {
"three": {
"oneOf":[
{"$ref": "#/$defs/three_material"},
{"$ref": "#/$defs/three_object"}
]
},
"aframe": {
"oneOf":[
{"$ref": "#/$defs/three_material"},
{"$ref": "#/$defs/three_object"}
]
},
"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
}
}
},
"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
}
}
}
}
}
####### JSONSchema-to-form-to-custom-properties generator
#
# The code below was written based on LLM-generated boilerplate.
# I've had to rewrite every function, so at best it was a very convoluted
# way to lookup all necessary function-names.
#
# model: Gemini3 (fast)
# prompt: write a jsonschemaform-tab-to-custom properties-tab blender script.
# The form should appear in a tab of Object properties, and render a
# statically defined jsonschema with: oneOf, anyOf, allOf, $ref & $defs,
# datatypes like float,string,int,boolean, nesting, enums, default, title
# (for human form-display), enumHuman (for human enum display), description,
# pattern, minimum, maximum,
# Regarding output: when clicking an apply-button, write the JSON-object to
# the custom property-tab as 'JSON' with valuetype 'string'.
# If 'flatten' is set on the schema-root, collect all selected key/value
# properties from the form, and write each key/value separately to the custom
# property tab (only integer, string, boolean, float properties).
# --- 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):
items = ["-- select janus asset --"]
# Use a set to ensure unique names if necessary,
# but obj.name is always unique in Blender
for obj in context.scene.objects:
items.append((obj.name, obj.name, f"Object: {obj.name}"))
if not items:
return [("NONE", "No Objects Found", "")]
# Optional: Sort alphabetically
items.sort(key=lambda x: x[0])
return items
def tell_if_const(p_name, p_info):
return p_name.startswith("_") or (p_info.get("type") == "string" and "default" in p_info and len(p_info) == 2)
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)
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
# --- 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:
box = layout.box()
resolved_node = resolve_schema_node(engine_opt, SCHEMA)
self.draw_schema_node(box, obj, resolved_node)
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:
# Create a row and explicitly set alignment to LEFT
row = layout.row(align=True)
row.alignment = 'LEFT'
if "url" in node:
op = row.operator("wm.url_open", text="", icon='HELP', emboss=False)
op.url = node["url"]
if "description" in node:
row.label(text=node["description"] )
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():
# Skip 'const' properties (Internal tags that shouldn't be edited)
is_const = tell_if_const(p_name, p_info)
if is_const:
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
enum_items = [tuple(item) for item in SCHEMA["properties"]["engine"]["enum"]]
bpy.types.Object.engine = bpy.props.EnumProperty(
items=enum_items,
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()

View file

@ -0,0 +1,469 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://xrfragment.org/schema/xrfragment-engine-prefixes.json",
"$version": 34,
"title": "XR Fragments Engine Prefixes",
"type": "object",
"flatten": true,
"properties": {
"engine": {
"type": "enum",
"enum": [
["NONE", "-- Select --", ""],
["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": "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)"}
]
}
},
"$defs": {
"three": {
"oneOf":[
{"$ref": "#/$defs/three_material"},
{"$ref": "#/$defs/three_object"}
]
},
"aframe": {
"oneOf":[
{"$ref": "#/$defs/three_material"},
{"$ref": "#/$defs/three_object"}
]
},
"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
}
}
},
"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
}
}
}
}
}