@mlightcad/mtext-input-box
v0.2.2
Published
Three.js-based MTEXT editor component with built-in IME bridge, cursor/selection rendering, and floating toolbar.
Downloads
671
Readme
Three.js Rich Text Editor
Three.js-based MTEXT editor component with built-in IME bridge, cursor/selection rendering, and floating toolbar.

Purpose
- MTEXT-style editing in Three.js
- Keyboard/mouse/IME editing, selection, formatting, and undo/redo
- Built-in toolbar (configurable theme, font list, container, and offset)
- Integration with
@mlightcad/mtext-rendererand@mlightcad/text-box-cursor
Technical Details
- Medium article: Building a Three.js Rich Text Editor
Install
pnpm add @mlightcad/mtext-input-box three @mlightcad/mtext-parser @mlightcad/mtext-renderer@mlightcad/mtext-parser and @mlightcad/mtext-renderer are peer dependencies of @mlightcad/mtext-input-box, so your app should install them directly.
Basic Usage
import * as THREE from 'three';
import { MTextInputBox } from '@mlightcad/mtext-input-box';
import { MTextColor } from '@mlightcad/mtext-renderer';
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(0, 1000, 600, 0, -1000, 1000);
const canvas = document.querySelector<HTMLCanvasElement>('#stage')!;
const editor = new MTextInputBox({
scene,
camera,
width: 800,
position: new THREE.Vector3(80, 520, 0),
initialText: 'Hello\\PWorld',
imeTarget: canvas,
textStyle: {
name: 'MTextInputBoxStyle',
standardFlag: 0,
fixedTextHeight: 24,
widthFactor: 1,
obliqueAngle: 0,
textGenerationFlag: 0,
lastHeight: 24,
font: 'simkai',
bigFont: ''
},
colorSettings: {
byLayerColor: 0xffffff,
byBlockColor: 0xffffff,
color: new MTextColor(256)
},
toolbar: {
enabled: true,
theme: 'dark',
offsetY: 12
}
});
function animate() {
requestAnimationFrame(animate);
editor.update();
}
animate();Notes:
imeTargetis required for text input/IME and built-in mouse interactions.- For multi-editor scenes, each editor instance can share the same canvas
imeTarget.
Toolbar
The built-in toolbar is a DOM overlay mounted above the active editor and updated automatically when the camera or viewport changes.
toolbar options:
enabled?: boolean(defaulttrue)theme?: 'light' | 'dark'(defaultdark)fontFamilies?: string[](default['Arial', 'Helvetica', 'Verdana', 'Tahoma', 'Trebuchet MS', 'Times New Roman', 'Georgia', 'Courier New', 'system-ui', 'sans-serif', 'serif', 'monospace'])colorPicker?: (context) => { setValue?, setTheme?, dispose? }(optional custom picker factory)container?: HTMLElement(defaultdocument.body)offsetY?: number(default10)
Custom color picker factory context:
container: HTMLElement: mount point for your UI.theme: 'light' | 'dark': current toolbar theme.initialColor: MTextColor: initial color (ACI or RGB). It reflects the active editor format at toolbar creation time.onChange(color: MTextColor): call this when user picks a new color (ACI or RGB).
Custom color picker instance methods:
setValue(color: MTextColor): sync when editor format changes (color may be ACI or RGB).setTheme(theme): optional hook for theme changes.dispose(): cleanup any mounted resources.
Example (mount a Vue color picker):
import { createApp, h, ref } from 'vue';
import { getColorByIndex, MTextColor } from '@mlightcad/mtext-renderer';
toolbar: {
colorPicker: ({ container, initialColor, theme, onChange }) => {
const toCssColor = (value: MTextColor) => {
if (value.isAci && value.aci !== null) {
return `#${getColorByIndex(value.aci).toString(16).padStart(6, '0')}`;
}
return value.toCssColor() ?? '#ffffff';
};
const color = ref(toCssColor(initialColor));
const app = createApp({
render() {
return h(MyVueColorPicker, {
modelValue: color.value,
theme,
'onUpdate:modelValue': (next: string) => {
const nextColor = MTextColor.fromCssColor(next);
if (!nextColor) return;
color.value = next;
onChange(nextColor);
}
});
}
});
app.mount(container);
return {
setValue(next: MTextColor) {
color.value = toCssColor(next);
},
setTheme(nextTheme: 'light' | 'dark') {
// Optional: forward theme into your picker state if needed.
},
dispose() {
app.unmount();
}
};
}
}Public toolbar-related methods:
setToolbarTheme('light' | 'dark')getToolbarTheme()
Editor Visibility And Activation
- Click inside editor bounds: activate editor and start editing.
- Click outside active editor bounds: editor closes (
closeEditor()behavior). - Double-click rendered text of a closed editor: re-open (
showEditor()behavior). - Programmatic control:
closeEditor(): voidshowEditor(): boolean
Important Options
textStyle: default text style passed to the renderer and used to derive base insert format.imeTarget: element used by built-in IME bridge and built-in pointer interactions.enableWordWrap?: boolean: enable/disable editor-side wrapping behavior.showBoundingBox?: boolean: show editor boundary rectangle.boundingBoxStyle?: { color?, opacity?, padding?, zOffset? }: boundary style overrides.workerUrl?: string | URL: worker URL for renderer initialization.fontUrl?: string: optional base URL for font loading (overrides renderer default).cursorStyle/selectionStyle: forwarded to cursor renderer.toolbar: built-in toolbar options (enabled,theme,fontFamilies,colorPicker,container,offsetY).
Input And Interaction
Built-in handling:
- Keyboard editing (including macOS/Windows shortcut variants)
- IME composition (via hidden textarea bridge)
- Mouse selection and word-select:
mousedown: place cursor / shift-selectmousemoveduring drag: update selectionmouseup: stop dragdblclick: select word
Advanced control:
attachIme(target)/detachIme()attachPointerInteractions(target)/detachPointerInteractions()
Events
changeselectionChangecursorMoveshowclose
Undo / Redo
- API:
undo(): booleanredo(): boolean
- Keyboard:
Ctrl/Cmd + Z: undoCtrl/Cmd + Y: redoCtrl/Cmd + Shift + Z: redo
Lifecycle
- Call
editor.update()inside your render loop. - Call
editor.dispose()when unmounting to release renderer, cursor, IME, toolbar, and pointer listeners.
Development
pnpm --filter @mlightcad/mtext-input-box lint
pnpm --filter @mlightcad/mtext-input-box build