Skip to content

Feature Request: Added Event Handlers#17

Merged
jonobr1 merged 2 commits intomainfrom
16-event-handlers
Nov 22, 2025
Merged

Feature Request: Added Event Handlers#17
jonobr1 merged 2 commits intomainfrom
16-event-handlers

Conversation

@jonobr1
Copy link
Owner

@jonobr1 jonobr1 commented Nov 22, 2025

All shape components support event handlers:

  • Circle
  • Rectangle
  • RoundedRectangle
  • Ellipse
  • Line
  • Path
  • Points
  • Polygon
  • Star
  • ArcSegment
  • Text
  • Sprite
  • ImageSequence
  • Group (for hierarchical event handling)

Available Event Handlers

All components support these 12 event handlers (matching React Three Fiber's API):

Mouse Events

  • onClick - Click on the shape
  • onContextMenu - Right-click/context menu
  • onDoubleClick - Double-click

Pointer Events

  • onPointerDown - Pointer/touch starts on shape
  • onPointerUp - Pointer/touch releases on shape
  • onPointerMove - Pointer moves while over shape
  • onPointerCancel - Pointer event cancelled

Hover Events

  • onPointerOver - Pointer enters shape (with bubbling)
  • onPointerOut - Pointer leaves shape (with bubbling)
  • onPointerEnter - Pointer enters shape (no bubbling)
  • onPointerLeave - Pointer leaves shape (no bubbling)

Scroll Event

  • onWheel - Mouse wheel/trackpad scroll over shape

Example Usage

  <Circle
    radius={50}
    fill="blue"
    onClick={(e) => console.log('Clicked!', e.point)}
    onPointerOver={(e) => console.log('Hovering')}
    onPointerLeave={(e) => console.log('Left')}
  />

  <Group onClick={(e) => console.log('Group clicked')}>
    <Rectangle width={100} height={100} />
    <Circle radius={20} />
  </Group>

Event Object Properties

Each handler receives a TwoEvent object with:

  • nativeEvent - Original DOM event
  • target - Shape that was directly hit
  • currentTarget - Shape with the handler (may be parent due to bubbling)
  • point - Coordinates in Two.js space (center-origin: {x, y})
  • stopPropagation() - Stop event from bubbling to parent groups
  • stopped - Whether propagation was stopped

Events bubble up through Group hierarchies just like DOM events!

Introduces a unified event system for react-two.js components, enabling R3F-style event handlers (e.g., onClick, onPointerDown) for all shape and group components. Adds an Events module for hit testing and event dispatch, updates Context and Provider to manage event registration, and refactors all shape components to extract and register event handlers from props.
Introduces getWorldCoordinates to convert DOM event coordinates to canvas-relative world-space. Refactors Provider event handlers to use getWorldCoordinates for hit testing, improving coordinate accuracy and simplifying event handling logic.
@jonobr1 jonobr1 requested a review from Copilot November 22, 2025 02:40
@jonobr1 jonobr1 merged commit 0fc9418 into main Nov 22, 2025
6 checks passed
@jonobr1 jonobr1 deleted the 16-event-handlers branch November 22, 2025 02:42
@jonobr1
Copy link
Owner Author

jonobr1 commented Nov 22, 2025

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements React Three Fiber-style event handlers for all shape components in react-two.js, enabling interactive graphics with mouse and pointer events. The event system includes bubbling support through Group hierarchies and provides 12 event handler types (onClick, onPointerOver, onPointerMove, etc.) that match the R3F API.

  • Added comprehensive event handling system with R3F-compatible API
  • Implemented event bubbling through Group component hierarchies
  • Updated all 14 shape components to support event handlers

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
lib/Events.ts New file implementing the core event system with hit testing, coordinate conversion, and event bubbling logic
lib/Provider.tsx Added event listener setup on canvas, event shape registration/unregistration, and onPointerMissed support
lib/Context.ts Extended context interface to include registerEventShape and unregisterEventShape functions
lib/Properties.ts Re-exported event handler types for convenient importing
lib/Circle.tsx Added event handler support with prop extraction and registration logic
lib/Rectangle.tsx Added event handler support with prop extraction and registration logic
lib/RoundedRectangle.tsx Added event handler support with prop extraction and registration logic
lib/Ellipse.tsx Added event handler support with prop extraction and registration logic
lib/Line.tsx Added event handler support with prop extraction and registration logic
lib/Path.tsx Added event handler support with prop extraction and registration logic
lib/Points.tsx Added event handler support with prop extraction and registration logic
lib/Polygon.tsx Added event handler support with prop extraction and registration logic
lib/Star.tsx Added event handler support with prop extraction and registration logic
lib/ArcSegment.tsx Added event handler support with prop extraction and registration logic
lib/Text.tsx Added event handler support with prop extraction and registration logic
lib/Sprite.tsx Added event handler support with prop extraction and registration logic
lib/ImageSequence.tsx Added event handler support with prop extraction and registration logic
lib/Image.tsx Added event handler support with prop extraction and registration logic
lib/Group.tsx Added event handler support and context propagation for event bubbling
src/Playground.tsx Added interactive examples demonstrating hover and click event handlers
package-lock.json Updated two.js peer dependency to point to GitHub repository and bumped version to v0.8.22

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +48
// Extract event handlers from props
const { eventHandlers, shapeProps } = useMemo(() => {
const eventHandlers: Partial<EventHandlers> = {};
const shapeProps: Record<string, unknown> = {};

for (const key in props) {
if (EVENT_HANDLER_NAMES.includes(key as keyof EventHandlers)) {
eventHandlers[key as keyof EventHandlers] = props[
key as keyof EventHandlers
] as any;
} else {
shapeProps[key] = (props as any)[key];
}
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Suggested change
// Extract event handlers from props
const { eventHandlers, shapeProps } = useMemo(() => {
const eventHandlers: Partial<EventHandlers> = {};
const shapeProps: Record<string, unknown> = {};
for (const key in props) {
if (EVENT_HANDLER_NAMES.includes(key as keyof EventHandlers)) {
eventHandlers[key as keyof EventHandlers] = props[
key as keyof EventHandlers
] as any;
} else {
shapeProps[key] = (props as any)[key];
}
}
return { eventHandlers, shapeProps };
}, [props]);
// Extract event handlers and shape props keys from props
const eventHandlerKeys = Object.keys(props).filter(key =>
EVENT_HANDLER_NAMES.includes(key as keyof EventHandlers)
);
const shapePropKeys = Object.keys(props).filter(key =>
!EVENT_HANDLER_NAMES.includes(key as keyof EventHandlers)
);
const { eventHandlers, shapeProps } = useMemo(() => {
const eventHandlers: Partial<EventHandlers> = {};
const shapeProps: Record<string, unknown> = {};
for (const key of eventHandlerKeys) {
eventHandlers[key as keyof EventHandlers] = props[
key as keyof EventHandlers
] as any;
}
for (const key of shapePropKeys) {
shapeProps[key] = (props as any)[key];
}
return { eventHandlers, shapeProps };
}, [
...eventHandlerKeys.map(key => props[key]),
...shapePropKeys.map(key => props[key])
]);

Copilot uses AI. Check for mistakes.
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Suggested change
}, [props]);
}, [Object.keys(props).join(','), ...Object.values(props)]);

Copilot uses AI. Check for mistakes.
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Suggested change
}, [props]);
}, [...Object.values(props)]);

Copilot uses AI. Check for mistakes.
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Suggested change
}, [props]);
}, [...Object.keys(props), ...Object.values(props)]);

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +54
// Extract event handlers from props
const { eventHandlers, shapeProps } = useMemo(() => {
const eventHandlers: Partial<EventHandlers> = {};
const shapeProps: Record<string, unknown> = {};

for (const key in props) {
if (EVENT_HANDLER_NAMES.includes(key as keyof EventHandlers)) {
eventHandlers[key as keyof EventHandlers] = props[
key as keyof EventHandlers
] as any;
} else {
shapeProps[key] = (props as any)[key];
}
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Suggested change
// Extract event handlers from props
const { eventHandlers, shapeProps } = useMemo(() => {
const eventHandlers: Partial<EventHandlers> = {};
const shapeProps: Record<string, unknown> = {};
for (const key in props) {
if (EVENT_HANDLER_NAMES.includes(key as keyof EventHandlers)) {
eventHandlers[key as keyof EventHandlers] = props[
key as keyof EventHandlers
] as any;
} else {
shapeProps[key] = (props as any)[key];
}
}
return { eventHandlers, shapeProps };
}, [props]);
// Extract event handler props and shape props before useMemo
const eventHandlerProps: Partial<EventHandlers> = {};
const shapePropsRaw: Record<string, unknown> = {};
for (const key in props) {
if (EVENT_HANDLER_NAMES.includes(key as keyof EventHandlers)) {
eventHandlerProps[key as keyof EventHandlers] = props[key as keyof EventHandlers] as any;
} else {
shapePropsRaw[key] = (props as any)[key];
}
}
// Memoize eventHandlers and shapeProps based on their values, not the whole props object
const { eventHandlers, shapeProps } = useMemo(() => {
return {
eventHandlers: eventHandlerProps,
shapeProps: shapePropsRaw,
};
}, [
...Object.values(eventHandlerProps),
...Object.values(shapePropsRaw),
]);

Copilot uses AI. Check for mistakes.
canvas.removeEventListener('pointermove', handlePointerMove);
canvas.removeEventListener('pointercancel', handlePointerCancel);
};
}, [state.two, props, registerEventShape, unregisterEventShape]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props dependency in this useEffect will cause all event listeners to be removed and re-attached whenever any prop changes (including unrelated props like children). This is inefficient and could cause performance issues.

Consider extracting onPointerMissed to a ref or using props.onPointerMissed directly in the dependency array instead of the entire props object.

Copilot uses AI. Check for mistakes.
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Copilot uses AI. Check for mistakes.
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Suggested change
}, [props]);
}, [Object.entries(props)]);

Copilot uses AI. Check for mistakes.
}

return { eventHandlers, shapeProps };
}, [props]);
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props object dependency will cause this useMemo to re-run on every render because props is a new object each time. This defeats the purpose of useMemo and will cause the eventHandlers object to be recreated on every render, triggering unnecessary re-registration of event handlers.

Consider using a more stable dependency array that only includes the actual props that might change, or extract individual props before the useMemo.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants