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.