// A capsule, built as a Shade `Geometry`.
//
// meep 3 ships seven primitive generators (box, plane, polyhedron, octahedron,
// torus, cylinder, torus knot) and a capsule is not one of them, so the demos
// that draw capsule-shaped bodies carry this local builder. Same convention as
// the physics `CapsuleShape3D` it usually accompanies: `radius` is the cap
// radius, `height` is the length of the cylindrical middle (so the total tip to
// tip length is `height + 2 * radius`), and the axis is Y.
import { Attribute } from "@woosh/meep-engine/src/shade/renderer/geometry/Attribute.js";
import { Geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/Geometry.js";
import { StandardAttributes } from "@woosh/meep-engine/src/shade/renderer/geometry/StandardAttributes.js";
/**
* @param {number} [radius] cap radius
* @param {number} [height] length of the cylindrical middle section
* @param {number} [capSegments] rings per hemisphere
* @param {number} [radialSegments] segments around the axis
* @returns {Geometry}
*/
export function make_capsule_geometry(radius = 0.5, height = 1, capSegments = 8, radialSegments = 16) {
const positions = [];
const normals = [];
const uvs = [];
const indices = [];
const halfHeight = height / 2;
// Rings from the top pole to the bottom pole. The two hemispheres are swept
// by polar angle; the middle is a single pair of rings offset along Y, so
// the seam between cap and cylinder shares its vertices' normals.
const rings = [];
for (let i = 0; i <= capSegments; i++) {
const v = (i / capSegments) * (Math.PI / 2); // 0 at the pole, PI/2 at the equator
rings.push({ y: halfHeight + Math.cos(v) * radius, r: Math.sin(v) * radius, ny: Math.cos(v), offset: halfHeight });
}
for (let i = 0; i <= capSegments; i++) {
const v = (Math.PI / 2) + (i / capSegments) * (Math.PI / 2);
rings.push({ y: -halfHeight + Math.cos(v) * radius, r: Math.sin(v) * radius, ny: Math.cos(v), offset: -halfHeight });
}
const totalLength = height + 2 * radius;
for (let ri = 0; ri < rings.length; ri++) {
const ring = rings[ri];
for (let s = 0; s <= radialSegments; s++) {
const u = s / radialSegments;
const theta = u * Math.PI * 2;
const cos = Math.cos(theta);
const sin = Math.sin(theta);
positions.push(ring.r * cos, ring.y, ring.r * sin);
// The normal of a capsule is the normal of the sphere its point
// belongs to, which is the point taken relative to that cap's centre.
const nx = ring.r * cos;
const nz = ring.r * sin;
const ny = ring.ny * radius;
const len = Math.hypot(nx, ny, nz) || 1;
normals.push(nx / len, ny / len, nz / len);
uvs.push(u, (ring.y + totalLength / 2) / totalLength);
}
}
const perRing = radialSegments + 1;
for (let ri = 0; ri < rings.length - 1; ri++) {
for (let s = 0; s < radialSegments; s++) {
const a = ri * perRing + s;
const b = a + 1;
const c = a + perRing;
const d = c + 1;
// Counter-clockwise seen from outside, which is what the renderer
// treats as the front face — wind these the other way round and the
// capsule draws as a hollow shell lit from the inside.
indices.push(a, b, c);
indices.push(b, d, c);
}
}
const geometry = new Geometry();
geometry.name = "Capsule";
geometry.index = Attribute.from(new Uint32Array(indices), 1, StandardAttributes.Index);
geometry.addAttribute(Attribute.from(new Float32Array(positions), 3, StandardAttributes.Position));
geometry.addAttribute(Attribute.from(new Float32Array(normals), 3, StandardAttributes.Normal));
geometry.addAttribute(Attribute.from(new Float32Array(uvs), 2, StandardAttributes.TextureCoordinates0));
return geometry;
}
// Overlap query probe — a Meep example for PhysicsSystem.overlap.
//
// A semi-transparent capsule follows the cursor and slowly spins. Every frame
// it asks the physics world a single question — "which bodies do I overlap,
// right now, at this position and orientation?" — and recolours the answer.
// Nothing is simulated against the probe; `overlap` is a read-only query, so
// the capsule glides through the field like a flashlight beam, not a bulldozer.
//
// The capsule is deliberately horizontal and always turning: the overlap test
// runs against the shape at its true world orientation, so this is also a
// demonstration that the query handles arbitrary rotations, not just
// axis-aligned boxes.
//
// Sections:
// §1 Tuning constants
// §2 Engine bootstrap physics + rendering systems, fixed camera
// §3 Materials the palette, and how a repaint reaches the GPU
// §4 The ground one Static squashed box
// §5 The body field a grid of Dynamic bodies at rest, each
// remembering which base material is its own
// §6 The probe a non-physical capsule: a mesh + a query shape
// §7 Per-frame: move, spin, overlap, recolour
// §8 HUD
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import { Light } from "@woosh/meep-engine/src/engine/graphics/ecs/light/Light.js";
import { LightType } from "@woosh/meep-engine/src/engine/graphics/ecs/light/LightType.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { t64_announce_change } from "@woosh/meep-engine/src/engine/ecs/transform/t64_announce_change.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import {
shaded_geometry_announce_change
} from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/shaded_geometry_announce_change.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
import { make_box_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
import {
make_octahedron_geometry
} from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_octahedron_geometry.js";
import {
meshlet_geometry_build_from_geometry
} from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
import { StandardShadeMaterial } from "@woosh/meep-engine/src/shade/renderer/material/StandardShadeMaterial.js";
import { TransparencyMode } from "@woosh/meep-engine/src/shade/renderer/material/TransparencyMode.js";
import { Color } from "@woosh/meep-engine/src/core/color/Color.js";
// Shade ships seven primitive generators and a capsule is not one of them, so
// the capsule-shaped bodies carry a local builder (src/lib/make_capsule_geometry.js).
import { make_capsule_geometry } from "./lib/make_capsule_geometry.js";
import { PhysicsSystem } from "@woosh/meep-engine/src/engine/physics/ecs/PhysicsSystem.js";
import { ColliderObserverSystem } from "@woosh/meep-engine/src/engine/physics/ecs/ColliderObserverSystem.js";
import { RigidBody } from "@woosh/meep-engine/src/engine/physics/ecs/RigidBody.js";
import { Collider } from "@woosh/meep-engine/src/engine/physics/ecs/Collider.js";
import { BodyKind } from "@woosh/meep-engine/src/engine/physics/ecs/BodyKind.js";
import { BoxShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/BoxShape3D.js";
import { CapsuleShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/CapsuleShape3D.js";
import { SphereShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/SphereShape3D.js";
import Vector2 from "@woosh/meep-engine/src/core/geom/Vector2.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
import Quaternion from "@woosh/meep-engine/src/core/geom/Quaternion.js";
import { randomFloatBetween } from "@woosh/meep-engine/src/core/math/random/randomFloatBetween.js";
import {
plane3_compute_ray_intersection
} from "@woosh/meep-engine/src/core/geom/3d/plane/plane3_compute_ray_intersection.js";
// ─── §1 Tuning constants ────────────────────────────────────────────────────
// Ground: one squashed box. Its top face sits at y = 0.
const GROUND_WIDTH = 22;
const GROUND_HEIGHT = 1.5;
const GROUND_DEPTH = 22;
// Body field: a COLS × COLS grid spanning ±FIELD_HALF on the platform.
const FIELD_COLS = 7;
const FIELD_HALF = 8;
// The probe capsule. Radius is generous so the (horizontal) capsule spans the
// full height of the resting bodies — overlap then reads as a clean 2D sweep,
// driven by the capsule's position and spin.
const PROBE_RADIUS = 1.0;
const PROBE_LENGTH = 4.0; // cylinder section, excluding the caps
const PROBE_Y = 1.1; // height of the probe's centre above the ground
const PROBE_SPIN_RATE = 0.6; // rad/s about the vertical axis
const PROBE_CLAMP = 11; // keep the probe over (and just past) the platform
const PROBE_ALPHA = 0.4; // a ghost — you should see the bodies through it
const HIGHLIGHT_COLOR = "#4ef0a8"; // overlapping → meep green
const HIGHLIGHT_EMISSIVE = "#1a5c3c"; // a little glow so it really pops
// Muted base palette for the field — chosen to sit back so the highlight reads.
const BASE_COLORS = ["#3b4759", "#45526a", "#515f74", "#5b6b80", "#6a5d7a", "#4a6470"];
// ─── §2 Engine bootstrap ────────────────────────────────────────────────────
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
config.addSystem(new ShadedGeometrySystem(engine.graphics, EngineHarness.shadeScene(engine)));
const physics = new PhysicsSystem();
config.addSystem(physics);
config.addSystem(new ColliderObserverSystem(physics));
},
});
// `cameraController: false` — the camera is fixed. We never wire orbit / pan
// input, so the only thing the user moves is the cursor (and thus the probe).
await EngineHarness.buildBasics({
engine,
enableTerrain: false,
enableWater: false,
enableLights: true,
enableShadows: true,
shadowmapResolution: 2048,
focus: new Vector3(0, 0.5, 0),
distance: 30,
pitch: 0.95,
yaw: 0.5,
cameraFarDistance: 200,
cameraController: false,
showFps: false,
});
// Exposure and key light, the same in every demo on this site. Shade's automatic
// exposure is off: these scenes are lit to be looked at, not adapted to, and an
// adapting frame makes two screenshots of the same demo disagree. That leaves
// the light doing the work - and a directional light's intensity is read as lux,
// where the harness's default of 0.9 is a value from the three.js era that only
// ever looked lit because auto-exposure opened up to meet it.
engine.graphics.renderer.feature_automatic_exposure_enabled = false;
engine.entityManager.dataset.traverseEntities([Light], (light) => {
if (light.type.getValue() === LightType.DIRECTION) {
light.intensity.set(6);
}
});
const ecd = engine.entityManager.dataset;
const physics = engine.entityManager.getSystem(PhysicsSystem);
const scene = EngineHarness.shadeScene(engine);
// ─── §3 Materials ───────────────────────────────────────────────────────────
//
// Shade's PBR material. Hex colours below are sRGB (what a designer types) and
// the renderer works in linear light, so decode once here.
function standardMaterial(hexColor, roughness, metalness) {
const material = new StandardShadeMaterial();
material.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse(hexColor)));
material.roughness_factor = roughness;
material.metallic_factor = metalness;
return material;
}
// Six base materials, cycled across the field, plus the one everything switches
// to while the probe is touching it.
const baseMaterials = BASE_COLORS.map((hex) => standardMaterial(hex, 0.7, 0.05));
const highlightMaterial = standardMaterial(HIGHLIGHT_COLOR, 0.5, 0.05);
// With no emissive texture the renderer samples a white pixel, so `emissive_factor`
// alone is the glow.
highlightMaterial.emissive_factor.copy(Color.from_sRGB_to_linear(Color.parse(HIGHLIGHT_EMISSIVE)));
// HOW A REPAINT REACHES THE GPU — the one thing that is genuinely different
// under Shade than it was under three.
//
// Under three the demo kept one material per body and wrote `material.color`.
// That does nothing here: Shade uploads a material's constants ONCE, into a GPU
// table keyed by material id, and there is no dirty flag to make it look again.
// What a mesh row stores is the material *id*, so a body is repainted by
// pointing it at a DIFFERENT material — which means writing the component and
// saying so. A `ShadedGeometry` carries no signals of its own, so
// `shaded_geometry_announce_change` is the announcement, and
// `ShadedGeometrySystem` rewrites the entity's `meshes` row from the component
// when it hears it. That one call covers both halves the renderer cares about:
// the row's material id, and the scene's set of materials in use, from which the
// frame builds one draw bucket per material.
//
// A material that has never been on a mesh has no entry in the material table at
// all, so it is worth registering up front rather than on the first frame that
// needs it. `scene_context` is the graphics facade's handle on the renderer's
// bookkeeping for one scene.
engine.graphics.scene_context(scene).materials.obtain(highlightMaterial);
/**
* Point one body's primitive at a different material.
*
* @param {number} entity the entity holding the primitive
* @param {ShadedGeometry} sg
* @param {StandardShadeMaterial} material
*/
function repaint(entity, sg, material) {
sg.material = material;
shaded_geometry_announce_change(ecd, entity);
}
// ─── §4 The ground ──────────────────────────────────────────────────────────
let groundEntity;
{
const transform = new Transform64();
transform.setTranslation(0, -GROUND_HEIGHT / 2, 0); // top face at y = 0
const body = new RigidBody();
body.kind = BodyKind.Static;
const collider = new Collider();
collider.shape = BoxShape3D.from_size(GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH);
collider.friction = 0.8;
const geometry = meshlet_geometry_build_from_geometry(
make_box_geometry(GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH)
);
groundEntity = new Entity()
.add(transform)
.add(body)
.add(collider)
.add(ShadedGeometry.from(geometry, standardMaterial("#222a33", 0.95, 0)))
.build(ecd);
}
// ─── §5 The body field ──────────────────────────────────────────────────────
//
// A tidy grid of Dynamic bodies placed exactly at rest on the platform. They
// settle (they're already touching the ground) and sleep immediately; the probe
// never disturbs them because an overlap query applies no impulse.
//
// Every body remembers the ShadedGeometry component it draws through and which
// of the six base materials is its own, so §7 can swap it to the highlight and
// swap it back. Seven materials for forty-nine bodies, not one each: a body is
// repainted by pointing its row at another material, so bodies that look alike
// can share one instance and still light up one at a time.
const Y_AXIS = new Vector3(0, 1, 0);
const Z_AXIS = new Vector3(0, 0, 1);
const scratchQuat = new Quaternion();
// A 90° roll about Z takes the Y-axis capsule (both the mesh and CapsuleShape3D
// are built along Y) onto its side.
const LIE_DOWN = new Quaternion().fromAxisAngle(Z_AXIS, Math.PI / 2);
// entity → { sg, baseMaterial } so we can paint and restore it.
const fieldBodies = new Map();
// All the spheres are the same size, so they share one clustered geometry; the
// boxes and capsules vary, so each gets its own.
const sphereGeometry = meshlet_geometry_build_from_geometry(make_octahedron_geometry(1, 12));
// Three body archetypes. Each returns { geometry, shape, restY, lieDown };
// `restY` is the centre height that leaves the body sitting on y = 0.
function makeArchetype() {
const roll = Math.random();
if (roll < 0.34) {
// Sphere — collider radius matches the mesh. Shade has no UV sphere; a
// subdivided octahedron at detail 3 is the smooth ball.
return {
geometry: sphereGeometry,
shape: SphereShape3D.from(1),
restY: 1,
lieDown: false,
};
} else if (roll < 0.67) {
// Box of varied size.
const w = randomFloatBetween(Math.random, 1.2, 2.0);
const h = randomFloatBetween(Math.random, 1.0, 1.8);
const d = randomFloatBetween(Math.random, 1.2, 2.0);
return {
geometry: meshlet_geometry_build_from_geometry(make_box_geometry(w, h, d)),
shape: BoxShape3D.from_size(w, h, d),
restY: h / 2,
lieDown: false,
};
} else {
// Capsule, laid on its side so the field has non-axis-aligned bodies too.
const cr = randomFloatBetween(Math.random, 0.5, 0.7);
const ch = randomFloatBetween(Math.random, 0.8, 1.6);
return {
geometry: meshlet_geometry_build_from_geometry(make_capsule_geometry(cr, ch, 8, 16)),
shape: CapsuleShape3D.from(cr, ch),
restY: cr,
lieDown: true,
};
}
}
let colorCursor = 0;
const step = (2 * FIELD_HALF) / (FIELD_COLS - 1);
for (let ix = 0; ix < FIELD_COLS; ix++) {
for (let iz = 0; iz < FIELD_COLS; iz++) {
const a = makeArchetype();
const x = -FIELD_HALF + ix * step + randomFloatBetween(Math.random, -0.3, 0.3);
const z = -FIELD_HALF + iz * step + randomFloatBetween(Math.random, -0.3, 0.3);
const transform = new Transform64();
transform.setTranslation(x, a.restY, z);
// A random yaw, plus a 90° tilt for capsules so they lie horizontally.
const yaw = randomFloatBetween(Math.random, 0, Math.PI * 2);
scratchQuat.fromAxisAngle(Y_AXIS, yaw);
if (a.lieDown) {
scratchQuat.multiply(LIE_DOWN);
}
transform.setRotation(scratchQuat.x, scratchQuat.y, scratchQuat.z, scratchQuat.w);
// the matrix is not maintained for you; compose it once the pose is set
transform.updateMatrix();
const body = new RigidBody();
body.kind = BodyKind.Dynamic;
body.mass = 1;
// Rotation stays locked (default inverse inertia of 0): the bodies are a
// still target for the probe, so they keep the orientation we gave them.
const collider = new Collider();
collider.shape = a.shape;
collider.friction = 0.8;
const baseMaterial = baseMaterials[colorCursor++ % baseMaterials.length];
const sg = ShadedGeometry.from(a.geometry, baseMaterial);
const entity = new Entity()
.add(transform)
.add(body)
.add(collider)
.add(sg)
.build(ecd);
fieldBodies.set(entity, { sg, baseMaterial });
}
}
// ─── §6 The probe ───────────────────────────────────────────────────────────
//
// The probe is NOT a physics body — it carries no RigidBody and no Collider, so
// it never enters the simulation or the broadphase. It is two things:
//
// 1. a semi-transparent capsule mesh (Transform64 + ShadedGeometry) we move and
// spin for the user to see, and
// 2. a CapsuleShape3D of identical dimensions that we hand to `overlap`.
//
// They share one world pose — §7 hands `overlap` the very Transform64 the mesh is
// drawn from — so what you see is exactly what gets queried.
const probeShape = CapsuleShape3D.from(PROBE_RADIUS, PROBE_LENGTH);
const probeTransform = new Transform64();
probeTransform.setTranslation(0, PROBE_Y, 0);
/** The probe's entity — what §7 announces its per-frame pose against. @type {number} */
let probeEntity = -1;
{
const geometry = meshlet_geometry_build_from_geometry(
make_capsule_geometry(PROBE_RADIUS, PROBE_LENGTH, 10, 22)
);
// Blended transparency: the mode says how to composite, and the alpha lives
// in the diffuse colour's fourth channel. This is the only transparent
// material in the scene, and it is what pulls Shade's order-independent
// transparency passes into the frame — a one-off pipeline compile of a few
// seconds on a cold shader cache, paid on the first frame that draws it.
//
// (There is no `depthWrite: false` to set and no CastShadow flag to clear:
// the transparent pass composites without writing depth, and shadow casting
// is no longer a per-primitive flag.)
const material = standardMaterial(HIGHLIGHT_COLOR, 0.4, 0);
material.transparency_mode = TransparencyMode.Transparent;
material.diffuse_color.w = PROBE_ALPHA;
probeEntity = new Entity()
.add(probeTransform)
.add(ShadedGeometry.from(geometry, material))
.build(ecd);
}
// ─── §7 Per-frame: move, spin, overlap, recolour ────────────────────────────
// Scratch + state reused every frame (zero per-frame allocation).
const ndc = new Vector2();
const rayOrigin = new Vector3();
const rayDirection = new Vector3();
const planeHit = new Vector3();
const probeQuat = new Quaternion();
const overlapIds = new Uint32Array(64);
// Overlap state, double-buffered so we only touch mesh rows on a transition.
let highlighted = new Set();
let nextHighlighted = new Set();
// Skip the ground (it's a body too, and the probe rides just above it).
const overlapFilter = (entity) => entity !== groundEntity;
// The pointer reports (0, 0) until it has actually moved, which would park the
// probe in a corner of the platform before the user has touched anything. Hold
// it over the middle of the field until the first real move.
let pointerLive = false;
engine.devices.pointer.on.move.addOne(() => {
pointerLive = true;
});
let spinAngle = 0;
let lastFrameMs = performance.now();
let fpsWindow = 0, fpsFrames = 0;
const fpsEl = document.getElementById("fps");
const countEl = document.getElementById("count");
engine.graphics.on.postRender.add(() => {
const nowMs = performance.now();
const dt = (nowMs - lastFrameMs) / 1000;
lastFrameMs = nowMs;
// — Orientation: horizontal capsule, spun about the vertical axis —
spinAngle += PROBE_SPIN_RATE * dt;
probeQuat.fromAxisAngle(Y_AXIS, spinAngle).multiply(LIE_DOWN);
probeTransform.setRotation(probeQuat.x, probeQuat.y, probeQuat.z, probeQuat.w);
// — Position: cursor ray → horizontal plane at PROBE_Y —
// engine.devices.pointer already tracks the live cursor position for us;
// normalize it to clip space and project it into the world.
if (pointerLive) {
engine.graphics.normalizeViewportPoint(engine.devices.pointer.position, ndc);
engine.graphics.viewportProjectionRay(ndc.x, ndc.y, rayOrigin, rayDirection);
const hit = plane3_compute_ray_intersection(
planeHit,
rayOrigin.x, rayOrigin.y, rayOrigin.z,
rayDirection.x, rayDirection.y, rayDirection.z,
0, 1, 0, -PROBE_Y, // plane y = PROBE_Y (n·x + dist = 0)
);
if (hit) {
const px = Math.max(-PROBE_CLAMP, Math.min(PROBE_CLAMP, planeHit.x));
const pz = Math.max(-PROBE_CLAMP, Math.min(PROBE_CLAMP, planeHit.z));
probeTransform.setTranslation(px, PROBE_Y, pz);
}
}
// The pose is complete: compose the matrix the rotation left stale, then
// announce the write - a Transform64 has no signals, so the capsule would
// otherwise stay where it was first placed.
probeTransform.updateMatrix();
t64_announce_change(ecd, probeEntity);
// — The query —
// Position and rotation come straight off the probe's own Transform64, which
// is what the mesh is drawn from.
const count = physics.overlap(
probeShape,
probeTransform.translation,
probeTransform.rotation,
overlapIds, 0,
overlapFilter,
);
// — Recolour: paint the new set, restore bodies that left it —
nextHighlighted.clear();
for (let i = 0; i < count; i++) {
const entity = physics.entityOf(overlapIds[i]);
const record = fieldBodies.get(entity);
if (record === undefined) continue; // ground / probe / stale id
nextHighlighted.add(entity);
if (!highlighted.has(entity)) {
repaint(entity, record.sg, highlightMaterial);
}
}
for (const entity of highlighted) {
if (!nextHighlighted.has(entity)) {
const record = fieldBodies.get(entity);
if (record !== undefined) {
repaint(entity, record.sg, record.baseMaterial);
}
}
}
const tmp = highlighted;
highlighted = nextHighlighted;
nextHighlighted = tmp;
// ─── §8 HUD ─────────────────────────────────────────────────────────────
if (countEl) countEl.textContent = String(highlighted.size);
fpsWindow += dt;
fpsFrames++;
if (fpsWindow >= 0.5) {
if (fpsEl) fpsEl.textContent = (fpsFrames / fpsWindow).toFixed(0);
fpsWindow = 0;
fpsFrames = 0;
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Overlap query probe · Meep</title>
<meta name="robots" content="noindex">
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
overflow: hidden;
background: #07090c;
color: #e6edf3;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
cursor: crosshair;
}
.panel {
position: fixed;
z-index: 100;
background: rgba(7, 9, 12, 0.72);
border: 1px solid #1f2731;
border-radius: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 12px 32px rgba(0,0,0,0.4);
}
.hud {
top: 1rem; left: 1rem;
padding: 0.8rem 1rem;
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 0.82rem;
line-height: 1.7;
color: #9aa5b1;
min-width: 180px;
}
.hud .label {
color: #6b7785;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.65rem;
margin-right: 0.5rem;
}
.hud .value { color: #4ef0a8; }
.legend {
bottom: 1rem; left: 1rem;
padding: 0.8rem 1rem;
font-size: 0.82rem;
line-height: 1.55;
max-width: 420px;
color: #9aa5b1;
}
.legend strong { color: #e6edf3; }
</style>
</head>
<body>
<div class="panel hud">
<div><span class="label">fps</span><span class="value" id="fps">--</span></div>
<div><span class="label">overlapping</span><span class="value" id="count">0</span></div>
</div>
<div class="panel legend">
<strong>Move your cursor.</strong> A semi-transparent capsule follows it and
slowly spins, running <code>PhysicsSystem.overlap</code> every frame — every
body it intersects, at its true (non-axis-aligned) orientation, lights up.
The camera is fixed; the probe is a query, not a collider, so it passes
through without pushing anything.
</div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
{
"title": "Overlap query probe",
"description": "A cursor-driven, slowly-spinning capsule runs PhysicsSystem.overlap every frame; bodies it intersects light up — arbitrary orientation, no collision.",
"category": "Physics",
"status": "live",
"order": 2,
"tags": ["physics", "overlap", "query", "collision", "ecs"],
"sourceHint": "examples-src/overlap-query/",
"demoUrl": "/examples/overlap-query/demo.html",
"defaultFile": "src/main.js"
}
import { defineConfig } from "vite";
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
import strip from "@rollup/plugin-strip";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
// Vite's dependency pre-bundling rewrites the engine's own asset imports and
// loses their URLs, so a dev server ends up serving ".../deps/undefined" for
// the renderer's data files. The engine is source-only ESM and needs no
// pre-bundling - but its CommonJS dependencies do, or the import of them from inside
// the excluded engine finds no default export.
optimizeDeps: {
exclude: ["@woosh/meep-engine"],
include: ["dat.gui", "opentype.js"],
},
plugins: [
{
// Copy the committed source thumbnail into the generated gallery folder.
// public/examples/<id>/ is build output (gitignored); thumbnail.png is
// kept in source here and copied through on every build so the gallery
// (src/data/examples.ts) can resolve it.
name: "copy-thumbnail",
apply: "build",
closeBundle() {
const thumb = resolve(__dirname, "thumbnail.png");
const dst = resolve(__dirname, "../../public/examples/overlap-query");
if (existsSync(thumb)) {
mkdirSync(dst, { recursive: true });
copyFileSync(thumb, resolve(dst, "thumbnail.png"));
}
},
},
],
base: "./",
build: {
outDir: resolve(__dirname, "../../public/examples/overlap-query"),
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, "demo.html"),
plugins: [
{
// this will remove all assert statements from the production build
...strip(),
apply: 'build'
}
],
},
target: "es2022",
},
});