fix(brush): stop painting after pointer release
This commit is contained in:
@@ -7,11 +7,17 @@ export type ImageTextureRenderer = {
|
|||||||
dispose(): void;
|
dispose(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TextureEntry =
|
type TextureState =
|
||||||
| { status: "loading"; image: HTMLImageElement }
|
| { status: "loading"; image: HTMLImageElement }
|
||||||
| { status: "ready"; texture: WebGLTexture }
|
| { status: "ready"; texture: WebGLTexture }
|
||||||
| { status: "error" };
|
| { status: "error" };
|
||||||
|
|
||||||
|
type TextureEntry = {
|
||||||
|
source: string;
|
||||||
|
current: TextureState;
|
||||||
|
previousTexture?: WebGLTexture;
|
||||||
|
};
|
||||||
|
|
||||||
export function createImageTextureRenderer(context: WebGlRendererContext, invalidate: () => void): ImageTextureRenderer {
|
export function createImageTextureRenderer(context: WebGlRendererContext, invalidate: () => void): ImageTextureRenderer {
|
||||||
const { gl } = context;
|
const { gl } = context;
|
||||||
const program = createProgram(gl);
|
const program = createProgram(gl);
|
||||||
@@ -30,11 +36,11 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
syncAssets(assets) {
|
syncAssets(assets) {
|
||||||
const activeKeys = new Set(assets.map(textureKey));
|
const activeIds = new Set(assets.map((asset) => asset.id));
|
||||||
for (const [key, entry] of textures) {
|
for (const [assetId, entry] of textures) {
|
||||||
if (!activeKeys.has(key)) {
|
if (!activeIds.has(assetId)) {
|
||||||
disposeEntry(gl, entry);
|
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;
|
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
|
||||||
|
|
||||||
const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed);
|
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.enable(gl.SCISSOR_TEST);
|
||||||
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
|
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.useProgram(program);
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
gl.uniform1i(samplerLocation, 0);
|
gl.uniform1i(samplerLocation, 0);
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||||
@@ -85,42 +92,71 @@ function getTextureEntry(
|
|||||||
invalidate: () => void,
|
invalidate: () => void,
|
||||||
isDisposed: () => boolean,
|
isDisposed: () => boolean,
|
||||||
): TextureEntry {
|
): TextureEntry {
|
||||||
const key = textureKey(asset);
|
const cached = textures.get(asset.id);
|
||||||
const cached = textures.get(key);
|
if (cached && cached.source === asset.source) return cached;
|
||||||
if (cached) 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();
|
const image = new Image();
|
||||||
textures.set(key, { status: "loading", image });
|
const source = asset.source;
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
if (isDisposed()) return;
|
if (isDisposed() || entry.source !== source) return;
|
||||||
const texture = createTexture(context.gl, image);
|
const texture = createTexture(context.gl, image);
|
||||||
disposeEntry(context.gl, textures.get(key));
|
if (entry.previousTexture) context.gl.deleteTexture(entry.previousTexture);
|
||||||
textures.set(key, { status: "ready", texture });
|
entry.previousTexture = undefined;
|
||||||
|
entry.current = { status: "ready", texture };
|
||||||
invalidate();
|
invalidate();
|
||||||
};
|
};
|
||||||
image.onerror = () => {
|
image.onerror = () => {
|
||||||
if (isDisposed()) return;
|
if (isDisposed() || entry.source !== source) return;
|
||||||
textures.set(key, { status: "error" });
|
entry.current = { status: "error" };
|
||||||
invalidate();
|
invalidate();
|
||||||
};
|
};
|
||||||
image.src = asset.source;
|
image.src = source;
|
||||||
|
|
||||||
return { status: "loading", image };
|
return { status: "loading", image };
|
||||||
}
|
}
|
||||||
|
|
||||||
function textureKey(asset: Asset) {
|
function renderableTexture(entry: TextureEntry): WebGLTexture | undefined {
|
||||||
return `${asset.id}:${asset.source}`;
|
if (entry.current.status === "ready") return entry.current.texture;
|
||||||
|
return entry.previousTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry | undefined) {
|
function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry) {
|
||||||
if (!entry) return;
|
disposeState(gl, entry.current);
|
||||||
if (entry.status === "ready") {
|
if (entry.previousTexture) gl.deleteTexture(entry.previousTexture);
|
||||||
gl.deleteTexture(entry.texture);
|
}
|
||||||
|
|
||||||
|
function disposeState(gl: WebGL2RenderingContext, state: TextureState) {
|
||||||
|
if (state.status === "ready") {
|
||||||
|
gl.deleteTexture(state.texture);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (entry.status === "loading") {
|
if (state.status === "loading") {
|
||||||
entry.image.onload = null;
|
state.image.onload = null;
|
||||||
entry.image.onerror = null;
|
state.image.onerror = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function useCanvasInput(
|
|||||||
): CanvasInputState {
|
): CanvasInputState {
|
||||||
const [isPanning, setIsPanning] = useState(false);
|
const [isPanning, setIsPanning] = useState(false);
|
||||||
const brushSession = useRef<BrushSession>();
|
const brushSession = useRef<BrushSession>();
|
||||||
|
const brushSessionId = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -64,6 +65,7 @@ export function useCanvasInput(
|
|||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const brush = beginBrushSession(state.document, state.editor, viewportPointToDocumentPoint(inputEvent.position, state.editor.viewport));
|
const brush = beginBrushSession(state.document, state.editor, viewportPointToDocumentPoint(inputEvent.position, state.editor.viewport));
|
||||||
if (brush) {
|
if (brush) {
|
||||||
|
brushSessionId.current += 1;
|
||||||
brushSession.current = brush;
|
brushSession.current = brush;
|
||||||
canvas.setPointerCapture(event.pointerId);
|
canvas.setPointerCapture(event.pointerId);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -99,9 +101,10 @@ export function useCanvasInput(
|
|||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
const inputEvent = pointerInputEventFromPointerEvent(event);
|
const inputEvent = pointerInputEventFromPointerEvent(event);
|
||||||
if (brushSession.current) {
|
if (brushSession.current) {
|
||||||
|
const activeSessionId = brushSessionId.current;
|
||||||
const point = viewportPointToDocumentPoint(inputEvent.position, store.getState().editor.viewport);
|
const point = viewportPointToDocumentPoint(inputEvent.position, store.getState().editor.viewport);
|
||||||
void updateBrushSession({ store, session: brushSession.current, point }).then((nextSession) => {
|
void updateBrushSession({ store, session: brushSession.current, point }).then((nextSession) => {
|
||||||
brushSession.current = nextSession;
|
if (brushSessionId.current === activeSessionId) brushSession.current = nextSession;
|
||||||
});
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -120,6 +123,7 @@ export function useCanvasInput(
|
|||||||
const handlePointerUp = (event: PointerEvent) => {
|
const handlePointerUp = (event: PointerEvent) => {
|
||||||
const inputEvent = pointerInputEventFromPointerEvent(event);
|
const inputEvent = pointerInputEventFromPointerEvent(event);
|
||||||
if (brushSession.current) {
|
if (brushSession.current) {
|
||||||
|
brushSessionId.current += 1;
|
||||||
brushSession.current = undefined;
|
brushSession.current = undefined;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user