Skip to content

Commit fb56048

Browse files
authored
feat: improve MTEXT insertion point handling, add case toggle support, and update demos (#8)
1 parent 8174f7e commit fb56048

28 files changed

Lines changed: 2359 additions & 805 deletions

File tree

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
# AGENTS.md
22

33
## Project Overview
4+
45
This repo is a pnpm monorepo for a Three.js-rich MTEXT editor and related demos.
56
Core packages live under `packages/`:
7+
68
- `packages/text-box-cursor`: renderer-agnostic cursor/selection engine.
79
- `packages/mtext-input-box`: Three.js MTEXT editor component with IME bridge + toolbar.
810
- `packages/demo-canvas-cursor`: Canvas2D demo app.
911
- `packages/demo-three-cursor`: Three.js cursor demo app.
1012
- `packages/demo-mtext-input-box`: Three.js MTEXT editor demo app.
1113

1214
## Tooling
15+
1316
- Package manager: `pnpm` (repo expects `pnpm@9.14.2`).
1417
- Language: TypeScript (ESM).
1518
- Demos use Vite (`pnpm --filter <pkg> dev`).
1619

1720
## Install
21+
1822
```bash
1923
pnpm install
2024
```
2125

2226
## Common Commands (workspace root)
27+
2328
```bash
2429
pnpm build
2530
pnpm lint
@@ -30,20 +35,24 @@ pnpm dev:mtext
3035
```
3136

3237
## Package-Scoped Commands
38+
3339
Use `pnpm --filter <pkg>` to target a single package.
3440
Examples:
41+
3542
```bash
3643
pnpm --filter @mlightcad/text-box-cursor test
3744
pnpm --filter @mlightcad/mtext-input-box build
3845
pnpm --filter @mlightcad/demo-mtext-input-box dev
3946
```
4047

4148
## Tests
49+
4250
- `@mlightcad/text-box-cursor` uses Vitest with coverage.
4351
- `@mlightcad/mtext-input-box` uses Vitest.
4452
- Demo packages do not have tests; they are built with Vite.
4553

4654
## Notes For Changes
55+
4756
- Prefer editing TypeScript sources under `packages/*/src/`.
4857
- Keep changes consistent with existing ESM + TypeScript patterns.
4958
- If touching demos, verify with the relevant `pnpm --filter <demo> dev` or `build`.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
"dev:canvas": "pnpm --filter @mlightcad/demo-canvas-cursor dev",
88
"dev:mtext": "pnpm --filter @mlightcad/demo-mtext-input-box dev",
99
"dev:three": "pnpm --filter @mlightcad/demo-three-cursor dev",
10+
"format": "prettier --write .",
1011
"lint": "pnpm -r --if-present run lint",
12+
"lint:fix": "pnpm -r --if-present run lint:fix",
1113
"publish:mtext": "pnpm run build && pnpm --filter @mlightcad/text-box-cursor publish --access public --no-git-checks && pnpm --filter @mlightcad/mtext-input-box publish --access public --no-git-checks",
1214
"release": "node tools/release.mjs",
1315
"test": "pnpm -r --if-present run test"
1416
},
1517
"devDependencies": {
16-
"@changesets/cli": "^2.30.0"
18+
"@changesets/cli": "^2.30.0",
19+
"prettier": "^3.8.3"
1720
}
1821
}

packages/demo-canvas-cursor/src/main.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,32 @@ function requireNode<T>(node: T | null, selector: string): T {
99

1010
const canvas = requireNode(document.querySelector<HTMLCanvasElement>('#stage'), '#stage');
1111
const status = requireNode(document.querySelector<HTMLElement>('#status'), '#status');
12-
const sceneSelect = requireNode(document.querySelector<HTMLSelectElement>('#sceneSelect'), '#sceneSelect');
12+
const sceneSelect = requireNode(
13+
document.querySelector<HTMLSelectElement>('#sceneSelect'),
14+
'#sceneSelect'
15+
);
1316
const tolerance = requireNode(document.querySelector<HTMLInputElement>('#tolerance'), '#tolerance');
14-
const toleranceValue = requireNode(document.querySelector<HTMLElement>('#toleranceValue'), '#toleranceValue');
15-
const zoomInBtn = requireNode(document.querySelector<HTMLButtonElement>('#zoomInBtn'), '#zoomInBtn');
16-
const zoomOutBtn = requireNode(document.querySelector<HTMLButtonElement>('#zoomOutBtn'), '#zoomOutBtn');
17+
const toleranceValue = requireNode(
18+
document.querySelector<HTMLElement>('#toleranceValue'),
19+
'#toleranceValue'
20+
);
21+
const zoomInBtn = requireNode(
22+
document.querySelector<HTMLButtonElement>('#zoomInBtn'),
23+
'#zoomInBtn'
24+
);
25+
const zoomOutBtn = requireNode(
26+
document.querySelector<HTMLButtonElement>('#zoomOutBtn'),
27+
'#zoomOutBtn'
28+
);
1729
const zoomResetBtn = requireNode(
1830
document.querySelector<HTMLButtonElement>('#zoomResetBtn'),
1931
'#zoomResetBtn'
2032
);
2133
const zoomValue = requireNode(document.querySelector<HTMLElement>('#zoomValue'), '#zoomValue');
22-
const dragToggle = requireNode(document.querySelector<HTMLInputElement>('#dragToggle'), '#dragToggle');
34+
const dragToggle = requireNode(
35+
document.querySelector<HTMLInputElement>('#dragToggle'),
36+
'#dragToggle'
37+
);
2338
const leftBtn = requireNode(document.querySelector<HTMLButtonElement>('#leftBtn'), '#leftBtn');
2439
const rightBtn = requireNode(document.querySelector<HTMLButtonElement>('#rightBtn'), '#rightBtn');
2540
const upBtn = requireNode(document.querySelector<HTMLButtonElement>('#upBtn'), '#upBtn');

packages/demo-mtext-input-box/index.html

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>MText Input Box Demo</title>
7-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsoneditor@10.2.0/dist/jsoneditor.min.css" />
7+
<link
8+
rel="stylesheet"
9+
href="https://cdn.jsdelivr.net/npm/jsoneditor@10.2.0/dist/jsoneditor.min.css"
10+
/>
811
<link rel="stylesheet" href="/src/styles.css" />
912
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@10.2.0/dist/jsoneditor.min.js"></script>
1013
</head>
@@ -18,22 +21,64 @@
1821
<div class="row row-main-actions">
1922
<button id="drawBoxBtn" class="btn-primary">Draw MTEXT Box</button>
2023
<button id="toggleThemeBtn">Toggle Toolbar Theme</button>
21-
<span id="interactionHint" class="interaction-hint">Tip: Click "Draw MTEXT Box", then drag on canvas.</span>
24+
<span id="interactionHint" class="interaction-hint"
25+
>Tip: Click "Draw MTEXT Box", then drag on canvas.</span
26+
>
2227
</div>
2328

2429
<div class="row row-debug-controls">
25-
<label class="control-check"><input id="debugEnabled" type="checkbox" /> Debug Mode</label>
26-
<label class="control-check"><input id="debugShowBoxes" type="checkbox" checked /> Char Boxes</label>
30+
<label class="control-check"
31+
><input id="debugEnabled" type="checkbox" /> Debug Mode</label
32+
>
33+
<label class="control-check"
34+
><input id="debugShowBoxes" type="checkbox" checked /> Char Boxes</label
35+
>
2736
<label class="control-check"><input id="debugShowChars" type="checkbox" /> Chars</label>
28-
<label class="control-check"><input id="debugShowCharIndices" type="checkbox" /> Char Indices</label>
29-
<label class="control-check"><input id="debugShowLineIndices" type="checkbox" /> Line Indices</label>
37+
<label class="control-check"
38+
><input id="debugShowCharIndices" type="checkbox" /> Char Indices</label
39+
>
40+
<label class="control-check"
41+
><input id="debugShowLineIndices" type="checkbox" /> Line Indices</label
42+
>
3043
</div>
3144

3245
<div class="tabs" role="tablist" aria-label="Bottom Panel Tabs">
33-
<button id="tabInfo" class="tab-btn is-active" role="tab" aria-selected="true" aria-controls="tabPaneInfo">Current Info</button>
34-
<button id="tabAst" class="tab-btn" role="tab" aria-selected="false" aria-controls="tabPaneAst">AST Tree</button>
35-
<button id="tabCursorLayout" class="tab-btn" role="tab" aria-selected="false" aria-controls="tabPaneCursorLayout">Layout</button>
36-
<button id="tabRaw" class="tab-btn" role="tab" aria-selected="false" aria-controls="tabPaneRaw">Raw MTEXT</button>
46+
<button
47+
id="tabInfo"
48+
class="tab-btn is-active"
49+
role="tab"
50+
aria-selected="true"
51+
aria-controls="tabPaneInfo"
52+
>
53+
Current Info
54+
</button>
55+
<button
56+
id="tabAst"
57+
class="tab-btn"
58+
role="tab"
59+
aria-selected="false"
60+
aria-controls="tabPaneAst"
61+
>
62+
AST Tree
63+
</button>
64+
<button
65+
id="tabCursorLayout"
66+
class="tab-btn"
67+
role="tab"
68+
aria-selected="false"
69+
aria-controls="tabPaneCursorLayout"
70+
>
71+
Layout
72+
</button>
73+
<button
74+
id="tabRaw"
75+
class="tab-btn"
76+
role="tab"
77+
aria-selected="false"
78+
aria-controls="tabPaneRaw"
79+
>
80+
Raw MTEXT
81+
</button>
3782
</div>
3883

3984
<div id="tabPaneInfo" class="tab-pane is-active" role="tabpanel" aria-labelledby="tabInfo">
@@ -42,7 +87,12 @@
4287
<div id="tabPaneAst" class="tab-pane" role="tabpanel" aria-labelledby="tabAst">
4388
<div id="astEditor" class="ast-editor"></div>
4489
</div>
45-
<div id="tabPaneCursorLayout" class="tab-pane" role="tabpanel" aria-labelledby="tabCursorLayout">
90+
<div
91+
id="tabPaneCursorLayout"
92+
class="tab-pane"
93+
role="tabpanel"
94+
aria-labelledby="tabCursorLayout"
95+
>
4696
<div id="cursorLayoutEditor" class="ast-editor"></div>
4797
</div>
4898
<div id="tabPaneRaw" class="tab-pane" role="tabpanel" aria-labelledby="tabRaw">

packages/demo-mtext-input-box/src/main.ts

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,49 @@ function buildInlineDiffHtml(previousText: string, currentText: string): string
128128
const canvas = requireNode(document.querySelector<HTMLCanvasElement>('#stage'), '#stage');
129129
const canvasWrap = requireNode(document.querySelector<HTMLElement>('.canvas-wrap'), '.canvas-wrap');
130130
const status = requireNode(document.querySelector<HTMLElement>('#status'), '#status');
131-
const drawBoxBtn = requireNode(document.querySelector<HTMLButtonElement>('#drawBoxBtn'), '#drawBoxBtn');
132-
const toggleThemeBtn = requireNode(document.querySelector<HTMLButtonElement>('#toggleThemeBtn'), '#toggleThemeBtn');
133-
const interactionHint = requireNode(document.querySelector<HTMLElement>('#interactionHint'), '#interactionHint');
134-
const debugEnabled = requireNode(document.querySelector<HTMLInputElement>('#debugEnabled'), '#debugEnabled');
135-
const debugShowBoxes = requireNode(document.querySelector<HTMLInputElement>('#debugShowBoxes'), '#debugShowBoxes');
136-
const debugShowChars = requireNode(document.querySelector<HTMLInputElement>('#debugShowChars'), '#debugShowChars');
137-
const debugShowCharIndices = requireNode(document.querySelector<HTMLInputElement>('#debugShowCharIndices'), '#debugShowCharIndices');
138-
const debugShowLineIndices = requireNode(document.querySelector<HTMLInputElement>('#debugShowLineIndices'), '#debugShowLineIndices');
131+
const drawBoxBtn = requireNode(
132+
document.querySelector<HTMLButtonElement>('#drawBoxBtn'),
133+
'#drawBoxBtn'
134+
);
135+
const toggleThemeBtn = requireNode(
136+
document.querySelector<HTMLButtonElement>('#toggleThemeBtn'),
137+
'#toggleThemeBtn'
138+
);
139+
const interactionHint = requireNode(
140+
document.querySelector<HTMLElement>('#interactionHint'),
141+
'#interactionHint'
142+
);
143+
const debugEnabled = requireNode(
144+
document.querySelector<HTMLInputElement>('#debugEnabled'),
145+
'#debugEnabled'
146+
);
147+
const debugShowBoxes = requireNode(
148+
document.querySelector<HTMLInputElement>('#debugShowBoxes'),
149+
'#debugShowBoxes'
150+
);
151+
const debugShowChars = requireNode(
152+
document.querySelector<HTMLInputElement>('#debugShowChars'),
153+
'#debugShowChars'
154+
);
155+
const debugShowCharIndices = requireNode(
156+
document.querySelector<HTMLInputElement>('#debugShowCharIndices'),
157+
'#debugShowCharIndices'
158+
);
159+
const debugShowLineIndices = requireNode(
160+
document.querySelector<HTMLInputElement>('#debugShowLineIndices'),
161+
'#debugShowLineIndices'
162+
);
139163
const tabInfo = requireNode(document.querySelector<HTMLButtonElement>('#tabInfo'), '#tabInfo');
140164
const tabAst = requireNode(document.querySelector<HTMLButtonElement>('#tabAst'), '#tabAst');
141165
const tabCursorLayout = requireNode(
142166
document.querySelector<HTMLButtonElement>('#tabCursorLayout'),
143167
'#tabCursorLayout'
144168
);
145169
const tabRaw = requireNode(document.querySelector<HTMLButtonElement>('#tabRaw'), '#tabRaw');
146-
const tabPaneInfo = requireNode(document.querySelector<HTMLElement>('#tabPaneInfo'), '#tabPaneInfo');
170+
const tabPaneInfo = requireNode(
171+
document.querySelector<HTMLElement>('#tabPaneInfo'),
172+
'#tabPaneInfo'
173+
);
147174
const tabPaneAst = requireNode(document.querySelector<HTMLElement>('#tabPaneAst'), '#tabPaneAst');
148175
const tabPaneCursorLayout = requireNode(
149176
document.querySelector<HTMLElement>('#tabPaneCursorLayout'),
@@ -152,7 +179,10 @@ const tabPaneCursorLayout = requireNode(
152179
const tabPaneRaw = requireNode(document.querySelector<HTMLElement>('#tabPaneRaw'), '#tabPaneRaw');
153180
const rawMText = requireNode(document.querySelector<HTMLTextAreaElement>('#rawMText'), '#rawMText');
154181
const mtextDiff = requireNode(document.querySelector<HTMLElement>('#mtextDiff'), '#mtextDiff');
155-
const astEditorContainer = requireNode(document.querySelector<HTMLElement>('#astEditor'), '#astEditor');
182+
const astEditorContainer = requireNode(
183+
document.querySelector<HTMLElement>('#astEditor'),
184+
'#astEditor'
185+
);
156186
const cursorLayoutEditorContainer = requireNode(
157187
document.querySelector<HTMLElement>('#cursorLayoutEditor'),
158188
'#cursorLayoutEditor'
@@ -384,7 +414,10 @@ function updateRawDiffView(): void {
384414

385415
const currentMText = active.editor.getText();
386416
rawMText.value = currentMText;
387-
mtextDiff.innerHTML = buildInlineDiffHtml(previousMTextByEditor[activeEditorIndex] ?? '', currentMText);
417+
mtextDiff.innerHTML = buildInlineDiffHtml(
418+
previousMTextByEditor[activeEditorIndex] ?? '',
419+
currentMText
420+
);
388421
previousMTextByEditor[activeEditorIndex] = currentMText;
389422
}
390423

@@ -404,7 +437,13 @@ toggleThemeBtn.addEventListener('click', () => {
404437
active.editor.setToolbarTheme(active.editor.getToolbarTheme() === 'dark' ? 'light' : 'dark');
405438
});
406439

407-
for (const input of [debugEnabled, debugShowBoxes, debugShowChars, debugShowCharIndices, debugShowLineIndices]) {
440+
for (const input of [
441+
debugEnabled,
442+
debugShowBoxes,
443+
debugShowChars,
444+
debugShowCharIndices,
445+
debugShowLineIndices
446+
]) {
408447
input.addEventListener('change', applyDebugControls);
409448
}
410449

@@ -466,10 +505,7 @@ function cancelDrawBoxInteraction(): void {
466505
controls.enabled = true;
467506
}
468507

469-
function createDynamicEditorByWorldBox(
470-
worldStart: THREE.Vector3,
471-
worldEnd: THREE.Vector3
472-
): void {
508+
function createDynamicEditorByWorldBox(worldStart: THREE.Vector3, worldEnd: THREE.Vector3): void {
473509
const minX = Math.min(worldStart.x, worldEnd.x);
474510
const maxY = Math.max(worldStart.y, worldEnd.y);
475511
const width = Math.max(Math.abs(worldEnd.x - worldStart.x), 20);
@@ -515,7 +551,11 @@ canvas.addEventListener('mousedown', (event) => {
515551
drawRectElement.className = 'draw-rect';
516552
canvasWrap.appendChild(drawRectElement);
517553
}
518-
updateDrawRect(drawRectElement, toCanvasLocalPoint(drawStartClient.x, drawStartClient.y), toCanvasLocalPoint(drawCurrentClient.x, drawCurrentClient.y));
554+
updateDrawRect(
555+
drawRectElement,
556+
toCanvasLocalPoint(drawStartClient.x, drawStartClient.y),
557+
toCanvasLocalPoint(drawCurrentClient.x, drawCurrentClient.y)
558+
);
519559
});
520560

521561
canvas.addEventListener('mousemove', (event) => {

packages/demo-mtext-input-box/src/styles.css

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ body {
2222
color: var(--text);
2323
background:
2424
radial-gradient(circle at 10% 10%, #253d6f 0%, transparent 42%),
25-
radial-gradient(circle at 88% 20%, #1f355b 0%, transparent 38%),
26-
var(--bg);
25+
radial-gradient(circle at 88% 20%, #1f355b 0%, transparent 38%), var(--bg);
2726
}
2827

2928
.app-shell {
@@ -88,7 +87,9 @@ canvas {
8887
background: #101828;
8988
color: var(--text);
9089
font-size: 12px;
91-
transition: border-color 0.2s ease, background-color 0.2s ease;
90+
transition:
91+
border-color 0.2s ease,
92+
background-color 0.2s ease;
9293
}
9394

9495
.control-check:hover {
@@ -106,7 +107,9 @@ button {
106107
border: 1px solid var(--line);
107108
border-radius: 8px;
108109
cursor: pointer;
109-
transition: border-color 0.2s ease, background-color 0.2s ease;
110+
transition:
111+
border-color 0.2s ease,
112+
background-color 0.2s ease;
110113
}
111114

112115
button:hover {
@@ -227,7 +230,12 @@ button:hover {
227230
background: #0d1322;
228231
color: #d2daf8;
229232
padding: 8px;
230-
font: 12px/1.45 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
233+
font:
234+
12px/1.45 'JetBrains Mono',
235+
'SF Mono',
236+
Menlo,
237+
Consolas,
238+
monospace;
231239
white-space: pre-wrap;
232240
}
233241

@@ -241,7 +249,6 @@ button:hover {
241249
}
242250
}
243251

244-
245252
.diff-view {
246253
white-space: pre-wrap;
247254
word-break: break-word;

0 commit comments

Comments
 (0)