Created
March 1, 2025 09:31
-
-
Save vincentfretin/94e2fdf631782595c26283032d605ce1 to your computer and use it in GitHub Desktop.
lil-gui aframe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import GUI from "lil-gui"; | |
import type { Entity } from "aframe"; | |
AFRAME.registerComponent("lil-gui", { | |
init: function () { | |
// Add styles for folder titles | |
const style = document.createElement("style"); | |
style.textContent = ` | |
.lil-gui .entity-title { | |
font-size: 1.2em; | |
font-weight: bold; | |
} | |
.lil-gui .component-title { | |
} | |
`; | |
document.head.appendChild(style); | |
const gui = createInspectorGUI(); | |
}, | |
}); | |
interface SchemaProperty { | |
type: string; | |
default: any; | |
oneOf?: string[]; | |
min?: number; | |
max?: number; | |
if?: any; | |
is?: string; | |
} | |
interface Schema { | |
[key: string]: SchemaProperty; | |
} | |
function createComponentController(folder: GUI, entity: Entity, componentName: string, component: any) { | |
const data = entity.getAttribute(componentName); | |
if (component.isSingleProperty) { | |
// Handle single-property component | |
createPropertyController(folder, componentName, data, component.schema as SchemaProperty, (value: any) => | |
entity.setAttribute(componentName, value) | |
); | |
return; | |
} | |
// Handle multi-property component | |
const componentSchema = component.schema as Schema; | |
const componentFolder = folder.addFolder(componentName); | |
componentFolder.close(); | |
// Add a custom class to component folder title | |
componentFolder.domElement.querySelector(".title")?.classList.add("component-title"); | |
Object.entries(componentSchema).forEach(([propName, propSchema]) => { | |
const propValue = data[propName]; | |
if (propValue === undefined) return; | |
createPropertyController(componentFolder, propName, propValue, propSchema, (value: any) => { | |
const update = { ...data }; | |
update[propName] = value; | |
entity.setAttribute(componentName, update); | |
}); | |
}); | |
} | |
export function createInspectorGUI(container?: HTMLElement) { | |
const gui = new GUI({ container }) as GUI; | |
gui.close(); | |
const scene = document.querySelector("a-scene"); | |
if (!scene) return gui; | |
const entities = Array.from(scene.querySelectorAll("[id]:not(a-mixin)")) as Entity[]; | |
const defaultComponents = ["position", "rotation", "scale", "visible"]; | |
entities.forEach((entity) => { | |
const entityId = entity.id || "unnamed-entity"; | |
const entityFolder = gui.addFolder(entityId); | |
entityFolder.close(); | |
// Add a custom class to entity folder title | |
entityFolder.domElement.querySelector(".title")?.classList.add("entity-title"); | |
// Handle default components | |
defaultComponents.forEach((componentName) => { | |
const component = AFRAME.components[componentName]; | |
createComponentController(entityFolder, entity, componentName, component); | |
}); | |
// Handle other components | |
const components = Object.keys(entity.components).filter((name) => !defaultComponents.includes(name)); | |
components.forEach((componentName) => { | |
const component = entity.components[componentName]; | |
createComponentController(entityFolder, entity, componentName, component); | |
}); | |
}); | |
return gui; | |
} | |
function createPropertyController( | |
folder: GUI, | |
name: string, | |
value: any, | |
schema: SchemaProperty, | |
onChange: (value: any) => void | |
) { | |
const controller = createController(folder, name, value, schema, onChange); | |
return controller; | |
} | |
function createController( | |
folder: GUI, | |
name: string, | |
value: any, | |
schema: SchemaProperty, | |
onChange: (value: any) => void | |
) { | |
const type = schema.type; | |
switch (type) { | |
case "number": | |
case "int": { | |
const controller = folder.add({ [name]: value }, name); | |
if (schema.min !== undefined) controller.min(schema.min); | |
if (schema.max !== undefined) controller.max(schema.max); | |
if (type === "int") controller.step(1); | |
controller.onChange(onChange); | |
return controller; | |
} | |
case "boolean": { | |
return folder.add({ [name]: value }, name).onChange(onChange); | |
} | |
case "color": { | |
return folder.addColor({ [name]: value.getHex(THREE.SRGBColorSpace) }, name).onChange(onChange); | |
} | |
case "vec2": { | |
const vecFolder = folder.addFolder(name); | |
["x", "y"].forEach((axis) => { | |
vecFolder.add({ [axis]: value[axis] || 0 }, axis).onChange((v: number) => { | |
value[axis] = v; | |
onChange(value); | |
}); | |
}); | |
return vecFolder; | |
} | |
case "vec3": { | |
const vecFolder = folder.addFolder(name); | |
["x", "y", "z"].forEach((axis) => { | |
vecFolder.add({ [axis]: value[axis] || 0 }, axis).onChange((v: number) => { | |
value[axis] = v; | |
onChange(value); | |
}); | |
}); | |
return vecFolder; | |
} | |
case "vec4": { | |
const vecFolder = folder.addFolder(name); | |
["x", "y", "z", "w"].forEach((axis) => { | |
vecFolder.add({ [axis]: value[axis] || 0 }, axis).onChange((v: number) => { | |
value[axis] = v; | |
onChange(value); | |
}); | |
}); | |
return vecFolder; | |
} | |
case "map": | |
case "asset": { | |
// Handle texture/asset selection | |
const assets = Array.from(document.querySelectorAll("a-assets img, a-assets video")) | |
.map((asset) => "#" + asset.id) | |
.filter((id) => id !== "#"); // Filter out assets without ID | |
return folder.add({ [name]: value || "" }, name, ["", ...assets]).onChange(onChange); | |
} | |
case "selector": { | |
// Handle entity selection | |
const entities = Array.from(document.querySelectorAll("[id]")) | |
.map((el) => "#" + el.id) | |
.filter((id) => id !== "#"); // Filter out elements without ID | |
return folder.add({ [name]: value || "" }, name, ["", ...entities]).onChange(onChange); | |
} | |
case "array": { | |
const arrayFolder = folder.addFolder(name); | |
arrayFolder.close(); | |
// For array types, create a text input that parses comma-separated values | |
const arrayStr = Array.isArray(value) ? value.join(", ") : ""; | |
arrayFolder.add({ value: arrayStr }, "value").onChange((v: string) => { | |
const newArray = v.split(",").map((item) => item.trim()); | |
onChange(newArray); | |
}); | |
return arrayFolder; | |
} | |
case "selectorAll": { | |
const selectorFolder = folder.addFolder(name); | |
selectorFolder.close(); | |
// Similar to selector but allows multiple selections | |
const entities = Array.from(document.querySelectorAll("[id]")) | |
.map((el) => "#" + el.id) | |
.filter((id) => id !== "#"); | |
const selected = value ? value.split(",").map((s: string) => s.trim()) : []; | |
entities.forEach((entityId) => { | |
selectorFolder.add({ [entityId]: selected.includes(entityId) }, entityId).onChange((checked: boolean) => { | |
const newSelected = checked ? [...selected, entityId] : selected.filter((id: string) => id !== entityId); | |
onChange(newSelected.join(", ")); | |
}); | |
}); | |
return selectorFolder; | |
} | |
case "string": { | |
if (schema.oneOf) { | |
return folder.add({ [name]: value }, name, schema.oneOf).onChange(onChange); | |
} | |
return folder.add({ [name]: value }, name).onChange(onChange); | |
} | |
case "model": { | |
return folder.add({ [name]: value }, name).onChange(onChange); | |
} | |
default: { | |
// For any other types, create a basic text input | |
console.warn(`Unsupported property type: ${type} for ${name}`); | |
return folder.add({ [name]: value }, name).onChange(onChange); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment