Compare commits

..

No commits in common. "31e4f450083d1792a84fdbb03c952cc90b9476cd" and "a43a030c08c95d0bae058e7cecdd7d12b6e633d1" have entirely different histories.

4 changed files with 993 additions and 1006 deletions

File diff suppressed because one or more lines are too long

View file

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

View file

@ -3,25 +3,45 @@ import json
# --- 1. SCHEMA --- # --- 1. SCHEMA ---
SCHEMA = { SCHEMA = {
"title": "XR Fragments",
"type": "object",
"$version": 2,
"flatten": True,
"$defs": {
"xrf":{ "$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "XR Fragments", "$id": "https://xrfragment.org/schema/xrfragment-engine-prefixes.json",
"description": "XR Fragments (level2) promotes embedding clickable hyperlinks\nin 3D files (via the href attribute).\nMake sure to check 'custom attributes' in your export dialog.", "$version": 35,
"url": "https://xrfragment.org/#How%20it%20works", "title": "XR Fragments Engine Prefixes",
"properties":{ "type": "object",
"_xrf-type": { "title": "type", "type": "string", "default":"xrf", "description": "needed for blender UI"}, "flatten": True,
"href":{
"type":"object.name", "properties": {
"description": "teleport to / href" "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": { "three": {
"oneOf":[ "oneOf":[
{"$ref": "#/$defs/three_material"}, {"$ref": "#/$defs/three_material"},
@ -103,7 +123,7 @@ SCHEMA = {
"-janus-scale": {"$ref": "#/$defs/-janus-scale"}, "-janus-scale": {"$ref": "#/$defs/-janus-scale"},
"-janus-rotation": {"$ref": "#/$defs/-janus-rotation" }, "-janus-rotation": {"$ref": "#/$defs/-janus-rotation" },
"-janus-locked": {"$ref": "#/$defs/-janus-locked"}, "-janus-locked": {"$ref": "#/$defs/-janus-locked"},
"-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"}, "-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"}
} }
}, },
"janus_video": { "janus_video": {
@ -120,7 +140,7 @@ SCHEMA = {
"-janus-scale": {"$ref": "#/$defs/-janus-scale"}, "-janus-scale": {"$ref": "#/$defs/-janus-scale"},
"-janus-rotation": {"$ref": "#/$defs/-janus-rotation" }, "-janus-rotation": {"$ref": "#/$defs/-janus-rotation" },
"-janus-locked": {"$ref": "#/$defs/-janus-locked"}, "-janus-locked": {"$ref": "#/$defs/-janus-locked"},
"-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"} "-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"},
"-janus-gain": { "type":"float", "minimum":0, "maximum":1 } "-janus-gain": { "type":"float", "minimum":0, "maximum":1 }
} }
}, },
@ -410,7 +430,7 @@ SCHEMA = {
}, },
"-babylon-material.sideOrientation": { "-babylon-material.sideOrientation": {
"type": "string", "type": "string",
"description": "mat.backFaceCulling = true // or mat.sideOrientation", "description": "mat.backFaceCulling = True // or mat.sideOrientation",
"enum": [ "enum": [
"BABYLON.Material.ClockWiseSideOrientation", "BABYLON.Material.ClockWiseSideOrientation",
"BABYLON.Material.CounterClockWiseSideOrientation", "BABYLON.Material.CounterClockWiseSideOrientation",
@ -450,40 +470,28 @@ SCHEMA = {
} }
} }
} }
},
"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)"},
]
}
} }
} }
####### 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 --- # --- 2. HELPERS ---
def resolve_schema_node(node, schema_root): def resolve_schema_node(node, schema_root):
@ -496,19 +504,21 @@ def resolve_schema_node(node, schema_root):
return node return node
def get_scene_object_items(self, context): def get_scene_object_items(self, context):
items = [("NONE", "-- select target / asset --", "")] items = ["-- select janus asset --"]
if context is None: # Use a set to ensure unique names if necessary,
return items # but obj.name is always unique in Blender
obj_list = []
for obj in context.scene.objects: for obj in context.scene.objects:
obj_list.append((obj.name, obj.name, f"Object: {obj.name}")) items.append((obj.name, obj.name, f"Object: {obj.name}"))
# Sort objects alphabetically by name
obj_list.sort(key=lambda x: x[1].lower()) if not items:
items.extend(obj_list) return [("NONE", "No Objects Found", "")]
# Optional: Sort alphabetically
items.sort(key=lambda x: x[0])
return items return items
def tell_if_const(p_name, p_info): def tell_if_const(p_name, p_info):
return (p_info.get("type") == "string" and "default" in p_info and len(p_info) == 2) 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): def rgb_to_hex(rgb):
"""Converts linear RGB floats to sRGB Hex string.""" """Converts linear RGB floats to sRGB Hex string."""
@ -583,7 +593,7 @@ class OBJECT_OT_bake_schema(bpy.types.Operator):
for p_name, p_info in properties.items(): for p_name, p_info in properties.items():
attr_name = f"ui_{p_name}" attr_name = f"ui_{p_name}"
is_const = tell_if_const(p_name, p_info) is_const = tell_if_const(p_name, p_info)
if not p_name.startswith("_"):
if is_const: if is_const:
data[p_name] = p_info["default"] data[p_name] = p_info["default"]
elif hasattr(obj, attr_name): elif hasattr(obj, attr_name):
@ -597,12 +607,6 @@ class OBJECT_OT_bake_schema(bpy.types.Operator):
else: else:
# Handle normal strings, ints, floats, etc. # Handle normal strings, ints, floats, etc.
data[p_name] = val 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]
# --- 4. PANEL --- # --- 4. PANEL ---
@ -625,11 +629,9 @@ class VIEW3D_PT_json_schema_props(bpy.types.Panel):
if opt["engine_val"] == active_engine), None) if opt["engine_val"] == active_engine), None)
if engine_opt: if engine_opt:
layout.separator() box = layout.box()
box = layout.column(align=True)
resolved_node = resolve_schema_node(engine_opt, SCHEMA) resolved_node = resolve_schema_node(engine_opt, SCHEMA)
self.draw_schema_node(box, obj, resolved_node) self.draw_schema_node(box, obj, resolved_node)
layout.separator()
layout.operator("object.bake_schema") layout.operator("object.bake_schema")
@ -637,15 +639,14 @@ class VIEW3D_PT_json_schema_props(bpy.types.Panel):
"""Recursively renders the schema UI with support for conditional branching.""" """Recursively renders the schema UI with support for conditional branching."""
if "description" in node or "url" in node: if "description" in node or "url" in node:
help_box = layout.column(align=True) # Create a row and explicitly set alignment to LEFT
row = layout.row(align=True)
row.alignment = 'LEFT'
if "url" in node: if "url" in node:
op = help_box.operator("wm.url_open", text="Documentation", icon='URL') op = row.operator("wm.url_open", text="", icon='HELP', emboss=False)
op.url = node["url"] op.url = node["url"]
if "description" in node: if "description" in node:
description_text = node["description"] row.label(text=node["description"] )
for line in description_text.split("\n"):
help_box.label(text=line)
layout.separator() # Add space before properties
layout.separator() layout.separator()
# 1. Handle oneOf Routing # 1. Handle oneOf Routing
@ -681,9 +682,9 @@ class VIEW3D_PT_json_schema_props(bpy.types.Panel):
# 2. Draw standard properties for the current node # 2. Draw standard properties for the current node
properties = node.get("properties", {}) properties = node.get("properties", {})
for p_name, p_info in properties.items(): for p_name, p_info in properties.items():
# Skip underscore properties + (Internal tags that shouldn't be edited) # Skip 'const' properties (Internal tags that shouldn't be edited)
is_const = tell_if_const(p_name, p_info) is_const = tell_if_const(p_name, p_info)
if is_const or p_name.startswith("_"): if is_const:
continue continue
attr_name = f"ui_{p_name}" attr_name = f"ui_{p_name}"
@ -700,8 +701,9 @@ class VIEW3D_PT_json_schema_props(bpy.types.Panel):
# --- 5. REGISTRATION --- # --- 5. REGISTRATION ---
def register(): def register():
# 1. Register top-level engine selector # 1. Register top-level engine selector
enum_items = [tuple(item) for item in SCHEMA["properties"]["engine"]["enum"]]
bpy.types.Object.engine = bpy.props.EnumProperty( bpy.types.Object.engine = bpy.props.EnumProperty(
items=SCHEMA["properties"]["engine"]["enum"], items=enum_items,
default="NONE" default="NONE"
) )
@ -808,4 +810,3 @@ def register():
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View file

@ -1,23 +1,42 @@
{ {
"title": "XR Fragments", "$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", "type": "object",
"$version": 3,
"flatten": true, "flatten": true,
"$defs": {
"xrf":{ "properties": {
"title": "XR Fragments", "engine": {
"description": "XR Fragments (level2) promotes embedding clickable hyperlinks\nin 3D files (via the href attribute).\nMake sure to check 'custom attributes' in your export dialog.", "type": "enum",
"url": "https://xrfragment.org/#How%20it%20works", "enum": [
"properties":{ ["NONE", "-- Select --", ""],
"_xrf-type": { "title": "type", "type": "string", "default":"xrf", "description": "needed for blender UI"}, ["JANUSXR", "JanusXR", ""],
"href":{ ["THREEJS", "Three.js", ""],
"type":"object.name", ["GODOT3", "Godot 3", ""],
"description": "teleport to / href" ["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": { "three": {
"oneOf":[ "oneOf":[
{"$ref": "#/$defs/three_material"}, {"$ref": "#/$defs/three_material"},
@ -99,7 +118,7 @@
"-janus-scale": {"$ref": "#/$defs/-janus-scale"}, "-janus-scale": {"$ref": "#/$defs/-janus-scale"},
"-janus-rotation": {"$ref": "#/$defs/-janus-rotation" }, "-janus-rotation": {"$ref": "#/$defs/-janus-rotation" },
"-janus-locked": {"$ref": "#/$defs/-janus-locked"}, "-janus-locked": {"$ref": "#/$defs/-janus-locked"},
"-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"}, "-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"}
} }
}, },
"janus_video": { "janus_video": {
@ -116,7 +135,7 @@
"-janus-scale": {"$ref": "#/$defs/-janus-scale"}, "-janus-scale": {"$ref": "#/$defs/-janus-scale"},
"-janus-rotation": {"$ref": "#/$defs/-janus-rotation" }, "-janus-rotation": {"$ref": "#/$defs/-janus-rotation" },
"-janus-locked": {"$ref": "#/$defs/-janus-locked"}, "-janus-locked": {"$ref": "#/$defs/-janus-locked"},
"-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"} "-janus-draw-layer": {"$ref": "#/$defs/-janus-draw_layer"},
"-janus-gain": { "type":"float", "minimum":0, "maximum":1 } "-janus-gain": { "type":"float", "minimum":0, "maximum":1 }
} }
}, },
@ -446,36 +465,5 @@
} }
} }
} }
},
"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)"},
]
}
} }
} }