diff --git a/docs-src/compass.md b/docs-src/compass.md new file mode 100644 index 0000000..9ad62dd --- /dev/null +++ b/docs-src/compass.md @@ -0,0 +1,147 @@ +# 3D Compass + +The `Compass3D` component displays a 3D compass that shows cardinal directions (N, S, E, W) and vertical orientation (Up, Down). It helps users maintain spatial awareness when navigating 3D map views. + +## Basic Usage + +The simplest way to add a compass is to use the default overlay mode, which automatically positions the compass in the corner of your canvas: + +```tsx +import { Canvas, Compass3D } from 'react-three-map/maplibre'; +import Map from 'react-map-gl/maplibre'; + +function App() { + return ( + + + + + + ); +} +``` + +When `overlay={true}` (the default), the compass: +- Renders as a HUD element that stays fixed on screen +- Automatically syncs its orientation with the camera +- Uses the `alignment` and `margin` props for positioning + +## Screen Position + +Control where the compass appears using `alignment` and `margin`: + +```tsx +// Bottom-left corner with 40px margin + + +// Top-center with default margin + + +// Available alignments: +// 'top-left', 'top-right', 'bottom-left', 'bottom-right' +// 'top-center', 'bottom-center', 'center-left', 'center-right' +``` + +## Customizing Appearance + +Adjust the compass size and proportions: + +```tsx + +``` + +## World-Space Compass + +For a compass that exists in 3D world space rather than as a screen overlay, disable overlay mode and provide manual bearing/pitch values: + +```tsx +import { Canvas, Compass3D, useMap } from 'react-three-map/maplibre'; +import { useState, useEffect } from 'react'; + +function WorldSpaceCompass() { + const map = useMap(); + const [bearing, setBearing] = useState(0); + const [pitch, setPitch] = useState(0); + + useEffect(() => { + if (!map) return; + + const update = () => { + setBearing(map.getBearing()); + setPitch(map.getPitch()); + }; + + update(); + map.on('move', update); + map.on('rotate', update); + map.on('pitch', update); + + return () => { + map.off('move', update); + map.off('rotate', update); + map.off('pitch', update); + }; + }, [map]); + + return ( + + ); +} +``` + +## Axis Convention + +The compass follows the library's axis convention: + +| Axis | Color | Direction | +|------|-------|-----------| +| X | Red | East (+X) / West (-X) | +| Y | Green | Up (+Y) / Down (-Y) | +| Z | Blue | South (+Z) / North (-Z) | + +## CompassOverlay Component + +For advanced use cases, the `CompassOverlay` component renders the compass in a separate React Three Fiber canvas that floats above the map. This can be useful when you want complete control over the compass rendering: + +```tsx +import { CompassOverlay } from 'react-three-map'; +import Map, { useMap } from 'react-map-gl/maplibre'; + +function App() { + return ( + + + + ); +} +``` + +The `CompassOverlay`: +- Creates its own R3F canvas layered above the map +- Syncs bearing and pitch with the MapLibre camera +- Provides a semi-transparent background + +## Props Reference + +See the full API documentation for all configuration options in the API reference section. diff --git a/src/components/compass-overlay.tsx b/src/components/compass-overlay.tsx index 09f71b8..4431249 100644 --- a/src/components/compass-overlay.tsx +++ b/src/components/compass-overlay.tsx @@ -1,22 +1,91 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +/** + * @packageDocumentation + * Screen-space compass overlay component. + */ +import { useEffect, useMemo, useRef, useState } from 'react'; import { Canvas } from '@react-three/fiber'; import { useMap } from 'react-map-gl/maplibre'; import { Compass3D } from './compass-3d'; +/** + * Props for the CompassOverlay component. + * + * @example Basic usage + * ```tsx + * + * ``` + * + * @example Custom size and position + * ```tsx + * + * ``` + */ export interface CompassOverlayProps { - /** Size of the overlay square in px */ + /** + * Size of the overlay square in pixels. + * @defaultValue 200 + */ size?: number; - /** CSS inset from bottom/left */ + + /** + * CSS inset from bottom-left corner in pixels. + * @defaultValue \{ x: 20, y: 20 \} + */ offset?: { x: number; y: number }; - /** Optional className for the outer div */ + + /** + * Optional className for the outer container div. + */ className?: string; - /** Respect external overlay toggle */ + + /** + * Controls visibility of the overlay. + * Set to false to hide the compass. + * @defaultValue true + */ overlay?: boolean; } /** - * Screen-space compass overlay that syncs to the MapLibre camera bearing/pitch. - * Renders its own R3F canvas layered above the map. + * A screen-space compass overlay that renders in its own React Three Fiber canvas. + * + * This component creates a separate R3F canvas that floats above the map and displays + * a 3D compass synchronized with the MapLibre camera's bearing and pitch. + * + * Use this when you want the compass in a separate rendering context from your main + * 3D scene, or when you need precise control over the overlay's position and size. + * + * @example Basic usage inside a Map + * ```tsx + * import Map from 'react-map-gl/maplibre'; + * import { CompassOverlay } from 'react-three-map/maplibre'; + * + * function App() { + * return ( + * + * + * + * ); + * } + * ``` + * + * @example Custom positioning + * ```tsx + * + * ``` + * + * @see {@link Compass3D} for the in-canvas compass component + * @see {@link CompassOverlayProps} for available configuration options */ export function CompassOverlay({ size = 200, diff --git a/src/mapbox.index.ts b/src/mapbox.index.ts index 8fd9f16..e8852b6 100644 --- a/src/mapbox.index.ts +++ b/src/mapbox.index.ts @@ -40,6 +40,8 @@ export { EnhancedPivotControls } from './components/enhanced-pivot-controls'; export type { PivotControlsProps } from './components/enhanced-pivot-controls'; export { Compass3D } from './components/compass-3d'; export type { Compass3DProps } from './components/compass-3d'; +export { CompassOverlay } from './components/compass-overlay'; +export type { CompassOverlayProps } from './components/compass-overlay'; /** * Hook to access the Mapbox GL JS map instance from within a Canvas. diff --git a/src/maplibre.index.ts b/src/maplibre.index.ts index 9975554..84c228a 100644 --- a/src/maplibre.index.ts +++ b/src/maplibre.index.ts @@ -39,6 +39,8 @@ export { EnhancedPivotControls } from './components/enhanced-pivot-controls'; export type { PivotControlsProps } from './components/enhanced-pivot-controls'; export { Compass3D } from './components/compass-3d'; export type { Compass3DProps } from './components/compass-3d'; +export { CompassOverlay } from './components/compass-overlay'; +export type { CompassOverlayProps } from './components/compass-overlay'; /** * Hook to access the MapLibre GL JS map instance from within a Canvas. diff --git a/stories/src/compass-3d.stories.tsx b/stories/src/compass-3d.stories.tsx index 058183f..784597a 100644 --- a/stories/src/compass-3d.stories.tsx +++ b/stories/src/compass-3d.stories.tsx @@ -1,114 +1,8 @@ -// Removed unused import import { Canvas } from "@react-three/fiber"; import { useControls } from "leva"; -import { Fragment, useEffect, useState } from "react"; -import { useMap } from "react-map-gl/maplibre"; -import { Compass3D } from "react-three-map"; -import { CompassOverlay } from "../../src/components/compass-overlay"; +import { Compass3D, CompassOverlay } from "react-three-map"; import { StoryMap } from "./story-map-storybook"; -// Component to sync compass rotation with map camera -function MapCompass({ overlay }: { overlay?: boolean }) { - const { current: map } = useMap(); - const [bearing, setBearing] = useState(0); - const [pitch, setPitch] = useState(0); - - const { - cylinderLength, - cylinderRadius, - sphereRadius, - compassScale - } = useControls({ - cylinderLength: { value: 2, min: 1, max: 5, step: 0.1 }, - cylinderRadius: { value: 0.05, min: 0.01, max: 0.2, step: 0.01 }, - sphereRadius: { value: 0.2, min: 0.1, max: 0.5, step: 0.05 }, - compassScale: { value: 0.5, min: 0.1, max: 2, step: 0.1 } - }); - - useEffect(() => { - if (!map) return; - - const updateRotation = () => { - setBearing(map.getBearing()); - setPitch(map.getPitch()); - }; - - // Initial update - updateRotation(); - - // Listen for map movements - map.on('move', updateRotation); - map.on('rotate', updateRotation); - map.on('pitch', updateRotation); - - return () => { - map.off('move', updateRotation); - map.off('rotate', updateRotation); - map.off('pitch', updateRotation); - }; - }, [map]); - - return ( -
- - - - - - - - {/* Add axes helper for debugging */} - - - - {/* Display bearing and pitch outside of Canvas */} -
-
Bearing: {Math.round(bearing)}°
-
Pitch: {Math.round(pitch)}°
-
-
- ); -} - // Main story component export function TerrainWith3DCompass() { const origin = useControls({ diff --git a/stories/src/story-map-storybook.tsx b/stories/src/story-map-storybook.tsx index a3e96ea..7fbe817 100644 --- a/stories/src/story-map-storybook.tsx +++ b/stories/src/story-map-storybook.tsx @@ -4,7 +4,7 @@ import { useControls } from 'leva'; import MapboxGl from 'mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css'; -import { FC, PropsWithChildren, ReactNode, cloneElement, isValidElement } from "react"; +import { FC, PropsWithChildren, ReactElement, ReactNode, cloneElement, isValidElement } from "react"; import MapboxMap from 'react-map-gl/mapbox'; import MaplibreMap from 'react-map-gl/maplibre'; import { Canvas as MapboxCanvas, CanvasProps } from 'react-three-map/mapbox'; @@ -65,16 +65,16 @@ export const StoryMap: FC = (props) => { const canvasProps = { overlay, ...canvas }; const mapChildrenWithOverlay = isValidElement(mapChildren) - ? cloneElement(mapChildren, { overlay }) + ? cloneElement(mapChildren as ReactElement<{ overlay?: boolean }>, { overlay }) : mapChildren; const mapboxChildrenWithOverlay = isValidElement(mapboxChildren) - ? cloneElement(mapboxChildren, { overlay }) + ? cloneElement(mapboxChildren as ReactElement<{ overlay?: boolean }>, { overlay }) : mapboxChildren; const maplibreChildrenWithOverlay = isValidElement(maplibreChildren) - ? cloneElement(maplibreChildren, { overlay }) + ? cloneElement(maplibreChildren as ReactElement<{ overlay?: boolean }>, { overlay }) : maplibreChildren; const childrenWithOverlay = isValidElement(children) - ? cloneElement(children, { overlay }) + ? cloneElement(children as ReactElement<{ overlay?: boolean }>, { overlay }) : children; // Set Mapbox token diff --git a/typedoc.json b/typedoc.json index 2a59000..01e1f22 100644 --- a/typedoc.json +++ b/typedoc.json @@ -22,7 +22,8 @@ "./docs-src/getting-started.md", "./docs-src/multiple-locations.md", "./docs-src/interacting-with-map.md", - "./docs-src/pivot-controls.md" + "./docs-src/pivot-controls.md", + "./docs-src/compass.md" ], "navigationLinks": { "Storybook": "https://wendylabsinc.github.io/react-three-map/storybook/",