Trigger colliders and Move and slide against solid colliders

- Collider property isTrigger (no blocking, still reports enter/stay/exit)
- Node 'Move and slide' with blocked x/y outputs, axis-wise resolution
- axisSeparation/axisCorrection geometry for box/box, circle/circle, box/circle
- Amber outline for trigger colliders in the viewport
- Demo mover uses Move and slide; E2E step 15b, 7 new unit tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 15:38:14 +02:00
1 parent 0a59513b74
commit 6ecb20cf02
14 files changed
+283 -12

No files matched your search

+33
View File
@@ -581,6 +581,39 @@ try {
await keyPress('Escape', 'Escape', { windowsVirtualKeyCode: 27 });
await sleep(200);
// 15b. Solid vs trigger. Put the blue crate 150 to the right of the red one (both 100 wide →
// 50 of free travel) and hold ArrowRight: the Mover's "Move and slide" parks the red
// crate flush against the blue one at world x = −100. Making the blue collider a
// trigger lets it pass through instead (the collider is then drawn amber).
const holdRight = async (ms) => {
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'ArrowRight', code: 'ArrowRight', windowsVirtualKeyCode: 39 });
await sleep(ms);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'ArrowRight', code: 'ArrowRight', windowsVirtualKeyCode: 39 });
};
const playAgainstBlue = async (trigger) => {
await evaluate(`(() => {
const scene = __gm.state.activeScene;
scene.objects.find(o => o.id === 'o2').position = { x: 0, y: 0, z: 0 };
scene.objects.find(o => o.id === 'oc2').overrides = ${trigger ? '{ isTrigger: true }' : '{}'};
__gm.state.select(null);
__gm.state.preview();
})()`);
await keyPress('Enter', 'Enter', { modifiers: 2, windowsVirtualKeyCode: 13 });
await sleep(300);
await holdRight(700);
const w = JSON.parse(await evaluate('JSON.stringify(__gm.world())'));
await keyPress('Escape', 'Escape', { windowsVirtualKeyCode: 27 });
await sleep(200);
return w.entities.find((e) => e.objectId === 'o1');
};
const blocked = await playAgainstBlue(false);
check('Move and slide stops the red crate flush against the blue crate\'s solid collider', blocked && Math.abs(blocked.x - -100) < 0.01, `world x=${blocked?.x}`);
const passed = await playAgainstBlue(true);
check('a trigger collider does not block (the crate passes through)', passed && passed.x > -100 + 20, `world x=${passed?.x}`);
await evaluate(`__gm.state.select({ kind: 'object', objectId: 'oc2' })`);
await sleep(200);
await shot('22-trigger-collider');
check('no exceptions during run', !consoleLog.some((l) => l.startsWith('EXCEPTION')), consoleLog.filter((l) => l.startsWith('EXCEPTION')).slice(0, 3).join(' | '));
ws.close();
} catch (error) {
+4 -3
View File
@@ -12,8 +12,9 @@ if (!projectId) {
/**
* "Mover" behavior in five nodes: every frame, move the owner along the normalized arrow-key
* axis, scaled by the `speed` variable per second (On update's dt feeds Move's dt input).
* Diagonals are not faster than straight moves because Axis input normalizes the vector.
* axis, scaled by the `speed` variable per second (On update's dt feeds the dt input).
* Diagonals are not faster than straight moves because Axis input normalizes the vector, and
* "Move and slide" stops the owner's solid colliders at other solid colliders.
*/
const moverGraph = {
nodes: [
@@ -21,7 +22,7 @@ const moverGraph = {
{ id: 'n-owner', type: 'Owner', x: 0, y: 120, params: {} },
{ id: 'n-axis', type: 'AxisInput', x: 0, y: 200, params: {} },
{ id: 'n-speed', type: 'GetVariable', x: 0, y: 400, params: { variable: 'speed' } },
{ id: 'n-move', type: 'Move', x: 340, y: 0, params: {} },
{ id: 'n-move', type: 'MoveAndSlide', x: 340, y: 0, params: {} },
],
edges: [
{ id: 'e1', from: { node: 'n-update', port: 'exec' }, to: { node: 'n-move', port: 'exec' } },