Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 112 additions & 3 deletions _layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -1485,7 +1485,15 @@ <h2>Asset Manager</h2>
{
kind: 'block',
type: 'set_texture_from_asset',
}
},
{
kind: 'block',
type: 'look_at',
},
{
kind: 'block',
type: 'move_towards',
}
]
},

Expand Down Expand Up @@ -1562,9 +1570,13 @@ <h2>Asset Manager</h2>
},
{
kind: 'category',
name: 'Utils',
categorystyle: 'colour_category',
name: 'Math',
categorystyle: 'math_category',
contents: [
{
kind: 'block',
type: 'get_distance',
},
{
kind: 'block',
type: 'colour_picker',
Expand Down Expand Up @@ -2849,6 +2861,44 @@ <h2>Asset Manager</h2>
mesh.rotation.set(x * (Math.PI / 180), y * (Math.PI / 180), z * (Math.PI / 180));
}
}

lookAt(target, destination) {
const targetMesh = this._getMesh(target);
const destMesh = this._getMesh(destination);
if (targetMesh && destMesh) {
targetMesh.lookAt(destMesh.getAbsolutePosition());
}
}

moveTowards(target, destination, speed) {
const targetMesh = this._getMesh(target);
const destMesh = this._getMesh(destination);
if (targetMesh && destMesh) {
const direction = destMesh.getAbsolutePosition().subtract(targetMesh.getAbsolutePosition());
const distance = direction.length();
if (distance > 0.01) {
const stepSize = speed * (this.engine.getDeltaTime() / 1000);
if (stepSize >= distance) {
// If we're close enough to reach or overshoot, just jump to the destination
// To avoid parent/local space issues, we add the remaining direction vector
targetMesh.position.addInPlace(direction);
} else {
direction.normalize();
const moveStep = direction.scale(stepSize);
targetMesh.position.addInPlace(moveStep);
}
}
}
}

getDistance(target1, target2) {
const mesh1 = this._getMesh(target1);
const mesh2 = this._getMesh(target2);
if (mesh1 && mesh2) {
return BABYLON.Vector3.Distance(mesh1.getAbsolutePosition(), mesh2.getAbsolutePosition());
}
return 0;
}
changeColor(target, color) {
let mesh = this._getMesh(target);
if (mesh) {
Expand Down Expand Up @@ -5863,6 +5913,45 @@ <h2>Asset Manager</h2>
"colour": "#5B80A5",
"tooltip": "Sets the main text of an existing popup.",
"helpUrl": ""
},
{
"type": "look_at",
"message0": "make %1 look at %2",
"args0": [
{ "type": "input_value", "name": "OBJECT", "check": ["String", "Mesh"] },
{ "type": "input_value", "name": "TARGET", "check": ["String", "Mesh"] }
],
"previousStatement": null,
"nextStatement": null,
"colour": 210,
"tooltip": "Makes one object face another object.",
"helpUrl": ""
},
{
"type": "move_towards",
"message0": "move %1 towards %2 with speed %3",
"args0": [
{ "type": "input_value", "name": "OBJECT", "check": ["String", "Mesh"] },
{ "type": "input_value", "name": "TARGET", "check": ["String", "Mesh"] },
{ "type": "input_value", "name": "SPEED", "check": "Number" }
],
"previousStatement": null,
"nextStatement": null,
"colour": 210,
"tooltip": "Moves one object towards another object at a given speed.",
"helpUrl": ""
},
{
"type": "get_distance",
"message0": "distance between %1 and %2",
"args0": [
{ "type": "input_value", "name": "OBJECT1", "check": ["String", "Mesh"] },
{ "type": "input_value", "name": "OBJECT2", "check": ["String", "Mesh"] }
],
"output": "Number",
"colour": 230,
"tooltip": "Returns the distance between two objects.",
"helpUrl": ""
}
]);

Expand Down Expand Up @@ -6401,6 +6490,26 @@ <h2>Asset Manager</h2>
return [`sceneManager.getPos${axis}(${target})`, javascript.Order.FUNCTION_CALL];
};

javascript.javascriptGenerator.forBlock['look_at'] = function (block, generator) {
const object = generator.valueToCode(block, 'OBJECT', generator.ORDER_ATOMIC) || 'null';
const target = generator.valueToCode(block, 'TARGET', generator.ORDER_ATOMIC) || 'null';
return `sceneManager.lookAt(${object}, ${target});\n`;
};

javascript.javascriptGenerator.forBlock['move_towards'] = function (block, generator) {
const object = generator.valueToCode(block, 'OBJECT', generator.ORDER_ATOMIC) || 'null';
const target = generator.valueToCode(block, 'TARGET', generator.ORDER_ATOMIC) || 'null';
const speed = generator.valueToCode(block, 'SPEED', generator.ORDER_ATOMIC) || 0;
return `sceneManager.moveTowards(${object}, ${target}, ${speed});\n`;
};

javascript.javascriptGenerator.forBlock['get_distance'] = function (block, generator) {
const object1 = generator.valueToCode(block, 'OBJECT1', generator.ORDER_ATOMIC) || 'null';
const object2 = generator.valueToCode(block, 'OBJECT2', generator.ORDER_ATOMIC) || 'null';
const code = `sceneManager.getDistance(${object1}, ${object2})`;
return [code, generator.ORDER_ATOMIC];
};

javascript.javascriptGenerator.forBlock['get_collided_object'] = function (block, generator) {
const collidedObjectVar = generator.nameDB_.getName('collided_object', Blockly.VARIABLE_CATEGORY_NAME);
return [collidedObjectVar, javascript.Order.ATOMIC];
Expand Down
107 changes: 107 additions & 0 deletions tests/stem.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const { test, expect } = require('@playwright/test');

test.describe('Engine STEM Features', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
// Handle the hero overlay
const startButton = page.locator('#start-button');
if (await startButton.isVisible()) {
await startButton.click();
}
});

test('Distance measurement and move towards work', async ({ page }) => {
// Switch to preview tab
await page.click('#preview-tab');

const consoleMessages = [];
page.on('console', msg => consoleMessages.push(msg.text()));

const testCode = `
(async () => {
const box1 = sceneManager.createBox('box1', 0, 0, 0);
const box2 = sceneManager.createBox('box2', 10, 0, 0);

// Force world matrix update to get correct absolute positions
box1.computeWorldMatrix(true);
box2.computeWorldMatrix(true);

const dist = sceneManager.getDistance('box1', 'box2');
console.log('DISTANCE_INITIAL: ' + dist);

sceneManager.everyFrame('box1', (mesh) => {
sceneManager.moveTowards('box1', 'box2', 10); // increased speed
});

// Wait a bit
await new Promise(r => setTimeout(r, 500));

// Again force update for measurement
box1.computeWorldMatrix(true);
box2.computeWorldMatrix(true);

const distMid = sceneManager.getDistance('box1', 'box2');
console.log('DISTANCE_MID: ' + distMid);

if (distMid < dist && dist > 0) {
console.log('STEM_MOVE_SUCCESS');
} else {
console.log('STEM_MOVE_FAILURE: initial=' + dist + ' mid=' + distMid);
}
})();
`;

await page.evaluate((code) => {
window.doRun(code);
}, testCode);

await expect.poll(() => consoleMessages).toContain('STEM_MOVE_SUCCESS');
});

test('Look at works', async ({ page }) => {
await page.click('#preview-tab');

const consoleMessages = [];
page.on('console', msg => consoleMessages.push(msg.text()));

const testCode = `
(async () => {
const box1 = sceneManager.createBox('box1', 0, 0, 0);
const box2 = sceneManager.createBox('box2', 10, 0, 10); // placed at an angle

box1.computeWorldMatrix(true);
box2.computeWorldMatrix(true);

const initialRot = box1.rotation.clone();
const initialQuat = box1.rotationQuaternion ? box1.rotationQuaternion.clone() : null;

sceneManager.lookAt('box1', 'box2');

const finalRot = box1.rotation;
const finalQuat = box1.rotationQuaternion;

console.log('ROTATION_INITIAL: ' + initialRot);
console.log('ROTATION_FINAL: ' + finalRot);

let changed = !finalRot.equals(initialRot);
if (finalQuat) {
if (!initialQuat || !finalQuat.equals(initialQuat)) {
changed = true;
}
}

if (changed) {
console.log('STEM_LOOKAT_SUCCESS');
} else {
console.log('STEM_LOOKAT_FAILURE');
}
})();
`;

await page.evaluate((code) => {
window.doRun(code);
}, testCode);

await expect.poll(() => consoleMessages).toContain('STEM_LOOKAT_SUCCESS');
});
});
Loading