From 3e3b7bcd7145868c605682d5bf21d4f853557249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Vivet?= Date: Wed, 13 May 2026 21:43:44 +0200 Subject: [PATCH 1/4] fix(editor): trigger hot reload when a behavior property is edited `onBehaviorUpdated` in CompactObjectPropertiesEditor was a no-op, so editing behavior properties from the compact panel never propagated to the in-app editor preview. Wire it to `onObjectsModified([object])` like the other property edits in the same file. Debounced at 250ms via `useDebounce` to avoid one hot reload per keystroke on fields. --- .../CompactObjectPropertiesEditor/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js index d82cc166f1d6..4fd498b97239 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js @@ -24,6 +24,7 @@ import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout'; import { IconContainer } from '../../UI/IconContainer'; import RemoveIcon from '../../UI/CustomSvgIcons/Remove'; import useForceUpdate from '../../Utils/UseForceUpdate'; +import { useDebounce } from '../../Utils/UseDebounce'; import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight'; import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom'; import ChevronArrowDownWithRoundedBorder from '../../UI/CustomSvgIcons/ChevronArrowDownWithRoundedBorder'; @@ -315,6 +316,11 @@ export const CompactObjectPropertiesEditor = ({ isBehaviorListLocked, }: Props): React.Node => { const forceUpdate = useForceUpdate(); + // Debounced to avoid one hot reload per keystroke on fields. + const debouncedNotifyBehaviorUpdated = useDebounce( + (objectToNotify: gdObject) => onObjectsModified([objectToNotify]), + 250 + ); const [isPropertiesFolded, setIsPropertiesFolded] = React.useState(false); const [isBehaviorsFolded, setIsBehaviorsFolded] = React.useState(false); const [isVariablesFolded, setIsVariablesFolded] = React.useState(false); @@ -824,7 +830,9 @@ export const CompactObjectPropertiesEditor = ({ behaviorOverriding={null} initialInstance={null} object={object} - onBehaviorUpdated={() => {}} + onBehaviorUpdated={() => + debouncedNotifyBehaviorUpdated(object) + } resourceManagementProps={resourceManagementProps} onOpenFullEditor={() => onEditObject(object, 'behaviors') From ba7903f54c2e008f1ce068a56d72da5807fb5cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Vivet?= Date: Wed, 13 May 2026 22:14:36 +0200 Subject: [PATCH 2/4] feat(physics3d): support hot reload of shape and physics properties Extract the field initialization from the constructor into a private `_applyBehaviorData` helper, and reuse it for both the constructor (full data) and the hot-reload hook `applyBehaviorOverriding` (diff containing only changed fields). When any shape-related property is present in the diff, the body is rebuilt via `recreateBody()`. Previously the default `applyBehaviorOverriding` returned false, so hot reload of Physics3D properties was silently dropped, forcing a full restart of the app or closing and reopening the tab to see any tweak made in the editor. --- .../Physics3DRuntimeBehavior.ts | 98 ++++++++++++++----- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts index 2a6aa28fbdda..cbee5e1fcbba 100644 --- a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts +++ b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts @@ -409,31 +409,7 @@ namespace gdjs { this.collisionChecker = new gdjs.Physics3DRuntimeBehavior.DefaultCollisionChecker(this); this.owner3D = owner; - this.bodyType = behaviorData.bodyType; - this.bullet = behaviorData.bullet; - this.fixedRotation = behaviorData.fixedRotation; - this._shape = behaviorData.shape; - this.meshShapeResourceName = behaviorData.meshShapeResourceName || ''; - this.shapeOrientation = - behaviorData.shape === 'Box' ? 'Z' : behaviorData.shapeOrientation; - this.shapeDimensionA = behaviorData.shapeDimensionA; - this.shapeDimensionB = behaviorData.shapeDimensionB; - this.shapeDimensionC = behaviorData.shapeDimensionC; - this.shapeOffsetX = behaviorData.shapeOffsetX || 0; - this.shapeOffsetY = behaviorData.shapeOffsetY || 0; - this.shapeOffsetZ = behaviorData.shapeOffsetZ || 0; - this.massCenterOffsetX = behaviorData.massCenterOffsetX || 0; - this.massCenterOffsetY = behaviorData.massCenterOffsetY || 0; - this.massCenterOffsetZ = behaviorData.massCenterOffsetZ || 0; - this.density = Math.max(0.0001, behaviorData.density); - this.massOverride = behaviorData.massOverride || 0; - this.friction = behaviorData.friction; - this.restitution = behaviorData.restitution; - this.linearDamping = Math.max(0, behaviorData.linearDamping); - this.angularDamping = Math.max(0, behaviorData.angularDamping); - this.gravityScale = behaviorData.gravityScale; - this.layers = behaviorData.layers; - this.masks = behaviorData.masks; + this._applyBehaviorData(behaviorData); this._sharedData = Physics3DSharedData.getSharedData( instanceContainer.getScene(), behaviorData.name @@ -1131,6 +1107,78 @@ namespace gdjs { this._sharedData.stepped = false; } + private _applyBehaviorData(data: any): void { + if ('bodyType' in data) this.bodyType = data.bodyType; + if ('bullet' in data) this.bullet = data.bullet; + if ('fixedRotation' in data) this.fixedRotation = data.fixedRotation; + + if ('shape' in data) { + this._shape = data.shape; + const orientation = + 'shapeOrientation' in data + ? data.shapeOrientation + : this.shapeOrientation; + this.shapeOrientation = data.shape === 'Box' ? 'Z' : orientation; + } else if ('shapeOrientation' in data) { + this.shapeOrientation = + this._shape === 'Box' ? 'Z' : data.shapeOrientation; + } + + if ('meshShapeResourceName' in data) + this.meshShapeResourceName = data.meshShapeResourceName || ''; + if ('shapeDimensionA' in data) + this.shapeDimensionA = data.shapeDimensionA; + if ('shapeDimensionB' in data) + this.shapeDimensionB = data.shapeDimensionB; + if ('shapeDimensionC' in data) + this.shapeDimensionC = data.shapeDimensionC; + if ('shapeOffsetX' in data) this.shapeOffsetX = data.shapeOffsetX || 0; + if ('shapeOffsetY' in data) this.shapeOffsetY = data.shapeOffsetY || 0; + if ('shapeOffsetZ' in data) this.shapeOffsetZ = data.shapeOffsetZ || 0; + if ('massCenterOffsetX' in data) + this.massCenterOffsetX = data.massCenterOffsetX || 0; + if ('massCenterOffsetY' in data) + this.massCenterOffsetY = data.massCenterOffsetY || 0; + if ('massCenterOffsetZ' in data) + this.massCenterOffsetZ = data.massCenterOffsetZ || 0; + if ('density' in data) this.density = Math.max(0.0001, data.density); + if ('massOverride' in data) this.massOverride = data.massOverride || 0; + if ('friction' in data) this.friction = data.friction; + if ('restitution' in data) this.restitution = data.restitution; + if ('linearDamping' in data) + this.linearDamping = Math.max(0, data.linearDamping); + if ('angularDamping' in data) + this.angularDamping = Math.max(0, data.angularDamping); + if ('gravityScale' in data) this.gravityScale = data.gravityScale; + if ('layers' in data) this.layers = data.layers; + if ('masks' in data) this.masks = data.masks; + } + + override applyBehaviorOverriding(diff: BehaviorData): boolean { + this._applyBehaviorData(diff); + + // Recreate the body if any shape-related property changed. + const d = diff as any; + const shapeChanged = + 'shape' in d || + 'shapeOrientation' in d || + 'shapeDimensionA' in d || + 'shapeDimensionB' in d || + 'shapeDimensionC' in d || + 'shapeOffsetX' in d || + 'shapeOffsetY' in d || + 'shapeOffsetZ' in d || + 'massCenterOffsetX' in d || + 'massCenterOffsetY' in d || + 'massCenterOffsetZ' in d || + 'meshShapeResourceName' in d || + 'bodyType' in d; + if (shapeChanged) { + this.recreateBody(); + } + return true; + } + onObjectHotReloaded() { this.updateBodyFromObject(); } From ddd93b6dd44aa6bbdf0d42e843350fb6b926dbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Vivet?= Date: Mon, 18 May 2026 11:31:48 +0200 Subject: [PATCH 3/4] fix types --- .../Physics3DRuntimeBehavior.ts | 101 +++++------------- 1 file changed, 24 insertions(+), 77 deletions(-) diff --git a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts index cbee5e1fcbba..2c6b7407bdb3 100644 --- a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts +++ b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts @@ -312,30 +312,30 @@ namespace gdjs { collisionChecker: gdjs.Physics3DRuntimeBehavior.CollisionChecker; owner3D: gdjs.RuntimeObject3D; - bodyType: string; - bullet: boolean; - fixedRotation: boolean; - _shape: string; - private meshShapeResourceName: string; - private shapeOrientation: string; - private shapeDimensionA: float; - private shapeDimensionB: float; - private shapeDimensionC: float; - private shapeOffsetX: float; - private shapeOffsetY: float; - shapeOffsetZ: float; - private massCenterOffsetX: float; - private massCenterOffsetY: float; - private massCenterOffsetZ: float; - private density: float; - massOverride: float; - friction: float; - restitution: float; - linearDamping: float; - angularDamping: float; - gravityScale: float; - private layers: integer; - private masks: integer; + bodyType!: string; + bullet!: boolean; + fixedRotation!: boolean; + _shape!: string; + private meshShapeResourceName!: string; + private shapeOrientation!: string; + private shapeDimensionA!: float; + private shapeDimensionB!: float; + private shapeDimensionC!: float; + private shapeOffsetX!: float; + private shapeOffsetY!: float; + shapeOffsetZ!: float; + private massCenterOffsetX!: float; + private massCenterOffsetY!: float; + private massCenterOffsetZ!: float; + private density!: float; + massOverride!: float; + friction!: float; + restitution!: float; + linearDamping!: float; + angularDamping!: float; + gravityScale!: float; + private layers!: integer; + private masks!: integer; shapeScale: number = 1; /** @@ -435,59 +435,6 @@ namespace gdjs { return tempQuat; } - override applyBehaviorOverriding(behaviorData): boolean { - if (behaviorData.bullet !== undefined) { - this.setBullet(behaviorData.bullet); - } - if (behaviorData.fixedRotation !== undefined) { - this.setFixedRotation(behaviorData.fixedRotation); - } - if (behaviorData.shapeDimensionA !== undefined) { - this.shapeDimensionA = behaviorData.shapeDimensionA; - this._needToRecreateShape = true; - } - if (behaviorData.shapeDimensionB !== undefined) { - this.shapeDimensionB = behaviorData.shapeDimensionB; - this._needToRecreateShape = true; - } - if (behaviorData.density !== undefined) { - this.setDensity(behaviorData.density); - } - if (behaviorData.friction !== undefined) { - this.setFriction(behaviorData.friction); - } - if (behaviorData.restitution !== undefined) { - this.setRestitution(behaviorData.restitution); - } - if (behaviorData.linearDamping !== undefined) { - this.setLinearDamping(behaviorData.linearDamping); - } - if (behaviorData.angularDamping !== undefined) { - this.setAngularDamping(behaviorData.angularDamping); - } - if (behaviorData.gravityScale !== undefined) { - this.setGravityScale(behaviorData.gravityScale); - } - - // TODO: make these properties updatable. - if (behaviorData.layers !== undefined) { - return false; - } - if (behaviorData.masks !== undefined) { - return false; - } - if (behaviorData.vertices !== undefined) { - return false; - } - if (behaviorData.bodyType !== undefined) { - return false; - } - if (behaviorData.shape !== undefined) { - return false; - } - return true; - } - override getNetworkSyncData( options: GetNetworkSyncDataOptions ): Physics3DNetworkSyncData { From cad8056d2416736f73ca72e285d1100f67f4856b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Vivet?= Date: Thu, 28 May 2026 14:15:21 +0200 Subject: [PATCH 4/4] Apply comments --- .../Physics3DRuntimeBehavior.ts | 79 +++++++++---------- .../CompactObjectPropertiesEditor/index.js | 7 +- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts index 450f627c143a..351042ec6da3 100644 --- a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts +++ b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts @@ -312,30 +312,30 @@ namespace gdjs { collisionChecker: gdjs.Physics3DRuntimeBehavior.CollisionChecker; owner3D: gdjs.RuntimeObject3D; - bodyType!: string; - bullet!: boolean; - fixedRotation!: boolean; - _shape!: string; - private meshShapeResourceName!: string; - private shapeOrientation!: string; - private shapeDimensionA!: float; - private shapeDimensionB!: float; - private shapeDimensionC!: float; - private shapeOffsetX!: float; - private shapeOffsetY!: float; - shapeOffsetZ!: float; - private massCenterOffsetX!: float; - private massCenterOffsetY!: float; - private massCenterOffsetZ!: float; - private density!: float; - massOverride!: float; - friction!: float; - restitution!: float; - linearDamping!: float; - angularDamping!: float; - gravityScale!: float; - private layers!: integer; - private masks!: integer; + bodyType: string; + bullet: boolean; + fixedRotation: boolean; + _shape: string; + private meshShapeResourceName: string; + private shapeOrientation: string; + private shapeDimensionA: float; + private shapeDimensionB: float; + private shapeDimensionC: float; + private shapeOffsetX: float; + private shapeOffsetY: float; + shapeOffsetZ: float; + private massCenterOffsetX: float; + private massCenterOffsetY: float; + private massCenterOffsetZ: float; + private density: float; + massOverride: float; + friction: float; + restitution: float; + linearDamping: float; + angularDamping: float; + gravityScale: float; + private layers: integer; + private masks: integer; shapeScale: number = 1; /** @@ -1101,25 +1101,24 @@ namespace gdjs { if ('masks' in data) this.masks = data.masks; } - override applyBehaviorOverriding(diff: BehaviorData): boolean { - this._applyBehaviorData(diff); + override applyBehaviorOverriding(behaviorData): boolean { + this._applyBehaviorData(behaviorData); // Recreate the body if any shape-related property changed. - const d = diff as any; const shapeChanged = - 'shape' in d || - 'shapeOrientation' in d || - 'shapeDimensionA' in d || - 'shapeDimensionB' in d || - 'shapeDimensionC' in d || - 'shapeOffsetX' in d || - 'shapeOffsetY' in d || - 'shapeOffsetZ' in d || - 'massCenterOffsetX' in d || - 'massCenterOffsetY' in d || - 'massCenterOffsetZ' in d || - 'meshShapeResourceName' in d || - 'bodyType' in d; + 'shape' in behaviorData || + 'shapeOrientation' in behaviorData || + 'shapeDimensionA' in behaviorData || + 'shapeDimensionB' in behaviorData || + 'shapeDimensionC' in behaviorData || + 'shapeOffsetX' in behaviorData || + 'shapeOffsetY' in behaviorData || + 'shapeOffsetZ' in behaviorData || + 'massCenterOffsetX' in behaviorData || + 'massCenterOffsetY' in behaviorData || + 'massCenterOffsetZ' in behaviorData || + 'meshShapeResourceName' in behaviorData || + 'bodyType' in behaviorData; if (shapeChanged) { this.recreateBody(); } diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js index a603e0610959..5efd333aea37 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js @@ -25,6 +25,7 @@ import { IconContainer } from '../../UI/IconContainer'; import RemoveIcon from '../../UI/CustomSvgIcons/Remove'; import useForceUpdate from '../../Utils/UseForceUpdate'; import { useDebounce } from '../../Utils/UseDebounce'; +import { useIsMounted } from '../../Utils/UseIsMounted'; import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight'; import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom'; import ChevronArrowDownWithRoundedBorder from '../../UI/CustomSvgIcons/ChevronArrowDownWithRoundedBorder'; @@ -316,9 +317,13 @@ export const CompactObjectPropertiesEditor = ({ isBehaviorListLocked, }: Props): React.Node => { const forceUpdate = useForceUpdate(); + const isMounted = useIsMounted(); // Debounced to avoid one hot reload per keystroke on fields. const debouncedNotifyBehaviorUpdated = useDebounce( - (objectToNotify: gdObject) => onObjectsModified([objectToNotify]), + (objectToNotify: gdObject) => { + if (!isMounted.current) return; + onObjectsModified([objectToNotify]); + }, 250 ); const [isPropertiesFolded, setIsPropertiesFolded] = React.useState(false);