text-edit

The pure text-editing model behind the SVG input field — no DOM, no Babylon, no document. It's a plain state object plus functions that return a new one, so the whole edit surface is unit-testable and behaves identically in a DOM overlay, on a 3D texture, and inside a headset (where there is no native input element at all).

import { edit, insert, backspace, moveCaret } from 'tosijs-3d'

let s = edit('hello')          // { text: 'hello', caret: 5, anchor: 5 }
s = insert(s, '!')             // 'hello!'
s = moveCaret(s, -3)           // caret back three
s = backspace(s)               // 'hel!'  ← deleted the char before the caret

Code points, not UTF-16 units

Every operation moves and deletes by code point, via Array.from. A string in JS is UTF-16, so '😀'.length === 2 and a naive text.slice(0, caret - 1) backspace splits it into half a surrogate pair — a broken glyph you then can't delete. Accented characters from the keyboard's long-press popup (ö, œ) and any emoji both go through this path, so it has to be right at the model level rather than patched in the view.

(Full grapheme clustering — combining marks, ZWJ emoji families, flags — is a bigger job needing Intl.Segmenter; code points are the correct next rung and cover the keyboard's own output.)

Selection

caret is where you type; anchor is where a selection started. They're equal when there's no selection, so "replace the selection" and "insert at the caret" are the same code path — the collapsed case is not special.