From 61dcc8be800393d1c410e4eb5f6ce55a008e8234 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Oct 2026 15:41:21 +0200 Subject: [PATCH 1/2] chore(deps): bump @lightningjs/renderer to 3.7.0 --- .changeset/renderer-3-7.md | 8 + patches/@lightningjs__renderer@3.1.1.patch | 96 --------- patches/@lightningjs__renderer@3.7.0.patch | 240 +++++++++++++++++++++ pnpm-lock.yaml | 36 ++-- pnpm-workspace.yaml | 10 +- 5 files changed, 271 insertions(+), 119 deletions(-) create mode 100644 .changeset/renderer-3-7.md delete mode 100644 patches/@lightningjs__renderer@3.1.1.patch create mode 100644 patches/@lightningjs__renderer@3.7.0.patch diff --git a/.changeset/renderer-3-7.md b/.changeset/renderer-3-7.md new file mode 100644 index 00000000..d780b1b5 --- /dev/null +++ b/.changeset/renderer-3-7.md @@ -0,0 +1,8 @@ +--- +'@plextv/react-lightning': patch +'@plextv/react-native-lightning': patch +'@plextv/react-lightning-plugin-reanimated': patch +'@plextv/react-lightning-plugin-css-transform': patch +--- + +bump @lightningjs/renderer to 3.7.0 diff --git a/patches/@lightningjs__renderer@3.1.1.patch b/patches/@lightningjs__renderer@3.1.1.patch deleted file mode 100644 index 7aaac64a..00000000 --- a/patches/@lightningjs__renderer@3.1.1.patch +++ /dev/null @@ -1,96 +0,0 @@ -diff --git a/dist/src/core/CoreNode.js b/dist/src/core/CoreNode.js -index d4bb6f75a8c58b6b1409556d8ed2730c3a6fe551..61d3faddf3ff2969a9fdd25be9df2ad51b023eda 100644 ---- a/dist/src/core/CoreNode.js -+++ b/dist/src/core/CoreNode.js -@@ -979,12 +979,12 @@ export class CoreNode extends EventEmitter { - const ownRadius = clippingRect.clipRadius; - intersectRect(parentClippingRect, clippingRect, clippingRect); - clippingRect.clipRadius = ownRadius; -- // intersectRect writes {0,0,0,0} when the rects don't overlap but does -- // not touch the valid flag. An empty intersection means nothing is -- // visible — mark the rect invalid so children are not clipped to a -- // zero-area region and the stencil pass is skipped. -+ // An empty intersection means the subtree is fully clipped. Keep the -+ // rect valid with zero area so the scissor rejects everything; -+ // valid=false here would render children unclipped instead. - if (clippingRect.w <= 0 || clippingRect.h <= 0) { -- clippingRect.valid = false; -+ clippingRect.w = 0; -+ clippingRect.h = 0; - clippingRect.clipRadius = 0; - } - } -@@ -1139,7 +1139,9 @@ export class CoreNode extends EventEmitter { - autosizeTarget.attach(node); - } - if (inRttCluster === true) { -- node.markChildrenWithRTT(this); -+ // Walk the NEW child's subtree: nodes attach bottom-up, so its -+ // descendants were stamped false before this attach. -+ node.markChildrenWithRTT(); - } - children.push(node); - if (children.length === 1) { -diff --git a/dist/src/core/renderers/webgl/WebGlRenderer.js b/dist/src/core/renderers/webgl/WebGlRenderer.js -index 27f1d49d8096157c29b223dd0327f7b0f598d002..693f1e6eb8a6261716cff83d85c5d0459274a401 100644 ---- a/dist/src/core/renderers/webgl/WebGlRenderer.js -+++ b/dist/src/core/renderers/webgl/WebGlRenderer.js -@@ -688,7 +688,9 @@ export class WebGlRenderer extends CoreRenderer { - op.w = Math.round(cr.w * pixelRatio); - op.h = Math.round(cr.h * pixelRatio); - op.y = Math.round(canvas.height - op.h - cr.y * pixelRatio); -- op.clipRadius = cr.clipRadius * pixelRatio; -+ // Clamp to half the min side: a radius past that (e.g. borderRadius -+ // 9999 for a circle) breaks the rounded-rect SDF and clips everything. -+ op.clipRadius = Math.min(cr.clipRadius * pixelRatio, op.w / 2, op.h / 2); - op.pixelRatio = pixelRatio; - op.canvasHeight = canvas.height; - op.parentHasRenderTexture = node.parentHasRenderTexture; -diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts -index fad32fb4acff160677c74eb91de43bf90e6f022b..3ee522354a33889e9a8bd2aa70184ff2164952f5 100644 ---- a/src/core/CoreNode.ts -+++ b/src/core/CoreNode.ts -@@ -1805,12 +1805,12 @@ export class CoreNode extends EventEmitter { - const ownRadius = clippingRect.clipRadius; - intersectRect(parentClippingRect, clippingRect, clippingRect); - clippingRect.clipRadius = ownRadius; -- // intersectRect writes {0,0,0,0} when the rects don't overlap but does -- // not touch the valid flag. An empty intersection means nothing is -- // visible — mark the rect invalid so children are not clipped to a -- // zero-area region and the stencil pass is skipped. -+ // An empty intersection means the subtree is fully clipped. Keep the -+ // rect valid with zero area so the scissor rejects everything; -+ // valid=false here would render children unclipped instead. - if (clippingRect.w <= 0 || clippingRect.h <= 0) { -- clippingRect.valid = false; -+ clippingRect.w = 0; -+ clippingRect.h = 0; - clippingRect.clipRadius = 0; - } - } else if (parentClippingRect.valid === true) { -@@ -1995,7 +1995,9 @@ export class CoreNode extends EventEmitter { - } - - if (inRttCluster === true) { -- node.markChildrenWithRTT(this); -+ // Walk the NEW child's subtree: nodes attach bottom-up, so its -+ // descendants were stamped false before this attach. -+ node.markChildrenWithRTT(); - } - - children.push(node); -diff --git a/src/core/renderers/webgl/WebGlRenderer.ts b/src/core/renderers/webgl/WebGlRenderer.ts -index 2780b17b4233d760f97f963133abbf5b6a0bd8c9..797518e33ab767fc777e9e65a6ee01ef3c8846da 100644 ---- a/src/core/renderers/webgl/WebGlRenderer.ts -+++ b/src/core/renderers/webgl/WebGlRenderer.ts -@@ -865,7 +865,9 @@ export class WebGlRenderer extends CoreRenderer { - op.w = Math.round(cr.w * pixelRatio); - op.h = Math.round(cr.h * pixelRatio); - op.y = Math.round(canvas.height - op.h - cr.y * pixelRatio); -- op.clipRadius = cr.clipRadius * pixelRatio; -+ // Clamp to half the min side: a radius past that (e.g. borderRadius -+ // 9999 for a circle) breaks the rounded-rect SDF and clips everything. -+ op.clipRadius = Math.min(cr.clipRadius * pixelRatio, op.w / 2, op.h / 2); - op.pixelRatio = pixelRatio; - op.canvasHeight = canvas.height; - op.parentHasRenderTexture = node.parentHasRenderTexture; diff --git a/patches/@lightningjs__renderer@3.7.0.patch b/patches/@lightningjs__renderer@3.7.0.patch new file mode 100644 index 00000000..dafeae23 --- /dev/null +++ b/patches/@lightningjs__renderer@3.7.0.patch @@ -0,0 +1,240 @@ +diff --git a/dist/src/core/CoreNode.js b/dist/src/core/CoreNode.js +index 0194fbfed66a8d1c65c70b5a72097409c0f6fb81..925bfcfc8d0ee2233d20470d4d185857926b001d 100644 +--- a/dist/src/core/CoreNode.js ++++ b/dist/src/core/CoreNode.js +@@ -1142,8 +1142,11 @@ export class CoreNode extends EventEmitter { + const ownRadius = clippingRect.clipRadius; + intersectRect(parentClippingRect, clippingRect, clippingRect); + clippingRect.clipRadius = ownRadius; ++ // Empty intersection: keep the rect valid with zero area so the scissor rejects ++ // everything. valid=false would render the children unclipped. + if (clippingRect.w <= 0 || clippingRect.h <= 0) { +- clippingRect.valid = false; ++ clippingRect.w = 0; ++ clippingRect.h = 0; + clippingRect.clipRadius = 0; + } + } +@@ -1306,7 +1309,9 @@ export class CoreNode extends EventEmitter { + autosizeTarget.attach(node); + } + if (inRttCluster === true) { +- node.markChildrenWithRTT(this); ++ // Walk the new child's subtree: nodes attach bottom-up, so its descendants ++ // were stamped false before this attach. ++ node.markChildrenWithRTT(); + } + children.push(node); + if (children.length === 1) { +diff --git a/dist/src/core/animations/AnimationManager.js b/dist/src/core/animations/AnimationManager.js +index 977825991a433dc29bb36d7ec29d3e31960193f0..3990f71d5a806052467873521d2b53029bf9205c 100644 +--- a/dist/src/core/animations/AnimationManager.js ++++ b/dist/src/core/animations/AnimationManager.js +@@ -34,39 +34,58 @@ export class AnimationManager { + constructor(stage) { + this.stage = stage; + } ++ updating = false; ++ needsCompact = false; ++ + registerAnimation(animation) { + animation.activeIndex = this.activeAnimations.length; + this.activeAnimations.push(animation); + this.stage.activeAnimationCount++; + } ++ ++ // Removal keeps start order so the newest animation on a prop writes last and ++ // wins, as in 3.1. Removals during update() are compacted after the loop. + unregisterAnimation(animation) { + const index = animation.activeIndex; + if (index === -1) { + return; + } +- const animations = this.activeAnimations; +- const last = animations.length - 1; +- if (index !== last) { +- // Swap with the last element and update its index +- const swap = animations[last]; +- animations[index] = swap; +- swap.activeIndex = index; +- } +- animations.pop(); + animation.activeIndex = -1; + this.stage.activeAnimationCount--; ++ if (this.updating === true) { ++ this.needsCompact = true; ++ return; ++ } ++ const animations = this.activeAnimations; ++ animations.splice(index, 1); ++ for (let i = index; i < animations.length; i++) { ++ animations[i].activeIndex = i; ++ } + } ++ + update(dt) { + const animations = this.activeAnimations; +- // Iterate backwards. With activeIndex tracking, if a sibling stop() during +- // a completion event swap-removes an already-visited element into index i, +- // we check activeIndex >= 0 before updating to avoid double-processing. +- for (let i = animations.length - 1; i >= 0; i--) { ++ const len = animations.length; ++ this.updating = true; ++ for (let i = 0; i < len; i++) { + const anim = animations[i]; +- if (anim.activeIndex >= 0) { ++ if (anim.activeIndex === i) { + anim.update(dt); + } + } ++ this.updating = false; ++ if (this.needsCompact === true) { ++ this.needsCompact = false; ++ let w = 0; ++ for (let i = 0; i < animations.length; i++) { ++ const anim = animations[i]; ++ if (anim.activeIndex === i) { ++ anim.activeIndex = w; ++ animations[w++] = anim; ++ } ++ } ++ animations.length = w; ++ } + } + /** + * Create an animation controller, reusing pooled objects when available. +diff --git a/dist/src/core/renderers/webgl/WebGlRenderer.js b/dist/src/core/renderers/webgl/WebGlRenderer.js +index 9d388c3dec607f6e521a5be2097dd1406de5edbf..7e0b4fd28b3e3a5c448cdb92aa3f88ed3fc06005 100644 +--- a/dist/src/core/renderers/webgl/WebGlRenderer.js ++++ b/dist/src/core/renderers/webgl/WebGlRenderer.js +@@ -1332,7 +1332,9 @@ export class WebGlRenderer extends CoreRenderer { + op.w = Math.round(cr.w * pixelRatio); + op.h = Math.round(cr.h * pixelRatio); + op.y = Math.round(canvas.height - op.h - cr.y * pixelRatio); +- op.clipRadius = cr.clipRadius * pixelRatio; ++ // Clamp to half the min side: a bigger radius (borderRadius 9999 for a circle) ++ // breaks the rounded-rect SDF and clips everything. ++ op.clipRadius = Math.min(cr.clipRadius * pixelRatio, op.w / 2, op.h / 2); + op.pixelRatio = pixelRatio; + op.canvasHeight = canvas.height; + op.parentHasRenderTexture = node.parentHasRenderTexture; +diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts +index c43d6addec388ccdf2e15a7284629a71a6699940..302ff8c9b1bee7883b17b0bc9cc83155f1eb686a 100644 +--- a/src/core/CoreNode.ts ++++ b/src/core/CoreNode.ts +@@ -1997,8 +1997,11 @@ export class CoreNode extends EventEmitter { + const ownRadius = clippingRect.clipRadius; + intersectRect(parentClippingRect, clippingRect, clippingRect); + clippingRect.clipRadius = ownRadius; ++ // Empty intersection: keep the rect valid with zero area so the scissor rejects ++ // everything. valid=false would render the children unclipped. + if (clippingRect.w <= 0 || clippingRect.h <= 0) { +- clippingRect.valid = false; ++ clippingRect.w = 0; ++ clippingRect.h = 0; + clippingRect.clipRadius = 0; + } + } else if (parentClippingRect.valid === true) { +@@ -2192,7 +2195,9 @@ export class CoreNode extends EventEmitter { + } + + if (inRttCluster === true) { +- node.markChildrenWithRTT(this); ++ // Walk the new child's subtree: nodes attach bottom-up, so its descendants ++ // were stamped false before this attach. ++ node.markChildrenWithRTT(); + } + + children.push(node); +diff --git a/src/core/animations/AnimationManager.ts b/src/core/animations/AnimationManager.ts +index cf9a363b99469b3893a9e79a40d4fbcdcb565276..dfa0ee6c7a296259fd7ca44ec58cb69c3c37327b 100644 +--- a/src/core/animations/AnimationManager.ts ++++ b/src/core/animations/AnimationManager.ts +@@ -43,41 +43,58 @@ export class AnimationManager { + this.stage = stage; + } + ++ private updating = false; ++ private needsCompact = false; ++ + registerAnimation(animation: CoreAnimation) { + animation.activeIndex = this.activeAnimations.length; + this.activeAnimations.push(animation); + this.stage.activeAnimationCount++; + } + ++ // Removal keeps start order so the newest animation on a prop writes last and ++ // wins, as in 3.1. Removals during update() are compacted after the loop. + unregisterAnimation(animation: CoreAnimation) { + const index = animation.activeIndex; + if (index === -1) { + return; + } +- const animations = this.activeAnimations; +- const last = animations.length - 1; +- if (index !== last) { +- // Swap with the last element and update its index +- const swap = animations[last]!; +- animations[index] = swap; +- swap.activeIndex = index; +- } +- animations.pop(); + animation.activeIndex = -1; + this.stage.activeAnimationCount--; ++ if (this.updating === true) { ++ this.needsCompact = true; ++ return; ++ } ++ const animations = this.activeAnimations; ++ animations.splice(index, 1); ++ for (let i = index; i < animations.length; i++) { ++ animations[i]!.activeIndex = i; ++ } + } + + update(dt: number) { + const animations = this.activeAnimations; +- // Iterate backwards. With activeIndex tracking, if a sibling stop() during +- // a completion event swap-removes an already-visited element into index i, +- // we check activeIndex >= 0 before updating to avoid double-processing. +- for (let i = animations.length - 1; i >= 0; i--) { ++ const len = animations.length; ++ this.updating = true; ++ for (let i = 0; i < len; i++) { + const anim = animations[i]!; +- if (anim.activeIndex >= 0) { ++ if (anim.activeIndex === i) { + anim.update(dt); + } + } ++ this.updating = false; ++ if (this.needsCompact === true) { ++ this.needsCompact = false; ++ let w = 0; ++ for (let i = 0; i < animations.length; i++) { ++ const anim = animations[i]!; ++ if (anim.activeIndex === i) { ++ anim.activeIndex = w; ++ animations[w++] = anim; ++ } ++ } ++ animations.length = w; ++ } + } + + /** +diff --git a/src/core/renderers/webgl/WebGlRenderer.ts b/src/core/renderers/webgl/WebGlRenderer.ts +index 39c0a6940c43bae8d3b38458623fc59d70b5d694..73a82709bb3fbb48fd5f352dd7a363f64adb136e 100644 +--- a/src/core/renderers/webgl/WebGlRenderer.ts ++++ b/src/core/renderers/webgl/WebGlRenderer.ts +@@ -1704,7 +1704,9 @@ export class WebGlRenderer extends CoreRenderer { + op.w = Math.round(cr.w * pixelRatio); + op.h = Math.round(cr.h * pixelRatio); + op.y = Math.round(canvas.height - op.h - cr.y * pixelRatio); +- op.clipRadius = cr.clipRadius * pixelRatio; ++ // Clamp to half the min side: a bigger radius (borderRadius 9999 for a circle) ++ // breaks the rounded-rect SDF and clips everything. ++ op.clipRadius = Math.min(cr.clipRadius * pixelRatio, op.w / 2, op.h / 2); + op.pixelRatio = pixelRatio; + op.canvasHeight = canvas.height; + op.parentHasRenderTexture = node.parentHasRenderTexture; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b130cef8..5caee69f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: apps: '@lightningjs/renderer': - specifier: 3.1.1 - version: 3.1.1 + specifier: 3.7.0 + version: 3.7.0 react: specifier: 19.2.5 version: 19.2.5 @@ -23,8 +23,8 @@ catalogs: version: 4.3.0 default: '@lightningjs/renderer': - specifier: 3.1.1 - version: 3.1.1 + specifier: 3.7.0 + version: 3.7.0 '@rolldown/plugin-babel': specifier: ^0.2.3 version: 0.2.3 @@ -66,9 +66,9 @@ catalogs: version: 8.0.8 patchedDependencies: - '@lightningjs/renderer@3.1.1': - hash: 8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623 - path: patches/@lightningjs__renderer@3.1.1.patch + '@lightningjs/renderer@3.7.0': + hash: 9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34 + path: patches/@lightningjs__renderer@3.7.0.patch importers: @@ -139,7 +139,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: catalog:apps - version: 3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623) + version: 3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34) '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning @@ -194,7 +194,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: catalog:apps - version: 3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623) + version: 3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34) '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning @@ -273,7 +273,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: catalog:apps - version: 3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623) + version: 3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34) '@plextv/react-lightning': specifier: workspace:* version: link:../../packages/react-lightning @@ -364,7 +364,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623) + version: 3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34) '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -427,7 +427,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623) + version: 3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34) '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -464,7 +464,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623) + version: 3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34) react: specifier: 'catalog:' version: 19.2.5 @@ -511,7 +511,7 @@ importers: dependencies: '@lightningjs/renderer': specifier: 'catalog:' - version: 3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623) + version: 3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34) '@plextv/react-lightning': specifier: workspace:^ version: link:../react-lightning @@ -1660,9 +1660,9 @@ packages: resolution: {integrity: sha512-FHIgj5rkOQPd9/wDXaiR0GOoWDEj7BytIzvYq5K8/wAh3z2bbW8gTNN+0J5kc1KXtqPrbyg1i87ksUVLrLEr1g==} engines: {node: '>=18.0.0'} - '@lightningjs/renderer@3.1.1': - resolution: {integrity: sha512-L1+9ZN13+mH5vz7wQOF9+edghWA8N7ETi1d8cFC2SCp+7evmB7utz/NROfUCZyDhOLXlXREN2GPcELTBXeU0wg==} - engines: {node: '>= 18.0.0', npm: '>= 10.0.0', pnpm: '>= 10.17.0'} + '@lightningjs/renderer@3.7.0': + resolution: {integrity: sha512-cCwf1H/KuegS/6mDw0OnA9f+g2j++OLyX/U9sA4nvnUqYMqdEa9bbUy2UPB/CvXc7upsBN780rDKT/iSy1uxtA==} + engines: {node: '>= 22.13.0', npm: '>= 10.0.0', pnpm: '>= 10.17.0'} '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -6793,7 +6793,7 @@ snapshots: msdf-bmfont-xml: 2.8.0 opentype.js: 1.3.4 - '@lightningjs/renderer@3.1.1(patch_hash=8a9b52055d19d3441ef640e67ba0f603d80233d87dcea76490db2810bb262623)': {} + '@lightningjs/renderer@3.7.0(patch_hash=9c235a3ecbdac17b10cd5b0afd66365e873289ba662910b87fb489c08821de34)': {} '@manypkg/find-root@1.1.0': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 189db423..c87c183e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,7 @@ packages: - packages/* catalog: - '@lightningjs/renderer': 3.1.1 + '@lightningjs/renderer': 3.7.0 '@rolldown/plugin-babel': ^0.2.3 '@types/react': 19.2.14 '@types/react-dom': 19.2.3 @@ -21,16 +21,16 @@ catalog: catalogs: apps: - '@lightningjs/renderer': 3.1.1 + '@lightningjs/renderer': 3.7.0 '@vitejs/plugin-react': 6.0.1 react: 19.2.5 react-dom: 19.2.5 react-native: 0.85.1 react-native-reanimated: 4.3.0 -patchedDependencies: - '@lightningjs/renderer@3.1.1': patches/@lightningjs__renderer@3.1.1.patch - onlyBuiltDependencies: - core-js - esbuild + +patchedDependencies: + '@lightningjs/renderer@3.7.0': patches/@lightningjs__renderer@3.7.0.patch From c2931e75ae469263af87cc1a936fbc306d254105 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Oct 2026 16:40:09 +0200 Subject: [PATCH 2/2] fix(reanimated): drop the finished controller before cancel can stop it --- .changeset/reanimated-pooled-controller.md | 5 ++++ .../src/animation/runAnimationProgram.test.ts | 30 +++++++++++++++++-- .../src/animation/runAnimationProgram.ts | 18 ++++++----- 3 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 .changeset/reanimated-pooled-controller.md diff --git a/.changeset/reanimated-pooled-controller.md b/.changeset/reanimated-pooled-controller.md new file mode 100644 index 00000000..fb6505dd --- /dev/null +++ b/.changeset/reanimated-pooled-controller.md @@ -0,0 +1,5 @@ +--- +'@plextv/react-lightning-plugin-reanimated': patch +--- + +don't stop a finished, pooled animation controller when a composed animation is cancelled diff --git a/packages/plugin-reanimated/src/animation/runAnimationProgram.test.ts b/packages/plugin-reanimated/src/animation/runAnimationProgram.test.ts index d6039f97..4a3ae72e 100644 --- a/packages/plugin-reanimated/src/animation/runAnimationProgram.test.ts +++ b/packages/plugin-reanimated/src/animation/runAnimationProgram.test.ts @@ -1,12 +1,12 @@ import type { AnimationSettings } from '@lightningjs/renderer'; import { describe, expect, it } from 'vitest'; + import type { LightningElement } from '@plextv/react-lightning'; + import { leafProgram, repeatProgram, sequenceProgram } from './animationProgram'; import { runAnimationProgram } from './runAnimationProgram'; -const settings = ( - over: Partial = {}, -): AnimationSettings => ({ +const settings = (over: Partial = {}): AnimationSettings => ({ duration: 10, easing: 'linear', delay: 0, @@ -96,6 +96,30 @@ describe('runAnimationProgram', () => { expect(view.applied.length).toBeLessThanOrEqual(1); }); + it('cancel after the program finished does not stop the pooled controller', async () => { + const view = makeView(); + let stops = 0; + view.animateStyle = function (key: PropertyKey, value: unknown) { + this.applied.push({ key, value }); + return { + waitUntilStopped: () => Promise.resolve(), + stop() { + stops++; + }, + }; + }; + + const cancel = runAnimationProgram( + view as unknown as LightningElement, + 'x', + leafProgram({ toValue: 1, lngAnimation: settings() }), + ); + await flush(); + cancel(); + + expect(stops).toBe(0); + }); + it('repeats a finite positive count that many times', async () => { const view = makeView(); diff --git a/packages/plugin-reanimated/src/animation/runAnimationProgram.ts b/packages/plugin-reanimated/src/animation/runAnimationProgram.ts index c9e3cb10..a7f1ff0d 100644 --- a/packages/plugin-reanimated/src/animation/runAnimationProgram.ts +++ b/packages/plugin-reanimated/src/animation/runAnimationProgram.ts @@ -1,8 +1,7 @@ import type { IAnimationController } from '@lightningjs/renderer'; -import type { - LightningElement, - LightningElementStyle, -} from '@plextv/react-lightning'; + +import type { LightningElement, LightningElementStyle } from '@plextv/react-lightning'; + import type { AnimationProgram, ProgramLeaf } from './animationProgram'; export type CancelAnimation = () => void; @@ -30,16 +29,19 @@ export function runAnimationProgram( // Call as a method: extracting animateStyle drops `this`, so it throws // and the catch below silently freezes the whole composed animation. const animatableView = view as unknown as { - animateStyle: ( - key: keyof LightningElementStyle, - value: unknown, - ) => IAnimationController; + animateStyle: (key: keyof LightningElementStyle, value: unknown) => IAnimationController; }; const controller = animatableView.animateStyle(prop, leaf.toValue); current = controller; await controller.waitUntilStopped(); + + // The renderer pools controllers: once stopped this one may already drive + // another animation, so cancel must not stop it. + if (current === controller) { + current = undefined; + } } catch { // node was destroyed or recycled mid-flight; stop quietly cancelled = true;