Skip to content

Instantly share code, notes, and snippets.

@vincentfretin
Created March 1, 2025 09:31
Show Gist options
  • Save vincentfretin/94e2fdf631782595c26283032d605ce1 to your computer and use it in GitHub Desktop.
Save vincentfretin/94e2fdf631782595c26283032d605ce1 to your computer and use it in GitHub Desktop.
lil-gui aframe
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