fix(refboard): fix WebGL null texture crash and add loading overlay
- Set texture to Texture.EMPTY before destroying sprites in AnimatedGifSprite and PdfPageSprite (matches VideoSprite pattern) - Add full-screen loading overlay that blocks interaction until scene and assets finish loading
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Container, Graphics } from "pixi.js";
|
import { Container, Graphics, Texture } from "pixi.js";
|
||||||
import { GifSprite } from "pixi.js/gif";
|
import { GifSprite } from "pixi.js/gif";
|
||||||
import { TextureManager } from "../TextureManager";
|
import { TextureManager } from "../TextureManager";
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export class AnimatedGifSprite extends Container {
|
|||||||
|
|
||||||
/** The underlying GIF sprite's current texture (for clipboard/export). */
|
/** The underlying GIF sprite's current texture (for clipboard/export). */
|
||||||
get texture() {
|
get texture() {
|
||||||
return this._gif?.texture ?? null;
|
return this._gif?.texture ?? Texture.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the GIF is currently animating. */
|
/** Whether the GIF is currently animating. */
|
||||||
@@ -134,6 +134,7 @@ export class AnimatedGifSprite extends Container {
|
|||||||
if (this._gif) {
|
if (this._gif) {
|
||||||
this._gif.stop();
|
this._gif.stop();
|
||||||
this._playing = false;
|
this._playing = false;
|
||||||
|
this._gif.texture = Texture.EMPTY;
|
||||||
}
|
}
|
||||||
this.textures.releaseGif(this.assetKey);
|
this.textures.releaseGif(this.assetKey);
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
@@ -148,6 +149,9 @@ export class AnimatedGifSprite extends Container {
|
|||||||
if (this._gif) {
|
if (this._gif) {
|
||||||
this._gif.stop();
|
this._gif.stop();
|
||||||
this._playing = false;
|
this._playing = false;
|
||||||
|
// Swap to EMPTY before destroying so PixiJS never reads a null source
|
||||||
|
// during its render-loop traversal (collectRenderables → alphaMode).
|
||||||
|
this._gif.texture = Texture.EMPTY;
|
||||||
this.removeChild(this._gif);
|
this.removeChild(this._gif);
|
||||||
this._gif.destroy();
|
this._gif.destroy();
|
||||||
this._gif = null;
|
this._gif = null;
|
||||||
|
|||||||
@@ -241,6 +241,9 @@ export class PdfPageSprite extends Container {
|
|||||||
|
|
||||||
// Remove sprite from display tree entirely — avoids PixiJS v8 render crash
|
// Remove sprite from display tree entirely — avoids PixiJS v8 render crash
|
||||||
if (this._sprite) {
|
if (this._sprite) {
|
||||||
|
// Swap to EMPTY before destroying so PixiJS never reads a null source
|
||||||
|
// during its render-loop traversal (collectRenderables → alphaMode).
|
||||||
|
this._sprite.texture = Texture.EMPTY;
|
||||||
this.removeChild(this._sprite);
|
this.removeChild(this._sprite);
|
||||||
this._sprite.destroy();
|
this._sprite.destroy();
|
||||||
this._sprite = null;
|
this._sprite = null;
|
||||||
|
|||||||
@@ -1150,21 +1150,29 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
canvasTransform={canvasTransform}
|
canvasTransform={canvasTransform}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Loading / Empty canvas guide */}
|
{/* Loading overlay — blocks interaction until scene is ready */}
|
||||||
{(sceneLoading || (objectCount > 0 && loadProgress.total > 0 && loadProgress.loaded < loadProgress.total)) && (
|
{(sceneLoading || (objectCount > 0 && loadProgress.total > 0 && loadProgress.loaded < loadProgress.total)) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute', bottom: '48px', left: '50%', transform: 'translateX(-50%)',
|
position: 'absolute', inset: 0, zIndex: 9999,
|
||||||
pointerEvents: 'none', textAlign: 'center', color: '#888', userSelect: 'none',
|
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px',
|
background: 'rgba(26, 26, 26, 0.85)', backdropFilter: 'blur(4px)',
|
||||||
|
color: '#ccc', userSelect: 'none', fontFamily: 'system-ui, sans-serif',
|
||||||
|
gap: '16px',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: '12px', fontFamily: 'system-ui, sans-serif' }}>
|
<div style={{
|
||||||
|
width: '32px', height: '32px',
|
||||||
|
border: '3px solid #333', borderTopColor: '#4a9eff', borderRadius: '50%',
|
||||||
|
animation: 'spin 0.8s linear infinite',
|
||||||
|
}} />
|
||||||
|
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
|
<div style={{ fontSize: '14px', fontWeight: 500 }}>
|
||||||
{sceneLoading
|
{sceneLoading
|
||||||
? `Loading ${loadProgress.total || '...'} items`
|
? `Loading ${loadProgress.total || ''} items…`
|
||||||
: `Loading assets ${loadProgress.loaded}/${loadProgress.total}`}
|
: `Loading assets ${loadProgress.loaded} / ${loadProgress.total}`}
|
||||||
</div>
|
</div>
|
||||||
{loadProgress.total > 0 && (
|
{loadProgress.total > 0 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '160px', height: '3px', borderRadius: '2px',
|
width: '200px', height: '4px', borderRadius: '2px',
|
||||||
background: '#333', overflow: 'hidden',
|
background: '#333', overflow: 'hidden',
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
Reference in New Issue
Block a user