Skip to content

Feature Request: Add useZUI hook for zoom/pan functionality #7

@jonobr1

Description

@jonobr1

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
): ZUIControls

Basic 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 | null to Context interface
  • Expose domElement from Provider (Canvas component)
  • Store reference to two.renderer.domElement in 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 UseZUIOptions and ZUIControls TypeScript interfaces
  • Implement useZUI hook with:
    • ZUI instance management via useRef
    • Reactive state for zoom/scale via useState
    • Initialization effect with cleanup
    • Apply zoom limits from options
  • Export stable method wrappers using useCallback:
    • zoomBy, zoomSet, translateSurface, reset
    • clientToSurface, 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 zoomBy at mouse cursor position
  • Implement mouse drag pan handler
    • Track drag state (mousedown/mousemove/mouseup)
    • Calculate translation delta
    • Call translateSurface with 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 useZUI hook
  • Export UseZUIOptions and ZUIControls types
  • 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 surfaceMatrix for zoom/pan state
  • Updates the Group's translation and scale properties
  • Provides bidirectional coordinate conversion (client ↔ surface)
  • Requires manual event listener setup (not automatic)

Key Methods:

  • zoomBy(byF, clientX, clientY) - Incremental zoom
  • zoomSet(zoom, clientX, clientY) - Absolute zoom
  • clientToSurface(x, y, z?) - Screen coords → world coords
  • surfaceToClient(x, y, z?) - World coords → screen coords
  • translateSurface(x, y) - Pan the view
  • addLimits(min, max) - Set zoom constraints
  • reset() - Reset to initial state

Design Decisions

  1. Hook-based pattern: Natural React pattern, composable and reusable
  2. Ref-based Group targeting: Works with existing component patterns
  3. Optional automatic events: Built-in handlers can be disabled for custom logic
  4. Imperative API exposure: Matches Two.ZUI while staying React-friendly
  5. Context integration: Leverages existing Canvas context for DOM element access
  6. TypeScript first: Full type safety with proper interfaces
  7. Automatic cleanup: Event listeners cleaned up on unmount
  8. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions