Skip to content

Commit ccedd2c

Browse files
committed
fix(reconciler): fix stale closure on event callbacks
1 parent 4116a0d commit ccedd2c

File tree

2 files changed

+68
-25
lines changed

2 files changed

+68
-25
lines changed

packages/library/src/components/hosts/GuiHost.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ import { TransformKeysMap } from '@constants';
55
import ObjectUtils from '@utils/ObjectUtils';
66
import guiConstructors from '../../_generated/babylon.gui.constructors';
77
import type { Vector2WithInfo, GUI3DManager, Control, Container, Button3D, Grid } from '@babylonjs/gui';
8-
import type { Observable } from '@babylonjs/core';
8+
import type { Observable, EventState } from '@babylonjs/core';
99

1010
// required for git hook (otherwise it can't resolve the augmented JSXElements)
1111
import '../../index';
1212
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
1313

14-
function handleEvents(props: GuiHostProps, element: any) {
14+
type AugmentedGuiElement = BabylonEntity<Control & GuiTriggerable> & {
15+
_triggerCallbacks: Partial<Record<keyof typeof GuiTriggers, Parameters<Observable<Vector2WithInfo>['add']>[0]>>;
16+
};
17+
18+
function handleEvents(element: AugmentedGuiElement) {
1519
Object.entries(GuiTriggers).forEach(([_key, observableName]) => {
16-
const key = _key as keyof GuiTriggerable;
17-
//TODO: handle addOnce
18-
const handlerFn = props[key] as Parameters<Observable<Vector2WithInfo>['add']>[0];
19-
if (handlerFn) {
20-
element[observableName].add(handlerFn);
20+
const key = _key as keyof typeof GuiTriggers;
21+
if (element._triggerCallbacks[key]) {
22+
(element as any)[observableName].add((evt: Vector2WithInfo, eventState: EventState) => element._triggerCallbacks[key]?.(evt, eventState));
2123
}
2224
});
2325
}
@@ -29,7 +31,7 @@ export type Params = {
2931

3032
type GuiComponent = Pick<Container, 'addControl' | 'removeControl'> & GuiTriggerable;
3133

32-
const excludedProps = ['children', 'onCreate', 'cloneFrom', 'propertiesFrom', 'kind', 'createFullscreenUI', 'createForMesh', 'ref', ...Object.keys(GuiTriggers)];
34+
const excludedProps = ['children', 'onCreate', 'cloneFrom', 'propertiesFrom', 'kind', 'createFullscreenUI', 'createForMesh', 'ref'];
3335

3436
export class GuiHost {
3537
static createInstance(type: string, Class: any, props: GuiHostProps, rootContainer: RootContainer, cloneFn?: Function, params?: Params) {
@@ -86,12 +88,21 @@ export class GuiHost {
8688
// execute custom code as soon the object is created
8789
props.onCreate?.(element);
8890

89-
handleEvents(props, element);
91+
element._triggerCallbacks = Object.keys(GuiTriggers).reduce(
92+
(acc, trigger) => {
93+
const key = trigger as keyof typeof GuiTriggers;
94+
if (element[key]) acc[key] = element[key];
95+
return acc;
96+
},
97+
{} as Partial<Record<keyof typeof GuiTriggers, Parameters<Observable<Vector2WithInfo>['add']>[0]>>,
98+
);
99+
100+
handleEvents(element);
90101

91102
element.handlers = {
92103
addChild: GuiHost.addChild,
93104
removeChild: GuiHost.removeChild,
94-
// add here your custom handlers
105+
commitUpdate: GuiHost.commitUpdate,
95106
};
96107

97108
return element;
@@ -106,7 +117,7 @@ export class GuiHost {
106117
} else {
107118
// @ts-expect-error - type is not a part of Grid
108119
if (parentInstance.type === 'row' || parentInstance.type === 'column') {
109-
(parentInstance as Grid).addControl?.(child, 0, 0);
120+
(parentInstance as unknown as Grid).addControl?.(child, 0, 0);
110121
} else {
111122
// ensure that addControl function exists (parentInstance could be transformNode)
112123
parentInstance.addControl?.(child);
@@ -124,5 +135,12 @@ export class GuiHost {
124135
return {};
125136
}
126137

127-
static commitUpdate(instance: BabylonEntity<Control>, updatePayload: UpdatePayload): void {}
138+
static commitUpdate(instance: AugmentedGuiElement, updatePayload: UpdatePayload): void {
139+
Object.keys(GuiTriggers).forEach(trigger => {
140+
const key = trigger as keyof typeof GuiTriggers;
141+
if (key in updatePayload) {
142+
instance._triggerCallbacks[key] = updatePayload[key] as Parameters<Observable<Vector2WithInfo>['add']>[0];
143+
}
144+
});
145+
}
128146
}

packages/library/src/components/hosts/MeshHost.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,27 @@ import { Host } from './Host';
33
import type { ActionEvent, Mesh, Scene, HighlightLayer } from '@babylonjs/core';
44
import { ActionManager } from '@babylonjs/core/Actions/actionManager.js';
55
import { ExecuteCodeAction } from '@babylonjs/core/Actions/directActions.js';
6-
import { type Triggerable, MeshTriggers, type MeshProps, type CoreHostProps } from '@props';
6+
import { MeshTriggers, type MeshProps, type CoreHostProps } from '@props';
77
import { isInstanceOf } from '@dvmstudios/reactylon-common';
88

9-
function handleEvents(props: CoreHostProps<MeshProps>, scene: Scene) {
10-
const isAtLeastOneTrigger = Object.keys(MeshTriggers).some(trigger => props[trigger as keyof Triggerable]);
9+
type AugmentedMesh = BabylonEntity<MeshProps & Mesh> & {
10+
_triggerCallbacks: Partial<Record<keyof typeof MeshTriggers, (evt: ActionEvent) => void>>;
11+
};
12+
13+
function handleEvents(scene: Scene, instance: AugmentedMesh) {
14+
const isAtLeastOneTrigger = Object.keys(MeshTriggers).some(trigger => instance._triggerCallbacks[trigger as keyof typeof MeshTriggers]);
1115
if (isAtLeastOneTrigger) {
1216
const actionManager = new ActionManager(scene);
1317
Object.entries(MeshTriggers).forEach(([_key, name]) => {
14-
const key = _key as keyof Triggerable;
15-
const handlerFn = props[key] as (evt: ActionEvent) => void;
16-
if (handlerFn) {
17-
const { intersectionMeshId } = props;
18+
const key = _key as keyof typeof MeshTriggers;
19+
if (instance._triggerCallbacks[key]) {
1820
actionManager.registerAction(
1921
new ExecuteCodeAction(
2022
{
2123
trigger: name,
22-
parameter: intersectionMeshId ? scene.getMeshById(intersectionMeshId) : undefined,
24+
parameter: instance.intersectionMeshId ? scene.getMeshById(instance.intersectionMeshId) : undefined,
2325
},
24-
handlerFn,
26+
evt => instance._triggerCallbacks[key]?.(evt),
2527
),
2628
);
2729
}
@@ -31,8 +33,6 @@ function handleEvents(props: CoreHostProps<MeshProps>, scene: Scene) {
3133
return null;
3234
}
3335

34-
type AugmentedMesh = BabylonEntity<MeshProps & Mesh>;
35-
3636
export class MeshHost {
3737
static createInstance(type: string, Class: any, props: CoreHostProps<MeshProps>, rootContainer: RootContainer) {
3838
let cloneFn = undefined;
@@ -58,12 +58,21 @@ export class MeshHost {
5858
const original = scene.getMeshById(instanceFrom) as Mesh;
5959
original.physicsBody?.clone(element);
6060
}
61-
element.actionManager = handleEvents(props, scene);
61+
62+
element._triggerCallbacks = Object.keys(MeshTriggers).reduce(
63+
(acc, trigger) => {
64+
const key = trigger as keyof typeof MeshTriggers;
65+
if (element[key]) acc[key] = element[key];
66+
return acc;
67+
},
68+
{} as Partial<Record<keyof typeof MeshTriggers, (evt: ActionEvent) => void>>,
69+
);
70+
71+
element.actionManager = handleEvents(scene, element);
6272
element.handlers = {
6373
addChild: MeshHost.addChild,
6474
commitUpdate: MeshHost.commitUpdate,
6575
removeChild: MeshHost.removeChild,
66-
// add here your custom handlers for meshes
6776
};
6877
return element;
6978
}
@@ -86,6 +95,22 @@ export class MeshHost {
8695
}
8796

8897
static commitUpdate(instance: AugmentedMesh, updatePayload: UpdatePayload): void {
98+
// update trigger callbacks
99+
Object.keys(MeshTriggers).forEach(trigger => {
100+
const key = trigger as keyof typeof MeshTriggers;
101+
if (key in updatePayload) {
102+
instance._triggerCallbacks[key] = updatePayload[key] as (evt: ActionEvent) => void;
103+
}
104+
});
105+
106+
const structuralChange = updatePayload.intersectionMeshId !== instance.intersectionMeshId;
107+
if (structuralChange) {
108+
instance.intersectionMeshId = updatePayload.intersectionMeshId as string;
109+
instance.actionManager?.dispose();
110+
instance.actionManager = handleEvents(instance.getScene(), instance);
111+
}
112+
113+
// highlight layer
89114
const parent = instance.highlightLayerParent as BabylonEntity<HighlightLayer>;
90115
if (parent && isInstanceOf(parent, 'HighlightLayer')) {
91116
const highlightLayer = updatePayload.highlightLayer as MeshProps['highlightLayer'];

0 commit comments

Comments
 (0)