WBrenderer JS API

A high-fidelity software implementation of the classic BRender engine, written in pure ES6 with zero runtime dependencies. Optimized for high-performance pixel manipulation and nostalgic 90s-era 3D rendering.

Core Principles

Data Structures

The engine relies on a small set of core classes and enums to manage scene state and assets.

Actor

The Actor is the fundamental node of the scene graph. It handles hierarchy and spatial transformation.

PropertyTypeDescription
typeACTOR_TYPEDetermines actor behavior (Model, Camera, etc).
transformMat34Local-to-parent transformation matrix.
modelModelThe geometry to render (if type is MODEL).
materialNumberFallback 0xRRGGBB color if no materials are set.
materialsMaterial[]Array of materials indexed by the model's face data.
cameraCameraCamera settings (if type is CAMERA).
lightLightLight settings (if type is LIGHT).

Model

A Model contains the geometry data loaded from .DAT files or generated via primitives.

PropertyTypeDescription
nameStringIdentifier used for registry lookups.
verticesFloat32ArrayPacked vertex data (x, y, z, nx, ny, nz, u, v).
facesUint16ArrayVertex indices forming triangles.
materialNamesString[]List of material identifiers referenced by faces.
faceMaterialIndexUint16ArrayIndex into the actor's material array per face.

Example: Manual Model Construction

You can create custom geometry by packing raw arrays into the interleaved format expected by the renderer.

import { Model, makeVertices } from 'wbrenderer';

// 1. Raw geometric data
const positions = new Float32Array([
    -1, -1, 0,  // v0
     1, -1, 0,  // v1
     0,  1, 0   // v2
]);

const uvs = new Float32Array([
    0, 0,
    1, 0,
    0.5, 1
]);

// 2. Pack into interleaved vertex buffer (stride 8: x,y,z, nx,ny,nz, u,v)
// Pass null for normals to let the renderer auto-calculate face normals.
const vertices = makeVertices(positions, null, uvs);

// 3. Create the model with an index array (faces)
const faces = new Uint16Array([0, 1, 2]);
const myModel = new Model(vertices, faces, { name: 'triangle.dat' });

Example: Per-Face Materials

To render a model with multiple materials, assign an array to actor.materials. The renderer uses the faceMaterialIndex from the model to look up which material to use for each triangle.

import { Actor, ACTOR_TYPE, Br } from 'wbrenderer';

const car = new Actor({ type: ACTOR_TYPE.MODEL, model: carModel });

// Option A: Resolve materials from the global registry using model metadata.
// This is the standard approach after using BrFmtMaterialLoad().
car.materials = carModel.materialNames.map(name => Br.BrMaterialFind(name));

// Option B: Manually define a material list.
// The model's faceMaterialIndex values will index into this array.
car.materials = [bodyMat, tireMat, glassMat];

Material

Defines the surface appearance of geometry.

PropertyTypeDescription
colourNumberBase 0xRRGGBB color.
flagsMATFBitset for lighting, transparency, and culling.
colourMapPixelmapThe texture to apply (palette-indexed).
paletteUint8Array256-entry RGB palette (stride 3) for the texture.

Pixelmap

A generic buffer for image or depth data.

PropertyTypeDescription
width / heightNumberDimensions in pixels.
typePixelmapTypeMemory layout (e.g. RGBX_8888 or DEPTH_F32).
pixelsTypedArrayThe raw pixel buffer.

Constants & Enums

ACTOR_TYPE

ValueDescription
NONEEmpty container node.
MODELVisual geometry node.
CAMERAViewpoint node.
LIGHTLight source node.

PixelmapType

ValueDescription
RGBX_888832-bit color (8888).
DEPTH_F3232-bit floating point depth.

Math Library

All operations are pure functions to avoid object allocation in hot paths.

Vec3 (3-Element Vector)

import { Vec3 } from 'wbrenderer';

// Create a Float32Array(3)
const pos = Vec3.create(0, 10, 0);

// Standard operations (out, a, b)
Vec3.add(pos, pos, [1, 0, 0]);
Vec3.normalize(pos, pos);
Function Parameters Description
create(x, y, z) x, y, z: Numbers (Optional) Creates a new Float32Array(3).
add / sub(out, a, b) out, a, b: Vec3 Vector addition or subtraction.
scale(out, a, s) out, a: Vec3, s: Number Scales vector a by scalar s.
dot(a, b) a, b: Vec3 Returns the dot product (scalar).
cross(out, a, b) out, a, b: Vec3 Calculates cross product.
length(a) / normalize(out, a) out, a: Vec3 Calculates magnitude or unit vector.

Mat4 (4x4 Matrix)

Used primarily for projection and view transformations. Column-major 16-element array.

Function Parameters Description
create() / identity(out) out: Mat4 Allocates or resets a 4x4 identity matrix.
multiply(out, a, b) out, a, b: Mat4 Matrix multiplication (out = a * b).
perspective(out, fovY, asp, n, f) out: Mat4, fovY, asp, n, f: Numbers Builds a perspective projection matrix.
lookAt(out, eye, tgt, up) out: Mat4, eye, tgt, up: Vec3 Builds a view matrix looking from eye to target.
transformPoint(out, m, p) out: Vec4, m: Mat4, p: Vec3 Transforms a 3D point into clip space.

Mat34 (3x4 Affine Matrix)

Used for all actor transformations. Compact representation of rotation and translation.

const m = Mat34.create(); // Identity
Mat34.rotationY(m, Math.PI / 4);
Function Parameters Description
create() / identity(out) out: Mat34 Allocates or resets a 3x4 identity affine matrix.
translation(out, x, y, z) out: Mat34, x, y, z: Numbers Sets a translation matrix.
rotationX/Y/Z(out, rad) out: Mat34, rad: Number (Radians) Sets a rotation matrix.
multiply(out, a, b) out, a, b: Mat34 Multiplies two affine matrices.
transformPoint(out, m, p) out: Vec3, m: Mat34, p: Vec3 Transforms a 3D point.
toMat4(out, m) out: Mat4, m: Mat34 Promotes a 3x4 affine matrix to a full 4x4.

Pixelmaps & Framebuffers

Pixelmaps wrap raw buffers for color and depth data.

import { Pixelmap, TYPE } from 'wbrenderer';

const color = new Pixelmap(320, 240, TYPE.RGBX_8888);
const depth = new Pixelmap(320, 240, TYPE.DEPTH_F32);

color.clear(0x101820); // Dark navy clear

Scene Graph

WBrenderer uses an Actor hierarchy similar to BRender's V1DB system.

Note: All actors require a type (MODEL, CAMERA, LIGHT, or NONE).
const scene = new SceneRoot();

const car = new Actor({ 
    type: ACTOR_TYPE.MODEL, 
    model: eagleModel, 
    material: 0xff0000 
});

scene.add(car);

Traversal & Custom Properties

The walkTree utility provides a way to traverse the entire hierarchy starting from a specific root. Because Actor is a standard class, you can attach arbitrary data to actors for use during these traversals.

import { walkTree } from 'wbrenderer';

// 1. Tagging actors with custom metadata during setup
const car = new Actor({ type: ACTOR_TYPE.MODEL, model: carModel });
car.isVehicleBody = true;
car.health = 100;

// 2. Using walkTree to find and process tagged actors
walkTree(scene, (actor) => {
    // Identify actors by custom properties or model names
    if (actor.isVehicleBody) {
        // Perform vehicle-specific logic
        actor.health -= 10;
    }

    // modelName is often populated by loaders for Actors in a hierarchy
    if (actor.modelName === 'WHEEL.DAT') {
        // Update wheel rotation, or link specialized wheel materials
    }
});

This pattern is highly effective for post-processing hierarchies loaded from .ACT files, where you may need to resolve materials or attach physics proxy objects to specific sub-models based on their names or tags.

Cameras & Lights

Settings for viewpoints and illumination sources within the scene hierarchy.

CAMERA_TYPE

Determines the projection model used by the camera actor.

TypeDescription
CAMERA_TYPE.PERSPECTIVEStandard perspective projection. Requires fovY and aspect.
CAMERA_TYPE.PARALLELOrthographic projection. Distances do not affect object size.

Camera Properties

PropertyTypeDescription
fovYNumberVertical field of view in radians. Default is ~60° (Math.PI / 3).
aspectNumberWidth / Height ratio (e.g., 800 / 600).
hitherNumberNear clipping plane. Minimal recommended value: 0.05.
yonNumberFar clipping plane. Geometry beyond this is culled.

LIGHT_TYPE

Determines how light is emitted from the actor's position or orientation.

TypeDescription
LIGHT_TYPE.DIRECTInfinite distance light (Sun). Parallel rays based on actor orientation.
LIGHT_TYPE.POINTOmnidirectional light source at actor position. Subject to attenuation.
LIGHT_TYPE.SPOTConical light source. Uses innerAngle and outerAngle cones.

Light Properties

PropertyTypeDescription
colourNumberLight color as 0xRRGGBB.
intensityNumberBrightness multiplier (typically 0.0 to 1.0).
attenC / attenL / attenQNumberAttenuation coefficients (Constant, Linear, Quadratic). Formula: 1 / (C + L·d + Q·d²).
innerAngle / outerAngleNumberSpotlight cone half-angles in radians.
Pro-tip: Use Br.BrLightAllocate(opts) to quickly create a Light actor with these properties in one call.

Camera Movement

Implementing input-driven camera movement involves updating the Actor.transform based on user input. Here are two common patterns.

Orbit Camera

An orbit camera rotates around a target point. This is typically implemented using spherical coordinates (Yaw, Pitch, Distance).

let yaw = 0.3, pitch = 0.35, dist = 14;
const target = [0, 0, -10];

// Update your camera transform every frame
function setOrbitCamera(camActor) {
    const cy = Math.cos(yaw), sy = Math.sin(yaw);
    const cp = Math.cos(pitch), sp = Math.sin(pitch);
    
    // Calculate eye position
    const eye = [
        target[0] + dist * cp * sy,
        target[1] + dist * sp,
        target[2] + dist * cp * cy
    ];

    // Build camera basis manually (LookAt target)
    const fx = target[0] - eye[0], fy = target[1] - eye[1], fz = target[2] - eye[2];
    const fl = Math.hypot(fx, fy, fz) || 1;
    const fnx = fx / fl, fny = fy / fl, fnz = fz / fl;

    // Right = Forward x Up (0,1,0)
    let rx = -fnz, ry = 0, rz = fnx;
    const rl = Math.hypot(rx, ry, rz) || 1;
    rx /= rl; rz /= rl;

    // Up = Right x Forward
    const ux = ry * fnz - rz * fny;
    const uy = rz * fnx - rx * fnz;
    const uz = rx * fny - ry * fnx;

    // Apply to Actor transform (Mat34 basis)
    const m = camActor.transform;
    m[0] = rx; m[1] = ry; m[2] = rz;   // X basis
    m[3] = ux; m[4] = uy; m[5] = uz;   // Y basis
    m[6] = -fnx; m[7] = -fny; m[8] = -fnz; // Z basis (-Forward)
    m[9] = eye[0]; m[10] = eye[1]; m[11] = eye[2]; // Translation
}

FPS (First-Person) Camera

An FPS camera rotates based on mouse movement and moves relative to its current orientation.

import { Mat34, Vec3 } from 'wbrenderer';

let pos = Vec3.create(0, 1.5, 5);
let yaw = 0, pitch = 0;

function updateFPSCamera(camActor, mouseDelta, keys) {
    // 1. Handle Rotation
    yaw -= mouseDelta.x * 0.005;
    pitch = Math.max(-Math.PI/2, Math.min(Math.PI/2, pitch - mouseDelta.y * 0.005));

    // 2. Create orientation matrix
    const rotY = Mat34.create();
    const rotX = Mat34.create();
    Mat34.rotationY(rotY, yaw);
    Mat34.rotationX(rotX, pitch);
    Mat34.multiply(camActor.transform, rotY, rotX);

    // 3. Handle Movement (relative to orientation)
    const m = camActor.transform;
    const forward = [-m[6], 0, -m[8]]; // Forward vector on XZ plane
    const right = [m[0], 0, m[2]];
    Vec3.normalize(forward, forward);
    Vec3.normalize(right, right);

    const move = Vec3.create();
    if (keys['w']) Vec3.add(move, move, forward);
    if (keys['s']) Vec3.sub(move, move, forward);
    if (keys['a']) Vec3.sub(move, move, right);
    if (keys['d']) Vec3.add(move, move, right);

    Vec3.normalize(move, move);
    Vec3.scale(move, move, 0.1); // Walking speed
    Vec3.add(pos, pos, move);

    // 4. Apply Translation
    m[9] = pos[0]; m[10] = pos[1]; m[11] = pos[2];
}

Materials & Flags

The Material class defines the surface properties of a model, including its color, texture, and shading behavior.

MATF Flags

The MATF bitset controls how the renderer processes the material's geometry. These can be combined using bitwise OR (|).

Flag Description
MATF.LIGHT Enables lighting calculations. If unset, the material is rendered at full intensity using its base colour.
MATF.SMOOTH / MATF.GOURAUD Enables Gouraud shading, which interpolates light intensity or vertex colors across the face for a smooth appearance.
MATF.TWO_SIDED Disables back-face culling. Both sides of the face will be rendered, essential for sprites or thin geometry.
MATF.ALWAYS_VISIBLE Ensures the face is always rendered by bypassing certain culling optimizations. Often used for important scene elements or effects.
MATF.DISABLE_COLOUR_KEY By default, palette index 0 is treated as transparent for textured materials. Setting this flag makes index 0 opaque.

The Renderer

The Renderer class processes the scene tree and executes the rasterization pipelines.

const r = new Renderer(color, depth);

// One-shot render of a full tree from a specific camera
r.renderTree(scene, cameraActor);

// Blit to a Canvas2D context
color.blitToImageData(myImageData);

Handling Window Resizing

When the browser window or container changes size, you must update the framebuffers and the camera's projection to prevent stretching or pixelation.

window.addEventListener('resize', () => {
    const W = window.innerWidth;
    const H = window.innerHeight;

    // 1. Re-allocate Pixelmaps (Renderer expects these to match in size)
    // 'color' and 'depth' should be accessible in your render loop
    color = new Pixelmap(W, H, TYPE.RGBX_8888);
    depth = new Pixelmap(W, H, TYPE.DEPTH_F32);

    // 2. Update Renderer with new buffers
    renderer = new Renderer(color, depth);

    // 3. Update Camera aspect ratio to prevent stretching
    camActor.camera.aspect = W / H;
    
    // 4. Update canvas element dimensions and ImageData cache
    canvas.width = W;
    canvas.height = H;
    imgData = ctx.createImageData(W, H);
});
Performance Note: If you are using the Br.BrZbSceneRender facade, the renderer is instantiated per-frame, so you only need to update the global pixelmap variables and the camera aspect ratio.

Format Loaders

Native support for binary Carmageddon/BRender assets.

Method Target Format Returns
loadDat(buffer) .DAT Array of Model objects
loadMat(buffer) .MAT Array of Material objects
loadPix(buffer) .PIX Array of Pixelmap (Bitmaps)
loadAct(buffer) .ACT Actor (Root of the hierarchy)

Example: Loading a Hierarchical Actor

The loadAct function reconstructs complex actor hierarchies from .ACT files. It is commonly used alongside walkTree to initialize materials and transformations for sub-models (e.g., wheels on a car).

import { loadAct, walkTree, Br } from 'wbrenderer';

// Load and parse the ACT hierarchy
const res = await fetch('EAGLE.ACT');
const carRoot = loadAct(await res.arrayBuffer());

// Traverse the tree to set up materials for specific sub-models
walkTree(carRoot, (actor) => {
    if (actor.modelName === 'WHEEL.DAT') {
        // Resolve materials from the global registry
        actor.materials = [Br.BrMaterialFind('BGLWEEL.MAT')];
    }
});

scene.add(carRoot);

Example: Manual Texture and Material Setup

For fine-grained control outside of the Br.* facade, you can use the raw loaders and manually link textures to materials.

import { loadMat, loadPix, Br } from 'wbrenderer';

const matAB = await (await fetch('LEVEL.MAT')).arrayBuffer();
const pixAB = await (await fetch('LEVEL.PIX')).arrayBuffer();

// 1. Load materials and pixelmaps into memory
const materials = loadMat(matAB);
const textures = loadPix(pixAB);

// 2. Register textures in the global map registry so they can be found by name
textures.forEach(pm => Br.BrMapAdd(pm));

// 3. Link textures and apply a shared palette to each material
for (const mat of materials) {
    if (mat.colourMapName) {
        mat.colourMap = Br.BrMapFind(mat.colourMapName);
        mat.palette = levelPalette; // 768-byte RGB palette (Uint8Array)
    }
    Br.BrMaterialAdd(mat);
}

Asset Registry

The Registry system provides a global, case-insensitive lookup mechanism for assets. This is critical for resolving named references found in .DAT, .MAT, and .ACT files.

Global Singletons

WBrenderer provides three specialized registry instances:

Methods

Method Parameters Description
add(name, item) name: String, item: Object Registers an asset. Names are normalized to uppercase internally.
find(name) name: String Retrieves an asset by name. Lookup is case-insensitive. Returns undefined if not found.
remove(name) name: String Removes an asset from the registry.

Example: Custom Registration

import { modelRegistry, Model } from 'wbrenderer';

// Manually register a custom model
modelRegistry.add('PROC_CUBE', myCustomCube);

// Retrieve it later (case does not matter)
const model = modelRegistry.find('proc_cube');

BRender Facade (Br.*)

The Br namespace provides a C-style entry point that mirrors BRender 1.3.2. This is the recommended path for porting legacy logic.

Lifecycle

Function Parameters Description
BrBegin() None Initializes the internal engine state.
BrEnd() None Shuts down the engine and clears state.
isInitialized() None Returns true if BrBegin is active.

Pixelmaps

Function Parameters Description
BrPixelmapAllocate(type, w, h) type: PixelmapType
w: Number (Width)
h: Number (Height)
Creates a new Pixelmap buffer.
BrPixelmapFill(pm, value) pm: Pixelmap
value: Number (color/depth)
Clears the pixelmap with a specific value.

Resource Registry

Function Parameters Description
BrModelAdd(model) / BrModelFind(name) model: Model / name: String Register or retrieve models by name.
BrMaterialAdd(mat) / BrMaterialFind(name) mat: Material / name: String Register or retrieve materials.
BrMapAdd(pm) / BrMapFind(name) pm: Pixelmap / name: String Register or retrieve textures/palettes.
Facade Loaders vs. Raw Loaders

The Br.BrFmt*Load functions (like BrFmtModelLoad) are wrappers around the raw loaders (like loadDat). The key differences are:

  • Side Effects: loadDat simply parses the buffer. BrFmtModelLoad parses the buffer AND calls BrModelAdd for every resulting model.
  • Name Resolution: By using the facade loaders, models and materials are indexed in the global Registry. This allows the loadAct function to automatically "stitch" models onto actors when it encounters a name like "EAGLE.DAT".
  • Palette Handling: BrFmtPixelmapLoad registers palettes found in .PIX files into the map registry. While loadPix returns them as raw objects, registration allows them to be found via BrMapFind. Note: Palettes often require manual repacking from 4-byte (XRGB) to 3-byte (RGB) formats for use in materials.
// The Raw Way: Manual management
const [model] = loadDat(buffer);
const actor = new Actor({ type: ACTOR_TYPE.MODEL, model });

// The BRender Way: Automatic registration
Br.BrFmtModelLoad(buffer); 
// Later, loading an .ACT file will find "EAGLE.DAT" automatically
const carRoot = loadAct(actBuffer); 

Example: Palette Repacking

Many legacy palettes (like DRRENDER.PAL) are stored as RGBX pixelmaps. You must repack these for use in Material.palette:

const palPm = Br.BrMapFind('DRRENDER.PAL');
const palData = new Uint8Array(256 * 3);
for (let i = 0; i < 256; i++) {
    const o = i * 4;
    palData[i * 3 + 0] = palPm.pixels[o + 1]; // R
    palData[i * 3 + 1] = palPm.pixels[o + 2]; // G
    palData[i * 3 + 2] = palPm.pixels[o + 3]; // B
}
// Link to material manually
mat.palette = palData;

Scene Graph

Function Parameters Description
BrActorAllocate(type) type: ACTOR_TYPE Creates a new actor of the specified type.
BrActorAdd(parent, child) parent: Actor, child: Actor Links a child actor to a parent. Returns child.
BrSceneRootAllocate() None Allocates a root container for the scene.

Rendering

Function Parameters Description
BrZbSceneRender(root, cam, color, depth) root, cam, color, depth Executes a Z-buffered render of the actor tree.
BrZbSceneRenderPrimitives(vp, prims, color, depth) vp: Mat4, prims: Array, color, depth Renders a flat list of meshes using a precomputed matrix.

Lights & Fog

Function Parameters Description
BrLightAllocate(opts) opts: Object Creates a Light actor (e.g. {type, color, intensity}).
BrLightEnable(actor) / BrLightDisable(actor) actor: Actor (Light) Toggles the active state of a light.
BrFogSet(scene, opts) scene: SceneRoot, opts: Object Configures linear fog ({colour, hither, yon}).

Quickstart: Plane and Cube

This example demonstrates how to set up a basic scene with a camera, a light, a plane, and a rotating cube.

import {
  Pixelmap, TYPE, Renderer, SceneRoot, Actor, ACTOR_TYPE,
  Camera, Light, LIGHT_TYPE, makeCube, makePlane, Mat34
} from 'wbrenderer';

export async function createScene({ canvas, wbr }) {
  const W = canvas.width;
  const H = canvas.height;

  const color = new Pixelmap(W, H, TYPE.RGBX_8888);
  const depth = new Pixelmap(W, H, TYPE.DEPTH_F32);
  const renderer = new Renderer(color, depth);

  const scene = new SceneRoot();
  scene.ambient = 0.2; // Some ambient light

  // Camera setup
  const cam = new Actor({ type: ACTOR_TYPE.CAMERA });
  cam.camera = new Camera({ fovY: Math.PI / 3, aspect: W / H, hither: 0.1, yon: 100 });
  Mat34.translation(cam.transform, 0, 1.5, 5);
  Mat34.rotationX(cam.transform, -Math.PI / 10); // Look slightly down
  scene.add(cam);

  // Light setup
  const light = new Actor({ type: ACTOR_TYPE.LIGHT });
  light.light = new Light(LIGHT_TYPE.DIRECT);
  light.light.color = 0xffffff;
  light.light.intensity = 1.0;
  Mat34.rotationY(light.transform, Math.PI / 4);
  Mat34.rotationX(light.transform, -Math.PI / 3);
  scene.add(light);

  // Create a plane
  const planeModel = makePlane(10);
  const planeActor = new Actor({ type: ACTOR_TYPE.MODEL, model: planeModel, material: 0x404040 });
  scene.add(planeActor);

  // Create a rotating cube
  const cubeModel = makeCube(1);
  const cubeActor = new Actor({ type: ACTOR_TYPE.MODEL, model: cubeModel, material: 0x0077ff });
  Mat34.translation(cubeActor.transform, 0, 0.5, 0);
  scene.add(cubeActor);

  // Animation loop (optional)
  const tick = (t) => {
    Mat34.rotationY(cubeActor.transform, t);
    Mat34.translation(cubeActor.transform, 0, 0.5 + Math.sin(t * 2) * 0.2, 0);
  };

  return { renderer, scene, camera: cam, color, depth, tick };
}

Physics Actors

While WBrenderer is primarily a visual engine, its dynamic Actor properties make it easy to integrate with a physics simulation. You can attach state like velocity and mass directly to actors and process them in your update loop.

Physical State

Initialize physical properties on your actors. Use Vec3 for vector state to maintain performance.

import { Vec3 } from 'wbrenderer';

const car = new Actor({ type: ACTOR_TYPE.MODEL, model: carModel });
car.velocity = Vec3.create(0, 0, 0);
car.mass = 1500;
car.isStatic = false;

Integration Loop

Perform physics calculations before rendering. Use walkTree to update all dynamic objects in the scene.

function physicsStep(scene, dt) {
    walkTree(scene, (actor) => {
        if (actor.isStatic || !actor.velocity) return;

        // 1. Apply Forces (e.g. Gravity)
        actor.velocity[1] -= 9.8 * dt;

        // 2. Integrate Position
        const m = actor.transform;
        m[9]  += actor.velocity[0] * dt;
        m[10] += actor.velocity[1] * dt;
        m[11] += actor.velocity[2] * dt;

        // 3. Simple Collision (Floor)
        if (m[10] < 0) {
            m[10] = 0;
            actor.velocity[1] *= -0.5; // Bounce with energy loss
        }
    });
}

Collision Proxies

You can use invisible actors as collision shapes. This is common when loading .ACT files that contain simplified geometry meant specifically for the physics engine.

walkTree(carRoot, (actor) => {
    // Identify collision actors by name
    if (actor.name.includes('BOUNDS')) {
        actor.isCollisionProxy = true;
        
        // Set type to NONE so it doesn't render, but stays in the tree
        actor.type = ACTOR_TYPE.NONE;
    }
});
link.setAttribute('data-text', link.textContent); } const text = link.getAttribute('data-text'); if (!query) { link.textContent = text; li.style.display = ''; hasVisible = true; } else { const index = text.toLowerCase().indexOf(query); if (index !== -1) { const before = text.slice(0, index); const middle = text.slice(index, index + query.length); const after = text.slice(index + query.length); link.innerHTML = `${before}${middle}${after}`; li.style.display = ''; hasVisible = true; } else { li.style.display = 'none'; } } }); group.style.display = hasVisible ? '' : 'none'; list.style.display = hasVisible ? '' : 'none'; } }); });