diff --git a/packages/vrender-core/__tests__/unit/render/rect-split-stroke.test.ts b/packages/vrender-core/__tests__/unit/render/rect-split-stroke.test.ts index 262b8131e..46194ff56 100644 --- a/packages/vrender-core/__tests__/unit/render/rect-split-stroke.test.ts +++ b/packages/vrender-core/__tests__/unit/render/rect-split-stroke.test.ts @@ -1,4 +1,5 @@ import { + DefaultRectRenderContribution, SplitRectAfterRenderContribution, SplitRectBeforeRenderContribution } from '../../../src/render/contributions/render/contributions/rect-contribution-render'; @@ -71,4 +72,153 @@ describe('rect split stroke contribution', () => { expect(context.lineTo).toHaveBeenCalledWith(10, 60); expect(context.stroke).toHaveBeenCalledTimes(1); }); + + test('emits the same outer-border path for positive and negative rect widths', () => { + const contribution = new DefaultRectRenderContribution(); + const positiveContext = { beginPath: jest.fn(), rect: jest.fn() }; + const negativeContext = { beginPath: jest.fn(), rect: jest.fn() }; + const rectAttribute = { + x: 92, + y: 20, + opacity: 1, + scaleX: 1, + scaleY: 1, + keepStrokeScale: true, + cornerRadius: 0, + cornerType: 'round', + outerBorder: { distance: 6 }, + innerBorder: { distance: 0 } + }; + + contribution.drawShape( + { attribute: { x: 92, y: 20, width: 8, height: 10, outerBorder: { distance: 6 } } } as any, + positiveContext as any, + 92, + 20, + true, + true, + true, + true, + rectAttribute as any, + {} as any + ); + contribution.drawShape( + { attribute: { x: 100, y: 20, x1: 92, y1: 30, outerBorder: { distance: 6 } } } as any, + negativeContext as any, + 100, + 20, + true, + true, + true, + true, + { ...rectAttribute, x: 100 } as any, + {} as any + ); + + expect(positiveContext.rect).toHaveBeenCalledWith(86, 14, 20, 22); + expect(negativeContext.rect).toHaveBeenCalledWith(86, 14, 20, 22); + }); + + test('extends a negative-width outer-border path across the physical bounds', () => { + const contribution = new DefaultRectRenderContribution(); + const context = { beginPath: jest.fn(), rect: jest.fn() }; + + contribution.drawShape( + { attribute: { x: 100, y: 20, x1: 92, y1: 30, outerBorder: { distance: 6 } } } as any, + context as any, + 100, + 20, + true, + true, + true, + true, + { + x: 100, + y: 20, + opacity: 1, + scaleX: 1, + scaleY: 1, + keepStrokeScale: true, + cornerRadius: 0, + cornerType: 'round', + outerBorder: { distance: 6 }, + innerBorder: { distance: 0 } + } as any, + {} as any + ); + + const [x, , width] = context.rect.mock.calls[0]; + expect([x, x + width]).toEqual([86, 106]); + }); + + test('resolves x1 width in attribute space when the draw origin includes dx', () => { + const contribution = new DefaultRectRenderContribution(); + const context = { beginPath: jest.fn(), rect: jest.fn() }; + + contribution.drawShape( + { + attribute: { + x: 100, + x1: 92, + y: 20, + height: 10, + dx: 10, + outerBorder: { distance: 6 } + } + } as any, + context as any, + 110, + 20, + true, + true, + true, + true, + { + x: 100, + y: 20, + opacity: 1, + scaleX: 1, + scaleY: 1, + keepStrokeScale: true, + cornerRadius: 0, + cornerType: 'round', + outerBorder: { distance: 6 }, + innerBorder: { distance: 0 } + } as any, + {} as any + ); + + expect(context.rect).toHaveBeenCalledWith(96, 14, 20, 22); + }); + + test('preserves the non-degenerate positive inner-border path', () => { + const contribution = new DefaultRectRenderContribution(); + const context = { beginPath: jest.fn(), rect: jest.fn() }; + + contribution.drawShape( + { attribute: { x: 92, y: 20, width: 8, height: 10, innerBorder: { distance: 2 } } } as any, + context as any, + 92, + 20, + true, + true, + true, + true, + { + x: 92, + y: 20, + opacity: 1, + scaleX: 1, + scaleY: 1, + keepStrokeScale: true, + cornerRadius: 0, + cornerType: 'round', + outerBorder: { distance: 0 }, + innerBorder: { distance: 2 } + } as any, + {} as any + ); + + expect(context.rect).toHaveBeenCalledWith(94, 22, 4, 6); + }); }); diff --git a/packages/vrender-core/src/render/contributions/render/contributions/rect-contribution-render.ts b/packages/vrender-core/src/render/contributions/render/contributions/rect-contribution-render.ts index e6924ca9d..48cbd1f44 100644 --- a/packages/vrender-core/src/render/contributions/render/contributions/rect-contribution-render.ts +++ b/packages/vrender-core/src/render/contributions/render/contributions/rect-contribution-render.ts @@ -68,8 +68,12 @@ export class DefaultRectRenderContribution implements IRectRenderContribution { let { width, height } = rect.attribute; - width = (width ?? x1 - x) || 0; - height = (height ?? y1 - y) || 0; + width = (width ?? x1 - originX) || 0; + height = (height ?? y1 - originY) || 0; + const borderX = width < 0 ? x + width : x; + const borderY = height < 0 ? y + height : y; + const borderWidth = width < 0 ? -width : width; + const borderHeight = height < 0 ? -height : height; const renderBorder = (borderStyle: Partial, key: 'outerBorder' | 'innerBorder') => { const doStroke = !!(borderStyle && borderStyle.stroke); @@ -77,13 +81,13 @@ export class DefaultRectRenderContribution implements IRectRenderContribution { const sign = key === 'outerBorder' ? -1 : 1; const { distance = rectAttribute[key].distance } = borderStyle; const d = keepStrokeScale ? (distance as number) : getScaledStroke(context, distance as number, context.dpr); - const nextX = x + sign * d; - const nextY = y + sign * d; + const nextX = borderX + sign * d; + const nextY = borderY + sign * d; const dw = d * 2; if (cornerRadius === 0 || (isArray(cornerRadius) && (cornerRadius).every(num => num === 0))) { // 不需要处理圆角 context.beginPath(); - context.rect(nextX, nextY, width - sign * dw, height - sign * dw); + context.rect(nextX, nextY, borderWidth - sign * dw, borderHeight - sign * dw); } else { context.beginPath(); @@ -92,8 +96,8 @@ export class DefaultRectRenderContribution implements IRectRenderContribution { context, nextX, nextY, - width - sign * dw, - height - sign * dw, + borderWidth - sign * dw, + borderHeight - sign * dw, cornerRadius, cornerType !== 'bevel' );