Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions docs-src/compass.md
Original file line number Diff line number Diff line change
@@ -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 (
<Map
initialViewState={{ latitude: 51.5074, longitude: -0.1278, zoom: 15, pitch: 60 }}
mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
>
<Canvas latitude={51.5074} longitude={-0.1278}>
<Compass3D />
</Canvas>
</Map>
);
}
```

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
<Compass3D alignment="bottom-left" margin={[40, 40]} />

// Top-center with default margin
<Compass3D alignment="top-center" />

// 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
<Compass3D
cylinderLength={3} // Length of each axis (default: 2)
cylinderRadius={0.08} // Thickness of axes (default: 0.05)
sphereRadius={0.3} // Size of endpoint spheres (default: 0.2)
scale={1.5} // Overall scale multiplier (default: 1)
/>
```

## 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 (
<Compass3D
overlay={false}
syncWithCamera={false}
bearing={bearing}
pitch={pitch}
position={[500, 100, 500]}
scale={50}
/>
);
}
```

## 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 (
<Map
initialViewState={{ latitude: 51.5074, longitude: -0.1278, zoom: 15, pitch: 60 }}
mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
>
<CompassOverlay
size={200} // Size of the overlay in pixels
offset={{ x: 20, y: 20 }} // Position from bottom-left
/>
</Map>
);
}
```

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.
83 changes: 76 additions & 7 deletions src/components/compass-overlay.tsx
Original file line number Diff line number Diff line change
@@ -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
* <CompassOverlay />
* ```
*
* @example Custom size and position
* ```tsx
* <CompassOverlay
* size={150}
* offset={{ x: 30, y: 30 }}
* />
* ```
*/
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 (
* <Map
* initialViewState={{ latitude: 51.5, longitude: -0.1, zoom: 15, pitch: 60 }}
* mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
* >
* <CompassOverlay />
* </Map>
* );
* }
* ```
*
* @example Custom positioning
* ```tsx
* <CompassOverlay
* size={150}
* offset={{ x: 10, y: 10 }}
* className="my-compass"
* />
* ```
*
* @see {@link Compass3D} for the in-canvas compass component
* @see {@link CompassOverlayProps} for available configuration options
*/
export function CompassOverlay({
size = 200,
Expand Down
2 changes: 2 additions & 0 deletions src/mapbox.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/maplibre.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
108 changes: 1 addition & 107 deletions stories/src/compass-3d.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{
position: 'absolute',
bottom: '20px',
left: '20px',
width: '200px',
height: '200px',
background: 'rgba(0, 0, 0, 0.7)',
borderRadius: '10px',
padding: '10px',
pointerEvents: 'none',
zIndex: 1000
}}>
<Canvas
orthographic
camera={{
position: [3, 3, 3],
zoom: 50,
near: -1000,
far: 1000
}}
style={{ width: '100%', height: '100%' }}
>
<ambientLight intensity={0.8} />
<directionalLight position={[5, 5, 5]} intensity={1} />
<directionalLight position={[-5, -5, -5]} intensity={0.5} />

<Compass3D
cylinderLength={cylinderLength}
cylinderRadius={cylinderRadius}
sphereRadius={sphereRadius}
bearing={overlay ? undefined : bearing}
pitch={overlay ? undefined : pitch}
scale={compassScale}
overlay={overlay}
syncWithCamera={overlay}
/>

{/* Add axes helper for debugging */}
<axesHelper args={[3]} />
</Canvas>

{/* Display bearing and pitch outside of Canvas */}
<div style={{
position: 'absolute',
bottom: '10px',
left: '50%',
transform: 'translateX(-50%)',
color: 'white',
fontSize: '11px',
textAlign: 'center',
fontFamily: 'monospace',
textShadow: '0 0 3px rgba(0,0,0,0.8)'
}}>
<div>Bearing: {Math.round(bearing)}°</div>
<div>Pitch: {Math.round(pitch)}°</div>
</div>
</div>
);
}

// Main story component
export function TerrainWith3DCompass() {
const origin = useControls({
Expand Down
Loading