Skip to content
Merged
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
106 changes: 97 additions & 9 deletions api/physics.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,19 +437,41 @@ export const flockPhysics = {
name.includes("__") ? name.split("__")[0] : name.split("_")[0];

const groupName = getGroupRoot(meshName);
const getAllGuiControls = () => {
const root =
flock.scene?.UITexture?._rootContainer ??
flock.scene?.UITexture?.rootContainer;
if (!root) return [];
if (typeof root.getDescendants === "function") {
return root.getDescendants(false);
}
const all = [];
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
const children = node?._children ?? node?.children ?? [];
for (const child of children) {
all.push(child);
stack.push(child);
}
}
return all;
};

if (!flock.scene) {
if (!flock.pendingTriggers.has(groupName))
flock.pendingTriggers.set(groupName, []);
flock.pendingTriggers.get(groupName).push({ trigger, callback, mode });
flock.pendingTriggers
.get(groupName)
.push({ meshName, trigger, callback, mode, applyToGroup });
return;
}

if (applyToGroup) {
let matchingButtons = [];
if (flock.scene.UITexture) {
matchingButtons = flock.scene.UITexture._rootContainer._children.filter(
(control) => control.name && getGroupRoot(control.name) === groupName,
matchingButtons = getAllGuiControls().filter(
(control) => control?.name && getGroupRoot(control.name) === groupName,
);
}
const matching = flock.scene.meshes.filter(
Expand Down Expand Up @@ -478,15 +500,15 @@ export const flockPhysics = {
}
if (!flock.pendingTriggers.has(groupName))
flock.pendingTriggers.set(groupName, []);
flock.pendingTriggers.get(groupName).push({ trigger, callback, mode });
flock.pendingTriggers
.get(groupName)
.push({ meshName, trigger, callback, mode, applyToGroup });
return;
}

let guiButton = null;
if (flock.scene.UITexture) {
guiButton = flock.scene.UITexture._rootContainer._children.find(
(c) => c.name === meshName,
);
guiButton = flock.scene.UITexture.getControlByName?.(meshName) ?? null;
}

const tryNow =
Expand All @@ -497,7 +519,9 @@ export const flockPhysics = {
if (!tryNow) {
if (!flock.pendingTriggers.has(groupName))
flock.pendingTriggers.set(groupName, []);
flock.pendingTriggers.get(groupName).push({ trigger, callback, mode });
flock.pendingTriggers
.get(groupName)
.push({ meshName, trigger, callback, mode, applyToGroup });
return;
}

Expand Down Expand Up @@ -606,7 +630,71 @@ export const flockPhysics = {
});
});
},
onIntersect(meshName, otherMeshName, { trigger, callback }) {
onIntersect(
meshName,
otherMeshName,
{ trigger, callback, applyToGroupOther = false } = {},
) {
const getGroupRoot = (name) =>
name.includes("__") ? name.split("__")[0] : name.split("_")[0];
const resolveCanonicalGroupName = (rawName) => {
const scene = flock.scene;
const exact = scene?.getMeshByName?.(rawName);
if (exact?.name) return getGroupRoot(exact.name);

let normalized = rawName.includes("__") ? rawName.split("__")[0] : rawName;
normalized = normalized.replace(/[^a-zA-Z0-9._-]/g, "");

if (normalized && normalized !== rawName) {
if (
scene?.getMeshByName?.(normalized) ||
flock.modelReadyPromises.has(normalized)
) {
return getGroupRoot(normalized);
}
}

return getGroupRoot(rawName);
};

if (applyToGroupOther) {
const groupName = resolveCanonicalGroupName(otherMeshName);

if (!flock.pendingIntersections.has(groupName)) {
flock.pendingIntersections.set(groupName, []);
}

const pendingEntry = {
meshName,
trigger,
callback,
registeredOthers: new Set(),
};
flock.pendingIntersections.get(groupName).push(pendingEntry);

const registerForOther = (name) => {
if (name === meshName || pendingEntry.registeredOthers.has(name)) {
return Promise.resolve();
}
pendingEntry.registeredOthers.add(name);
return flock.onIntersect(meshName, name, {
trigger,
callback,
applyToGroupOther: false,
});
};

if (flock.scene) {
const matching = flock.scene.meshes.filter(
(m) => getGroupRoot(m.name) === groupName,
);
const matchingNames = [...new Set(matching.map((m) => m.name))];
return Promise.all(matchingNames.map((name) => registerForOther(name)));
}

return;
}

return new Promise((resolve) => {
flock.whenModelReady(meshName, async function (mesh) {
if (!mesh) {
Expand Down
89 changes: 66 additions & 23 deletions flock.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const flock = {
modelReadyPromises: new Map(),
pendingMeshCreations: 0,
pendingTriggers: new Map(),
pendingIntersections: new Map(),
_nameRegistry: new Map(),
_animationFileCache: {},
getModelDisplayName,
Expand Down Expand Up @@ -1833,6 +1834,7 @@ export const flock = {
flock.geometryCache = {};
flock.materialCache = {};
flock.pendingTriggers = new Map();
flock.pendingIntersections = new Map();
flock._nameRegistry = new Map();
flock._animationFileCache = {};
flock.ground = null;
Expand Down Expand Up @@ -1878,6 +1880,7 @@ export const flock = {
flock.originalModelTransformations = {};
flock.geometryCache = {};
flock.pendingTriggers = new Map();
flock.pendingIntersections = new Map();
flock._nameRegistry = new Map();
flock._animationFileCache = {};
flock.materialCache = {};
Expand Down Expand Up @@ -2498,32 +2501,72 @@ export const flock = {
const getGroupRoot = (name) =>
name.includes("__") ? name.split("__")[0] : name.split("_")[0];

if (!flock.pendingTriggers.has(groupName)) return;

const triggers = flock.pendingTriggers.get(groupName);

for (const { trigger, callback, mode, applyToGroup } of triggers) {
if (applyToGroup) {
// 🔁 Reapply trigger across all matching meshes
const matching = flock.scene.meshes.filter(
(m) => getGroupRoot(m.name) === groupName,
);
for (const m of matching) {
flock.onTrigger(m.name, {
trigger,
callback,
mode,
applyToGroup: false, // prevent recursion
});
}
} else {
// ✅ Apply to just this specific mesh
flock.onTrigger(meshName, {
if (flock.pendingTriggers.has(groupName)) {
const triggers = flock.pendingTriggers.get(groupName);
const remaining = [];

for (const pending of triggers) {
const {
meshName: pendingMeshName,
trigger,
callback,
mode,
applyToGroup: false,
});
applyToGroup,
} = pending;
const targetMeshName = pendingMeshName ?? meshName;

if (applyToGroup) {
// 🔁 Reapply trigger across all matching meshes
const matching = flock.scene.meshes.filter(
(m) => getGroupRoot(m.name) === groupName,
);
for (const m of matching) {
flock.onTrigger(m.name, {
trigger,
callback,
mode,
applyToGroup: false, // prevent recursion
});
}
// Keep group-applied triggers pending for future siblings.
remaining.push(pending);
} else {
const guiControl =
flock.scene?.UITexture?.getControlByName?.(targetMeshName) ?? null;
const targetExists =
flock.scene?.getMeshByName(targetMeshName) || guiControl;

if (targetExists) {
// ✅ Apply to the original target this pending registration was created for.
flock.onTrigger(targetMeshName, {
trigger,
callback,
mode,
applyToGroup: false,
});
} else {
remaining.push(pending);
}
}
}

flock.pendingTriggers.set(groupName, remaining);
}

if (flock.pendingIntersections.has(groupName)) {
const intersections = flock.pendingIntersections.get(groupName);
for (const pending of intersections) {
if (
meshName !== pending.meshName &&
!pending.registeredOthers.has(meshName)
) {
pending.registeredOthers.add(meshName);
flock.onIntersect(pending.meshName, meshName, {
trigger: pending.trigger,
callback: pending.callback,
applyToGroupOther: false,
});
}
}
}
},
Expand Down
6 changes: 5 additions & 1 deletion generators/generators-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,20 @@ export function registerEventsGenerators(javascriptGenerator) {

const trigger = block.getFieldValue("TRIGGER");
const doCode = javascriptGenerator.statementToCode(block, "DO");
const isTopLevel = !block.getSurroundParent();

if (
trigger === "OnIntersectionEnterTrigger" ||
trigger === "OnIntersectionExitTrigger"
) {
const applyToGroupOtherLine = isTopLevel
? ",\n applyToGroupOther: true"
: "";
return `onIntersect(${modelName}, ${otherModelName}, {
trigger: "${trigger}",
callback: async function(${modelName}, ${otherModelName}) {
${doCode}
}
}${applyToGroupOtherLine}
});\n`;
} else {
console.error("Invalid trigger type for 'on_collision' block:", trigger);
Expand Down
80 changes: 80 additions & 0 deletions tests/events.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,86 @@ export function runEventsTests(flock) {

expect(count).to.equal(0);
});

it("replays pending non-group trigger on the original target mesh only", async function () {
const target = "latepick_1";
const sibling = "latepick_2";

let count = 0;
flock.onTrigger(target, {
trigger: "OnPickTrigger",
callback: () => count++,
applyToGroup: false,
});

await flock.createBox(sibling, {
width: 1,
height: 1,
depth: 1,
position: [0, 0, 0],
});
await flock.createBox(target, {
width: 1,
height: 1,
depth: 1,
position: [2, 0, 0],
});
meshIds.push(target, sibling);

const targetMesh = flock.scene.getMeshByName(target);
const siblingMesh = flock.scene.getMeshByName(sibling);
expect(targetMesh).to.exist;
expect(siblingMesh).to.exist;

siblingMesh.actionManager?.processTrigger(
flock.BABYLON.ActionManager.OnPickTrigger,
);
targetMesh.actionManager?.processTrigger(
flock.BABYLON.ActionManager.OnPickTrigger,
);

expect(count).to.equal(1);
});

it("replays pending group trigger across siblings when applyToGroup is true", async function () {
const first = "lategroup_1";
const second = "lategroup_2";

let count = 0;
flock.onTrigger(first, {
trigger: "OnPickTrigger",
callback: () => count++,
applyToGroup: true,
});

await flock.createBox(first, {
width: 1,
height: 1,
depth: 1,
position: [0, 0, 0],
});
await flock.createBox(second, {
width: 1,
height: 1,
depth: 1,
position: [2, 0, 0],
});
meshIds.push(first, second);

const mesh1 = flock.scene.getMeshByName(first);
const mesh2 = flock.scene.getMeshByName(second);
expect(mesh1).to.exist;
expect(mesh2).to.exist;

mesh1.actionManager?.processTrigger(
flock.BABYLON.ActionManager.OnPickTrigger,
);
mesh2.actionManager?.processTrigger(
flock.BABYLON.ActionManager.OnPickTrigger,
);

expect(count).to.equal(2);
});
});
});
}
Loading
Loading