-
-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Summary
Add a useZUI React hook to integrate Two.js's ZUI (Zooming User Interface) functionality naturally into the react-two.js library. This will enable Google Maps or Adobe Illustrator-style zoom and pan interactions for Two.js scenes.
Motivation
Two.js provides a powerful ZUI class (in extras/jsm/zui.js) that enables interactive zoom and pan functionality. However, it requires imperative setup with manual event listener management. A React hook would make this functionality declarative, easy to use, and properly integrated with the library's existing patterns.
Proposed API
Hook Signature
interface UseZUIOptions {
minZoom?: number; // Minimum zoom level (default: -Infinity)
maxZoom?: number; // Maximum zoom level (default: Infinity)
zoomDelta?: number; // Amount to zoom per wheel event (default: 0.05)
enableMouse?: boolean; // Enable mouse drag to pan (default: true)
enableTouch?: boolean; // Enable touch gestures (default: true)
enableWheel?: boolean; // Enable wheel to zoom (default: true)
domElement?: HTMLElement | null; // Override target element
}
interface ZUIControls {
// Core methods
zoomBy: (byF: number, clientX: number, clientY: number) => void;
zoomSet: (zoom: number, clientX: number, clientY: number) => void;
translateSurface: (x: number, y: number) => void;
reset: () => void;
// Coordinate conversion
clientToSurface: (x: number, y: number, z?: number) => { x: number; y: number; z: number };
surfaceToClient: (x: number, y: number, z?: number) => { x: number; y: number; z: number };
// Reactive state
zoom: number;
scale: number;
// Advanced: Direct ZUI instance access
instance: ZUI | null;
}
function useZUI(
groupRef: RefObject<RefGroup>,
options?: UseZUIOptions
): ZUIControlsBasic Usage Example
import { Canvas, Group, Circle, useZUI } from 'react-two.js';
function ZoomableScene() {
const groupRef = useRef<RefGroup>(null);
const { zoom, scale, reset } = useZUI(groupRef, {
minZoom: 0.5,
maxZoom: 3.0,
zoomDelta: 0.05,
});
return (
<>
<button onClick={reset}>Reset View</button>
<div>Zoom: {zoom.toFixed(2)} | Scale: {scale.toFixed(2)}</div>
<Group ref={groupRef}>
<Circle radius={50} fill="red" />
<Circle x={100} radius={30} fill="blue" />
</Group>
</>
);
}
function App() {
return (
<Canvas fullscreen>
<ZoomableScene />
</Canvas>
);
}Advanced Usage Example
function AdvancedZUI() {
const groupRef = useRef<RefGroup>(null);
const { zoomSet, clientToSurface } = useZUI(groupRef, {
enableMouse: false, // Disable automatic event handling
});
const handleCustomZoom = (e: React.MouseEvent) => {
// Get world coordinates from screen coordinates
const worldPos = clientToSurface(e.clientX, e.clientY);
console.log('Clicked at world position:', worldPos);
// Custom zoom logic
zoomSet(2.0, e.clientX, e.clientY);
};
return (
<div onClick={handleCustomZoom}>
<Group ref={groupRef}>
{/* content */}
</Group>
</div>
);
}Implementation Phases
Phase 1: Context Enhancement
Files: lib/Context.ts, lib/Provider.tsx
- Add
domElement: HTMLElement | nullto Context interface - Expose
domElementfrom Provider (Canvas component) - Store reference to
two.renderer.domElementin context - Update Context types and exports
Deliverable: Context now provides access to the DOM element for event listeners
Phase 2: Core Hook Implementation
Files: lib/ZUI.tsx (new file)
- Import ZUI from
two.js/extras/jsm/zui.js - Create
UseZUIOptionsandZUIControlsTypeScript interfaces - Implement
useZUIhook with:- ZUI instance management via
useRef - Reactive state for zoom/scale via
useState - Initialization effect with cleanup
- Apply zoom limits from options
- ZUI instance management via
- Export stable method wrappers using
useCallback:zoomBy,zoomSet,translateSurface,resetclientToSurface,surfaceToClient
- Handle edge cases (no group ref, no Two instance, etc.)
Deliverable: Core hook that manages ZUI lifecycle and exposes imperative API
Phase 3: Event Handler Implementation
Files: lib/ZUI.tsx
- Implement mouse wheel zoom handler
- Prevent default scroll behavior
- Calculate zoom delta based on wheel direction
- Call
zoomByat mouse cursor position
- Implement mouse drag pan handler
- Track drag state (mousedown/mousemove/mouseup)
- Calculate translation delta
- Call
translateSurfacewith delta
- Implement touch gesture handlers
- Single touch: pan
- Pinch gesture: zoom
- Handle touch start/move/end events
- Add event listener lifecycle management
- Attach listeners based on options flags
- Proper cleanup on unmount
- Handle window vs element listeners appropriately
Deliverable: Automatic event handling for common zoom/pan interactions
Phase 4: Integration & Export
Files: lib/main.ts
- Export
useZUIhook - Export
UseZUIOptionsandZUIControlstypes - Update package exports
- Verify tree-shaking works correctly
Deliverable: Hook available in public API
Phase 5: Testing
Files: lib/__tests__/ZUI.test.tsx (new file)
- Test hook initialization and cleanup
- Test zoom methods (zoomBy, zoomSet)
- Test pan method (translateSurface)
- Test reset functionality
- Test coordinate conversion methods
- Test with/without options
- Test state updates
- Mock ZUI class for isolated testing
- Test event handler attachment/cleanup
Deliverable: Comprehensive test coverage for useZUI hook
Phase 6: Documentation & Examples
Files: README.md, example apps, documentation site
- Add useZUI section to main README
- Create interactive example in dev app (
src/App.tsx) - Document all options and return values
- Add troubleshooting section
- Include performance considerations
- Add TypeScript usage examples
- Document coordinate system conversions
Deliverable: Complete documentation and working examples
Technical Considerations
How Two.ZUI Works
Two.ZUI is a class that:
- Wraps a Two.js Group with transformation management
- Maintains a
surfaceMatrixfor zoom/pan state - Updates the Group's
translationandscaleproperties - Provides bidirectional coordinate conversion (client ↔ surface)
- Requires manual event listener setup (not automatic)
Key Methods:
zoomBy(byF, clientX, clientY)- Incremental zoomzoomSet(zoom, clientX, clientY)- Absolute zoomclientToSurface(x, y, z?)- Screen coords → world coordssurfaceToClient(x, y, z?)- World coords → screen coordstranslateSurface(x, y)- Pan the viewaddLimits(min, max)- Set zoom constraintsreset()- Reset to initial state
Design Decisions
- Hook-based pattern: Natural React pattern, composable and reusable
- Ref-based Group targeting: Works with existing component patterns
- Optional automatic events: Built-in handlers can be disabled for custom logic
- Imperative API exposure: Matches Two.ZUI while staying React-friendly
- Context integration: Leverages existing Canvas context for DOM element access
- TypeScript first: Full type safety with proper interfaces
- Automatic cleanup: Event listeners cleaned up on unmount
- State exposure: Optional reactive state for UI updates (zoom/scale display)
Performance Considerations
- ZUI transformations don't trigger React re-renders (uses direct Two.js manipulation)
- Optional state updates only for zoom/scale values when needed for UI
- Event handlers use refs to avoid recreating on every render
- Multiple ZUI instances can coexist (each hook manages its own)
Browser Compatibility
- Requires ES6+ for Two.js ZUI class
- Touch events for mobile support
- Pointer events could be considered for future enhancement
Resources
Success Criteria
- useZUI hook successfully integrates Two.ZUI functionality
- Works with all existing components (Group, shapes, etc.)
- Automatic event handling works for mouse, wheel, and touch
- Manual control mode allows custom interaction patterns
- Full TypeScript support with proper types
- Comprehensive tests with good coverage
- Documentation includes examples and API reference
- No breaking changes to existing API
- Performance is equivalent to direct Two.ZUI usage
Labels: enhancement, feature request, hooks
Milestone: v0.3.0