From a3037e987e8fefb1311c441e22fa637294f8d030 Mon Sep 17 00:00:00 2001 From: Ni55aN Date: Tue, 7 Jul 2026 20:10:51 +0000 Subject: [PATCH 1/2] style: apply lint auto-fixes Remove stale eslint-disable comments and fix void-expression warnings from rete lint --fix. Co-authored-by: Cursor --- src/base.ts | 2 +- src/connection-view.ts | 4 +++- src/content.ts | 1 - src/extensions/restrictor.ts | 1 - src/extensions/zoom-at.ts | 2 +- src/index.ts | 8 ++++++-- src/node-view.ts | 4 +++- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/base.ts b/src/base.ts index e1c404a..44542ac 100644 --- a/src/base.ts +++ b/src/base.ts @@ -18,7 +18,7 @@ interface ConnectionView { interface Content { holder: HTMLElement add(element: HTMLElement): void - // eslint-disable-next-line no-undef + reorder(target: HTMLElement, next: ChildNode | null): void remove(element: HTMLElement): void } diff --git a/src/connection-view.ts b/src/connection-view.ts index 4a2db9d..99179f0 100644 --- a/src/connection-view.ts +++ b/src/connection-view.ts @@ -12,6 +12,8 @@ export class ConnectionView { this.element.style.position = 'absolute' this.element.style.left = '0' this.element.style.top = '0' - this.element.addEventListener('contextmenu', event => events.contextmenu(event)) + this.element.addEventListener('contextmenu', event => { + events.contextmenu(event) + }) } } diff --git a/src/content.ts b/src/content.ts index 41c743e..7bbe723 100644 --- a/src/content.ts +++ b/src/content.ts @@ -18,7 +18,6 @@ export class Content { this.holder.appendChild(element) } - // eslint-disable-next-line no-undef async reorder(target: HTMLElement, next: ChildNode | null) { if (!this.holder.contains(target)) { throw new Error(`content doesn't have 'target' for reordering`) diff --git a/src/extensions/restrictor.ts b/src/extensions/restrictor.ts index e9d44f9..4b3390f 100644 --- a/src/extensions/restrictor.ts +++ b/src/extensions/restrictor.ts @@ -50,7 +50,6 @@ export function restrictor(plugin: AreaPlugin(plugin: AreaPlugin, nodes: NodeRef[], params?: Params) { const { scale = 0.9 } = params || {} const editor = plugin.parentScope>(NodeEditor) diff --git a/src/index.ts b/src/index.ts index a930465..10d29cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -203,8 +203,12 @@ export class AreaPlugin exten */ public destroy() { this.container.removeEventListener('contextmenu', this.onContextMenu) - Array.from(this.connectionViews.keys()).forEach(id => this.removeConnectionView(id)) - Array.from(this.nodeViews.keys()).forEach(id => this.removeNodeView(id)) + Array.from(this.connectionViews.keys()).forEach(id => { + this.removeConnectionView(id) + }) + Array.from(this.nodeViews.keys()).forEach(id => { + this.removeNodeView(id) + }) this.area.destroy() } } diff --git a/src/node-view.ts b/src/node-view.ts index 3a81337..0a91f7c 100644 --- a/src/node-view.ts +++ b/src/node-view.ts @@ -27,7 +27,9 @@ export class NodeView { this.position = { x: 0, y: 0 } void this.translate(0, 0) - this.element.addEventListener('contextmenu', event => this.events.contextmenu(event)) + this.element.addEventListener('contextmenu', event => { + this.events.contextmenu(event) + }) this.dragHandler = new Drag() this.dragHandler.initialize( From 6eaa35f44d89d7c7783b0229c63e430a58851770 Mon Sep 17 00:00:00 2001 From: Ni55aN Date: Tue, 7 Jul 2026 20:11:00 +0000 Subject: [PATCH 2/2] fix(zoom): normalize wheel delta for touchpad pinch zoom Scale wheel input by deltaMode, cap steps at intensity to preserve mouse wheel behavior, and skip zero deltas. Add zoom unit and handler tests. Fixes #31 Co-authored-by: Cursor --- package.json | 3 +- src/zoom-wheel.ts | 32 ++++++++++ src/zoom.ts | 7 ++- test/zoom-wheel.test.ts | 94 ++++++++++++++++++++++++++++ test/zoom.test.ts | 133 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 6 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 src/zoom-wheel.ts create mode 100644 test/zoom-wheel.test.ts create mode 100644 test/zoom.test.ts diff --git a/package.json b/package.json index ef9cf02..7984ec9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "build": "rete build -c rete.config.ts", "doc": "rete doc", - "lint": "rete lint" + "lint": "rete lint", + "test": "rete test" }, "author": "Vitaliy Stoliarov", "license": "MIT", diff --git a/src/zoom-wheel.ts b/src/zoom-wheel.ts new file mode 100644 index 0000000..b4df998 --- /dev/null +++ b/src/zoom-wheel.ts @@ -0,0 +1,32 @@ +/** Pixels per wheel line ({@link WheelEvent.DOM_DELTA_LINE}) */ +export const LINE_HEIGHT_PX = 8 + +/** Pixels per wheel page ({@link WheelEvent.DOM_DELTA_PAGE}) */ +export const PAGE_HEIGHT_PX = 24 + +const DOM_DELTA_LINE = 1 +const DOM_DELTA_PAGE = 2 + +/** + * Converts wheel delta to pixels based on {@link WheelEvent.deltaMode}. + * @internal + */ +export function wheelDeltaToPixels(delta: number, mode: number): number { + if (mode === DOM_DELTA_LINE) return delta * LINE_HEIGHT_PX + if (mode === DOM_DELTA_PAGE) return delta * PAGE_HEIGHT_PX + + return delta +} + +/** + * Maps normalized wheel movement to a zoom delta, capped at `intensity`. + * @internal + */ +export function computeWheelZoomDelta(pixels: number, intensity: number): number { + if (pixels === 0) return 0 + + const unit = intensity / LINE_HEIGHT_PX + const raw = -pixels * unit + + return Math.sign(raw) * Math.min(intensity, Math.abs(raw)) +} diff --git a/src/zoom.ts b/src/zoom.ts index e37b31d..f4cb3f7 100644 --- a/src/zoom.ts +++ b/src/zoom.ts @@ -1,3 +1,4 @@ +import { computeWheelZoomDelta, wheelDeltaToPixels } from './zoom-wheel' /** * Zoom source @@ -35,8 +36,12 @@ export class Zoom { protected wheel = (e: WheelEvent) => { e.preventDefault() + const pixels = wheelDeltaToPixels(e.deltaY, e.deltaMode) + const delta = computeWheelZoomDelta(pixels, this.intensity) + + if (delta === 0) return + const { left, top } = this.element.getBoundingClientRect() - const delta = -this.intensity * Math.sign(e.deltaY); const ox = (left - e.clientX) * delta const oy = (top - e.clientY) * delta diff --git a/test/zoom-wheel.test.ts b/test/zoom-wheel.test.ts new file mode 100644 index 0000000..c92e76b --- /dev/null +++ b/test/zoom-wheel.test.ts @@ -0,0 +1,94 @@ +import { + computeWheelZoomDelta, + LINE_HEIGHT_PX, + PAGE_HEIGHT_PX, + wheelDeltaToPixels +} from '../src/zoom-wheel' + +const intensity = 0.1 + +const DOM_DELTA_PIXEL = 0 +const DOM_DELTA_LINE = 1 +const DOM_DELTA_PAGE = 2 + +function signBasedWheelDelta(deltaY: number): number { + if (deltaY === 0) return 0 + + return -intensity * Math.sign(deltaY) +} + +function wheelZoomDelta(deltaY: number, deltaMode: number): number { + const pixels = wheelDeltaToPixels(deltaY, deltaMode) + + return computeWheelZoomDelta(pixels, intensity) +} + +describe('wheelDeltaToPixels', () => { + it('returns delta unchanged in DOM_DELTA_PIXEL mode', () => { + expect(wheelDeltaToPixels(120, DOM_DELTA_PIXEL)).toBe(120) + expect(wheelDeltaToPixels(0.05, DOM_DELTA_PIXEL)).toBe(0.05) + expect(wheelDeltaToPixels(-3, DOM_DELTA_PIXEL)).toBe(-3) + }) + + it('multiplies by line height in DOM_DELTA_LINE mode', () => { + expect(wheelDeltaToPixels(1, DOM_DELTA_LINE)).toBe(LINE_HEIGHT_PX) + expect(wheelDeltaToPixels(-2, DOM_DELTA_LINE)).toBe(-2 * LINE_HEIGHT_PX) + }) + + it('multiplies by page height in DOM_DELTA_PAGE mode', () => { + expect(wheelDeltaToPixels(1, DOM_DELTA_PAGE)).toBe(PAGE_HEIGHT_PX) + expect(wheelDeltaToPixels(-1, DOM_DELTA_PAGE)).toBe(-PAGE_HEIGHT_PX) + }) +}) + +describe('mouse-like wheel zoom', () => { + const cases = [ + { name: 'Chrome wheel down', deltaY: 120, deltaMode: DOM_DELTA_PIXEL, expected: -intensity }, + { name: 'Chrome wheel up', deltaY: -120, deltaMode: DOM_DELTA_PIXEL, expected: intensity }, + { name: 'Firefox wheel down', deltaY: 1, deltaMode: DOM_DELTA_LINE, expected: -intensity }, + { name: 'Firefox wheel up', deltaY: -1, deltaMode: DOM_DELTA_LINE, expected: intensity }, + { name: 'page scroll down', deltaY: 1, deltaMode: DOM_DELTA_PAGE, expected: -intensity } + ] + + cases.forEach(({ name, deltaY, deltaMode, expected }) => { + it(`${name} matches sign-based delta`, () => { + const delta = wheelZoomDelta(deltaY, deltaMode) + + expect(delta).toBe(expected) + expect(delta).toBe(signBasedWheelDelta(deltaY)) + }) + }) +}) + +describe('touchpad wheel zoom', () => { + const cases = [ + { name: 'pinch light', deltaY: 0.05, expected: -0.000625 }, + { name: 'pinch medium', deltaY: 0.5, expected: -0.00625 }, + { name: 'pinch strong', deltaY: 3, expected: -0.0375 } + ] + + cases.forEach(({ name, deltaY, expected }) => { + it(`${name} scales proportionally`, () => { + const delta = wheelZoomDelta(deltaY, DOM_DELTA_PIXEL) + + expect(delta).toBeCloseTo(expected) + expect(delta).not.toBe(signBasedWheelDelta(deltaY)) + }) + }) +}) + +describe('zero deltaY', () => { + it('ignores horizontal wheel and gesture end', () => { + expect(wheelZoomDelta(0, DOM_DELTA_PIXEL)).toBe(0) + expect(wheelZoomDelta(0, DOM_DELTA_PIXEL)).toBe(signBasedWheelDelta(0)) + }) +}) + +describe('small discrete wheel delta', () => { + it('uses proportional step below mouse cap threshold', () => { + const delta = wheelZoomDelta(4, DOM_DELTA_PIXEL) + + expect(delta).toBeCloseTo(-0.05) + expect(delta).not.toBe(signBasedWheelDelta(4)) + }) +}) diff --git a/test/zoom.test.ts b/test/zoom.test.ts new file mode 100644 index 0000000..6620aa2 --- /dev/null +++ b/test/zoom.test.ts @@ -0,0 +1,133 @@ +import { OnZoom, Zoom } from '../src/zoom' + +const intensity = 0.1 + +const DOM_DELTA_PIXEL = 0 +const DOM_DELTA_LINE = 1 +const DOM_DELTA_PAGE = 2 + +class TestableZoom extends Zoom { + public setup(element: HTMLElement, onzoom: OnZoom) { + this.element = element + this.onzoom = onzoom + } + + public triggerWheel(event: Partial) { + this.wheel(event as WheelEvent) + } +} + +function createWheelEvent(overrides: Partial = {}): Partial { + return { + deltaY: 0, + deltaMode: 0, + clientX: 50, + clientY: 50, + preventDefault: jest.fn(), + ...overrides + } +} + +function createHolder(): HTMLElement { + return { + getBoundingClientRect: () => ({ + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + x: 0, + y: 0, + toJSON: () => ({}) + }) + } as unknown as HTMLElement +} + +function triggerWheel(deltaY: number, deltaMode: number) { + const onzoom = jest.fn() + const zoom = new TestableZoom(intensity) + + zoom.setup(createHolder(), onzoom) + zoom.triggerWheel(createWheelEvent({ deltaY, deltaMode })) + + return onzoom +} + +describe('Zoom wheel handler', () => { + describe('mouse-like wheel zoom', () => { + const cases = [ + { name: 'Chrome wheel down', deltaY: 120, deltaMode: DOM_DELTA_PIXEL, expected: -intensity }, + { name: 'Chrome wheel up', deltaY: -120, deltaMode: DOM_DELTA_PIXEL, expected: intensity }, + { name: 'Firefox wheel down', deltaY: 1, deltaMode: DOM_DELTA_LINE, expected: -intensity }, + { name: 'Firefox wheel up', deltaY: -1, deltaMode: DOM_DELTA_LINE, expected: intensity }, + { name: 'page scroll down', deltaY: 1, deltaMode: DOM_DELTA_PAGE, expected: -intensity } + ] + + cases.forEach(({ name, deltaY, deltaMode, expected }) => { + it(name, () => { + const onzoom = triggerWheel(deltaY, deltaMode) + + expect(onzoom).toHaveBeenCalledWith( + expected, + expect.any(Number), + expect.any(Number), + 'wheel' + ) + }) + }) + }) + + describe('touchpad wheel zoom', () => { + const cases = [ + { name: 'pinch light', deltaY: 0.05, expected: -0.000625 }, + { name: 'pinch medium', deltaY: 0.5, expected: -0.00625 }, + { name: 'pinch strong', deltaY: 3, expected: -0.0375 } + ] + + cases.forEach(({ name, deltaY, expected }) => { + it(name, () => { + const onzoom = triggerWheel(deltaY, DOM_DELTA_PIXEL) + + expect(onzoom).toHaveBeenCalledWith( + expect.closeTo(expected), + expect.any(Number), + expect.any(Number), + 'wheel' + ) + }) + }) + }) + + describe('zero deltaY', () => { + it('ignores horizontal wheel and gesture end', () => { + const onzoom = triggerWheel(0, DOM_DELTA_PIXEL) + + expect(onzoom).not.toHaveBeenCalled() + }) + }) + + describe('small discrete wheel delta', () => { + it('uses proportional step below mouse cap threshold', () => { + const onzoom = triggerWheel(4, DOM_DELTA_PIXEL) + + expect(onzoom).toHaveBeenCalledWith( + expect.closeTo(-0.05), + expect.any(Number), + expect.any(Number), + 'wheel' + ) + }) + }) + + it('calls preventDefault on wheel', () => { + const onzoom = jest.fn() + const zoom = new TestableZoom(intensity) + const event = createWheelEvent({ deltaY: 120 }) + + zoom.setup(createHolder(), onzoom) + zoom.triggerWheel(event) + + expect(event.preventDefault).toHaveBeenCalled() + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 0edbe42..309b784 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,5 +5,5 @@ "DOM" ] }, - "include": ["src"] + "include": ["src", "test"] }