fix(brush): stop painting after pointer release

This commit is contained in:
syntaxbullet
2026-07-03 17:43:13 +02:00
parent c13a6f5917
commit e75f8bc0a7
2 changed files with 68 additions and 28 deletions

View File

@@ -7,11 +7,17 @@ export type ImageTextureRenderer = {
dispose(): void;
};
type TextureEntry =
type TextureState =
| { status: "loading"; image: HTMLImageElement }
| { status: "ready"; texture: WebGLTexture }
| { status: "error" };
type TextureEntry = {
source: string;
current: TextureState;
previousTexture?: WebGLTexture;
};
export function createImageTextureRenderer(context: WebGlRendererContext, invalidate: () => void): ImageTextureRenderer {
const { gl } = context;
const program = createProgram(gl);
@@ -30,11 +36,11 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
return {
syncAssets(assets) {
const activeKeys = new Set(assets.map(textureKey));
for (const [key, entry] of textures) {
if (!activeKeys.has(key)) {
const activeIds = new Set(assets.map((asset) => asset.id));
for (const [assetId, entry] of textures) {
if (!activeIds.has(assetId)) {
disposeEntry(gl, entry);
textures.delete(key);
textures.delete(assetId);
}
}
},
@@ -43,7 +49,8 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed);
if (entry.status !== "ready") return false;
const texture = renderableTexture(entry);
if (!texture) return false;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
@@ -52,7 +59,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
gl.useProgram(program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(samplerLocation, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
@@ -85,42 +92,71 @@ function getTextureEntry(
invalidate: () => void,
isDisposed: () => boolean,
): TextureEntry {
const key = textureKey(asset);
const cached = textures.get(key);
if (cached) return cached;
const cached = textures.get(asset.id);
if (cached && cached.source === asset.source) return cached;
if (cached) {
if (cached.current.status === "ready") {
if (cached.previousTexture) context.gl.deleteTexture(cached.previousTexture);
cached.previousTexture = cached.current.texture;
} else {
disposeState(context.gl, cached.current);
}
cached.source = asset.source;
cached.current = loadTextureState(context, asset, cached, invalidate, isDisposed);
return cached;
}
const entry: TextureEntry = { source: asset.source, current: { status: "error" } };
entry.current = loadTextureState(context, asset, entry, invalidate, isDisposed);
textures.set(asset.id, entry);
return entry;
}
function loadTextureState(
context: WebGlRendererContext,
asset: Asset,
entry: TextureEntry,
invalidate: () => void,
isDisposed: () => boolean,
): TextureState {
const image = new Image();
textures.set(key, { status: "loading", image });
const source = asset.source;
image.onload = () => {
if (isDisposed()) return;
if (isDisposed() || entry.source !== source) return;
const texture = createTexture(context.gl, image);
disposeEntry(context.gl, textures.get(key));
textures.set(key, { status: "ready", texture });
if (entry.previousTexture) context.gl.deleteTexture(entry.previousTexture);
entry.previousTexture = undefined;
entry.current = { status: "ready", texture };
invalidate();
};
image.onerror = () => {
if (isDisposed()) return;
textures.set(key, { status: "error" });
if (isDisposed() || entry.source !== source) return;
entry.current = { status: "error" };
invalidate();
};
image.src = asset.source;
image.src = source;
return { status: "loading", image };
}
function textureKey(asset: Asset) {
return `${asset.id}:${asset.source}`;
function renderableTexture(entry: TextureEntry): WebGLTexture | undefined {
if (entry.current.status === "ready") return entry.current.texture;
return entry.previousTexture;
}
function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry | undefined) {
if (!entry) return;
if (entry.status === "ready") {
gl.deleteTexture(entry.texture);
function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry) {
disposeState(gl, entry.current);
if (entry.previousTexture) gl.deleteTexture(entry.previousTexture);
}
function disposeState(gl: WebGL2RenderingContext, state: TextureState) {
if (state.status === "ready") {
gl.deleteTexture(state.texture);
return;
}
if (entry.status === "loading") {
entry.image.onload = null;
entry.image.onerror = null;
if (state.status === "loading") {
state.image.onload = null;
state.image.onerror = null;
}
}