# GlyphHotspot

`<GlyphHotspot>` renders a real, absolutely-positioned `<div>` over the ASCII render,
tracking a 3D anchor through the live camera. The `<div>` is `pointer-events: auto`
inside an otherwise transparent hit layer — events fire on it like any normal DOM
element.

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `id` | `string` | required | Stable identifier for this hotspot |
| `at` | `Vec3` | required | World-space anchor `[x, y, z]` |
| `size` | `[number, number]` | `[1, 1]` | Hitbox size in character cells |
| `onClick` | `MouseEventHandler` | — | Click handler |
| `aria-label` | `string` | — | Accessibility label |
| `className` | `string` | — | CSS class |
| `children` | `ReactNode` | — | Rendered inside the `<div>` (use for tooltips/badges) |

## Tracking math

On every render pass the anchor is projected through the current camera and the
resulting cell is written straight to the element's inline `transform` — one
assignment per hotspot, no DOM rebuild and no second projection.

While the user drags, the same projection runs per pointermove, so the hotspot
stays glued to its anchor through the whole gesture.

## Full examples

```tsx
import {
  GlyphCamera,
  GlyphScene,
  GlyphMesh,
  GlyphOrbitControls,
  GlyphHotspot,
} from "@glyphcss/react";
import { cubePolygons } from "@glyphcss/core";

const cube = cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" });

export function HotspotDemo() {
  return (
    <GlyphCamera rotX={25}>
      <GlyphScene mode="solid" cols={100} rows={30}>
        <GlyphOrbitControls />
        <GlyphMesh polygons={cube}>
          {/* Hotspot on the top face */}
          <GlyphHotspot
            id="top"
            at={[0, 0.5, 0]}
            size={[5, 3]}
            aria-label="Top face"
            onClick={() => alert("top face clicked")}
          >
            <span className="badge">Top</span>
          </GlyphHotspot>
          {/* Hotspot on the right face */}
          <GlyphHotspot
            id="right"
            at={[0.5, 0, 0]}
            size={[5, 3]}
            aria-label="Right face"
            onClick={() => alert("right face clicked")}
          >
            <span className="badge">Right</span>
          </GlyphHotspot>
        </GlyphMesh>
      </GlyphScene>
    </GlyphCamera>
  );
}
```

  

```vue
<template>
  <GlyphCamera :rot-x="25">
    <GlyphScene mode="solid" :cols="100" :rows="30">
      <GlyphOrbitControls />
      <GlyphMesh :polygons="cube">
        <GlyphHotspot
          id="top"
          :at="[0, 0.5, 0]"
          :size="[5, 3]"
          aria-label="Top face"
          @click="onTopClick"
        />
        <GlyphHotspot
          id="right"
          :at="[0.5, 0, 0]"
          :size="[5, 3]"
          aria-label="Right face"
          @click="onRightClick"
        />
      </GlyphMesh>
    </GlyphScene>
  </GlyphCamera>
</template>

<script setup lang="ts">
import {
  GlyphCamera,
  GlyphScene,
  GlyphMesh,
  GlyphOrbitControls,
  GlyphHotspot,
} from "@glyphcss/vue";
import { cubePolygons } from "@glyphcss/core";

const cube = cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" });
function onTopClick() { alert("top face clicked"); }
function onRightClick() { alert("right face clicked"); }
</script>
```

  

```ts
import { createGlyphScene } from "glyphcss";
import { cubePolygons } from "@glyphcss/core";

const host = document.querySelector<HTMLElement>("#scene")!;
const scene = createGlyphScene(host, { mode: "solid", cols: 100, rows: 30 });

scene.add(cubePolygons({ center: [0, 0, 0], size: 1, color: "#4488ff" }));

const topHotspot = scene.addHotspot(
  { id: "top", at: [0, 0.5, 0], size: [5, 3] },
  () => alert("top face clicked"),
);

const rightHotspot = scene.addHotspot(
  { id: "right", at: [0.5, 0, 0], size: [5, 3] },
  () => alert("right face clicked"),
);

// Remove a hotspot when no longer needed:
// topHotspot.remove();
```

## Visibility

A hotspot whose projected cell is not visible (behind the camera, or occluded)
is hidden with `display: "none"` rather than faded — the renderer stages the
style and applies it in the same single write as the rest of the frame. There is
no opacity keyframe to hook a CSS transition onto; animate the element yourself
if you want a fade.

## Anchors

`at` is a world-space `Vec3`. String anchors (`"vertex:42"`, `"face:roof"`,
`"centroid"`) are **not** implemented — resolve the coordinate yourself from the
polygon data and pass the result:

```ts
const centroid = polygons
  .flatMap((p) => p.vertices)
  .reduce((a, v, _i, all) => [a[0] + v[0] / all.length, a[1] + v[1] / all.length, a[2] + v[2] / all.length], [0, 0, 0]);
```
