Address issue where drawing operations on offset, cropped Layers would not use correct coordinate space

This commit is contained in:
Igor Zinken
2025-03-15 10:19:27 +01:00
parent 9ca9bbb732
commit 83d8e7af4d
6 changed files with 18 additions and 14 deletions

View File

@@ -52,9 +52,9 @@ export const usesInteractionPane = ( tool: ToolTypes ): boolean => PANE_TYPES.in
export const SELECTION_TOOLS = [ ToolTypes.SELECTION, ToolTypes.LASSO, ToolTypes.WAND ];
export const canDraw = ( activeDocument: Document, activeLayer: Layer, activeLayerMask: HTMLCanvasElement | null ): boolean => {
export const canDraw = ( activeDocument: Document, activeLayer: Layer, activeLayerMask?: HTMLCanvasElement ): boolean => {
return activeDocument &&
(( activeLayer?.mask !== null && activeLayer.mask === activeLayerMask ) || activeLayer?.type === LayerTypes.LAYER_GRAPHIC );
(( !!activeLayer?.mask && activeLayer.mask === activeLayerMask ) || activeLayer?.type === LayerTypes.LAYER_GRAPHIC );
};
// we cannot draw in selection if a layer is mirrored (see https://github.com/igorski/bitmappery/issues/5)

View File

@@ -396,9 +396,12 @@ class InteractionPane extends sprite {
}
draw( ctx: CanvasRenderingContext2D, viewport: Viewport ): void {
const document = this.getActiveDocument();
if ( !document ) {
return; // pane was active prior to Document closing
}
let { activeSelection, invertSelection, width, height } = document;
// render selection outline
let { invertSelection, width, height } = this.getActiveDocument();
const { activeSelection } = this.getActiveDocument();
if ( /*this.mode === InteractionModes.MODE_SELECTION && */ activeSelection?.length > 0 ) {
for ( let shape of activeSelection ) {
const connectToPointer = shape === activeSelection.at( -1 );
@@ -523,7 +526,9 @@ function calculateSelectionSize( firstPoint: Point, destX: number, destY: number
function syncSelection(): void {
const { getters } = getCanvasInstance().store;
getSpriteForLayer( getters.activeLayer )?.setSelection( getters.activeDocument );
if ( getters.activeLayer ) {
getSpriteForLayer( getters.activeLayer )?.setSelection( getters.activeDocument );
}
}
function storeSelectionHistory( document: Document, optPreviousSelection: Selection = [], optType = "" ): void {

View File

@@ -418,8 +418,7 @@ export default class LayerSprite extends ZoomableSprite {
}
getPaintSize(): Size {
const source = this.getPaintSource();
return { width: source.width, height: source.height };
return this.canvas.getActiveDocument(); // always use the Document size to allow drawing on offset, cropped Layers
/*
// depending on zoom level, the interpolation when committing the drawableCanvas content onto the source
// may benefit from a higher resolution when drawing on a zoomed in canvas. But maybe negligible and not worth memory overhead

View File

@@ -62,8 +62,8 @@ const DocumentModule: Module<DocumentState, any> = {
activeLayer: ( state: DocumentState, getters: any ): Layer => {
return getters.layers?.[ state.activeLayerIndex ];
},
activeLayerMask: ( state: DocumentState, getters: any ): HTMLCanvasElement | null => {
return ( state.maskActive && getters.activeLayer.mask ) || null;
activeLayerMask: ( state: DocumentState, getters: any ): HTMLCanvasElement | undefined => {
return ( state.maskActive && getters.activeLayer?.mask ) || undefined;
},
activeLayerEffects: ( _: DocumentState, getters: any ): Effects => {
return getters.activeLayer?.effects || {};

View File

@@ -22,14 +22,14 @@ describe( "tool types", () => {
LayerTypes.LAYER_IMAGE, LayerTypes.LAYER_TEXT,
])( `should not consider a "%s"-layer to be drawable`, ( type: LayerTypes ) => {
const layer = LayerFactory.create({ type });
expect( canDraw( document, layer, null )).toBe( false );
expect( canDraw( document, layer, undefined )).toBe( false );
});
it.each([
LayerTypes.LAYER_IMAGE, LayerTypes.LAYER_TEXT,
])( `should not consider a "%s"-layer with an unselected mask to be drawable`, ( type: LayerTypes ) => {
const layer = LayerFactory.create({ type, mask: createMockCanvasElement() });
expect( canDraw( document, layer, null )).toBe( false );
expect( canDraw( document, layer, undefined )).toBe( false );
});
it.each([
@@ -41,7 +41,7 @@ describe( "tool types", () => {
it( `should consider a "${LayerTypes.LAYER_GRAPHIC}"-layer to be drawable`, () => {
const layer = LayerFactory.create({ type: LayerTypes.LAYER_GRAPHIC });
expect( canDraw( document, layer, null )).toBe( true );
expect( canDraw( document, layer, undefined )).toBe( true );
});
});
});

View File

@@ -98,10 +98,10 @@ describe( "Vuex document module", () => {
activeLayer: LayerFactory.create({ name: "layer1" }),
};
// null because mask is not active
expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toBeNull();
expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toBeUndefined();
state.maskActive = true;
// null because layer has no mask drawable
expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toBeNull();
expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toBeUndefined();
mockedGetters.activeLayer.mask = createMockCanvasElement();
expect( getters.activeLayerMask( state, mockedGetters, {}, {} )).toEqual( mockedGetters.activeLayer.mask );
});