npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mlightcad/text-box-cursor

v0.1.1

Published

Renderer-agnostic text cursor and selection toolkit.

Readme

@mlightcad/text-box-cursor

Renderer-agnostic text cursor and selection toolkit.

Purpose

  • Provide stable cursor movement/selection logic based on character boxes.
  • Render cursor/selection overlays through pluggable adapters.
  • Support both Canvas2D and Three.js environments with one API.

Main Exports

  • TextBoxCursor: pure cursor/selection logic.
  • CursorRenderer: renderer-facing overlay controller.
  • Canvas2DRendererAdapter: canvas implementation.
  • ThreeJsRendererAdapter: Three.js implementation.

Install

pnpm add @mlightcad/text-box-cursor

If you use ThreeJsRendererAdapter, install three as well.

Quick Start

import { TextBoxCursor, type Box } from '@mlightcad/text-box-cursor';

const containerBox: Box = { x: 0, y: 0, width: 500, height: 200 };
const charBoxes: Box[] = [
  { x: 10, y: 20, width: 12, height: 20 },
  { x: 24, y: 20, width: 12, height: 20 }
];

const cursor = new TextBoxCursor({ containerBox, charBoxes });
cursor.moveRight();
cursor.setSelection(0, 2);

TextBoxCursorOptions

interface TextBoxCursorOptions {
  containerBox: Box;
  charBoxes: (Box | null | undefined)[];
  lineBreakIndices?: number[];
  lineLayouts?: { y: number; height: number }[];
  initialIndex?: number;
  initialSelection?: { start: number; end: number };
  verticalAlign?: 'top' | 'center' | 'bottom';
  lineTolerance?: number;
  debug?: boolean;
}
  • containerBox: outer editable region in screen-space pixels.
  • charBoxes: per-character bounds in logical text order (no need to provide \n box).
  • lineBreakIndices: optional explicit line breaks as half-open boundary indices.
  • lineLayouts: optional per-line y/height from your text layout engine (visual line order).
  • initialIndex: initial cursor index, clamped to [0, charCount].
  • initialSelection: initial selection range [start, end).
  • verticalAlign: cursor Y alignment against each line box.
  • lineTolerance: geometry line-grouping tolerance (used when explicit breaks are not provided).
  • debug: enables extra debug state snapshots.

lineBreakIndices meaning

lineBreakIndices uses boundary indices between characters:

  • Index 0 means before first character.
  • Index charCount means after last character.
  • Valid break values are internal boundaries: 1..charCount-1.
  • Break k means: split between character k - 1 and k.

Example with 6 characters (index: 0 1 2 3 4 5):

  • lineBreakIndices: [2, 4] creates lines:
    • line 1: chars 0..1
    • line 2: chars 2..3
    • line 3: chars 4..5

How to set lineBreakIndices

  1. Build charBoxes in text order (do not include \n as a normal character box).
  2. For each logical newline, push the current character count as a break index.
  3. Pass that array to TextBoxCursor.
import { TextBoxCursor, type Box } from '@mlightcad/text-box-cursor';

const text = 'AB\nCD\nEF';
const charBoxes: Box[] = [
  { x: 10, y: 20, width: 10, height: 20 }, // A
  { x: 22, y: 20, width: 10, height: 20 }, // B
  { x: 10, y: 50, width: 10, height: 20 }, // C
  { x: 22, y: 50, width: 10, height: 20 }, // D
  { x: 10, y: 80, width: 10, height: 20 }, // E
  { x: 22, y: 80, width: 10, height: 20 }  // F
];

// Breaks after B (index 2) and after D (index 4).
const lineBreakIndices = [2, 4];

const cursor = new TextBoxCursor({
  containerBox: { x: 0, y: 0, width: 300, height: 120 },
  charBoxes,
  lineBreakIndices
});

Empty line positioning (recommended)

If there are empty lines, provide lineLayouts from your text engine so cursor y is exact:

const cursor = new TextBoxCursor({
  containerBox: { x: 0, y: 0, width: 300, height: 160 },
  charBoxes: [
    { x: 10, y: 20, width: 10, height: 20 },
    { x: 22, y: 20, width: 10, height: 20 },
    { x: 10, y: 80, width: 10, height: 20 },
    { x: 22, y: 80, width: 10, height: 20 }
  ],
  lineBreakIndices: [2, 2],
  lineLayouts: [
    { y: 20, height: 20 }, // line 1
    { y: 50, height: 20 }, // empty line
    { y: 80, height: 20 }  // line 3
  ]
});

When lineLayouts is omitted, empty-line y falls back to internal estimation.

When lineBreakIndices is not specified

TextBoxCursor falls back to geometry-based line detection from charBoxes order.

It scans adjacent boxes and starts a new line when either condition is true:

  • x reset: current x is significantly smaller than previous x (controlled by lineTolerance).
  • Strong vertical separation: large y jump with low vertical overlap ratio between adjacent boxes.

Then cursor position is computed from the detected line model:

  • Cursor y: from that line's center/top/bottom (depends on verticalAlign).
  • Cursor x: from the nearest gap in the current line (line start + every char right edge).
  • Vertical movement (moveUp/moveDown): keeps preferred x and finds nearest gap on target line.

Practical notes:

  • Keep charBoxes in logical text order.
  • If your layout has mixed font sizes or noisy glyph boxes, increase lineTolerance.
  • If your source already knows exact line breaks, prefer lineBreakIndices for deterministic behavior.

Coordinate Model

TextBoxCursor expects top-left style coordinates:

  • x grows to the right.
  • y grows downward.

If your renderer uses another coordinate system, convert before passing boxes.

Development

pnpm --filter @mlightcad/text-box-cursor lint
pnpm --filter @mlightcad/text-box-cursor test
pnpm --filter @mlightcad/text-box-cursor build