web-a2e 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- package/wiki/Video-Rendering.md +658 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* text-selection.js - Screen text selection and copy support
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TextSelection - Enable text selection and copying from the Apple II screen
|
|
10
|
+
*
|
|
11
|
+
* Handles mouse selection on the canvas and converts screen memory to text.
|
|
12
|
+
* Character encoding and screen memory reading are handled by C++ core.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class TextSelection {
|
|
16
|
+
constructor(canvas, wasmModule, renderer) {
|
|
17
|
+
this.canvas = canvas;
|
|
18
|
+
this.wasmModule = wasmModule;
|
|
19
|
+
this.renderer = renderer || null;
|
|
20
|
+
|
|
21
|
+
// Selection state
|
|
22
|
+
this.isSelecting = false;
|
|
23
|
+
this.selectionStart = null; // {row, col}
|
|
24
|
+
this.selectionEnd = null; // {row, col}
|
|
25
|
+
|
|
26
|
+
// Overlay canvas for selection highlight
|
|
27
|
+
this.overlay = null;
|
|
28
|
+
this.overlayCtx = null;
|
|
29
|
+
|
|
30
|
+
// Screen dimensions
|
|
31
|
+
this.charWidth40 = 14; // 40-col: 14 pixels per char (7 * 2)
|
|
32
|
+
this.charWidth80 = 7; // 80-col: 7 pixels per char
|
|
33
|
+
this.charHeight = 16; // 16 pixels per char (8 * 2)
|
|
34
|
+
this.rows = 24;
|
|
35
|
+
|
|
36
|
+
// Bind event handlers for proper cleanup
|
|
37
|
+
this.boundOnMouseDown = this.onMouseDown.bind(this);
|
|
38
|
+
this.boundOnMouseMove = this.onMouseMove.bind(this);
|
|
39
|
+
this.boundOnMouseUp = this.onMouseUp.bind(this);
|
|
40
|
+
this.boundOnKeyDown = this.onKeyDown.bind(this);
|
|
41
|
+
this.boundOnContextMenu = this.onContextMenu.bind(this);
|
|
42
|
+
|
|
43
|
+
this.setupOverlay();
|
|
44
|
+
this.setupEventListeners();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setupOverlay() {
|
|
48
|
+
this.overlay = document.createElement('canvas');
|
|
49
|
+
this.overlay.width = 560;
|
|
50
|
+
this.overlay.height = 384;
|
|
51
|
+
this.overlayCtx = this.overlay.getContext('2d');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setupEventListeners() {
|
|
55
|
+
// mousedown on canvas starts selection
|
|
56
|
+
this.canvas.addEventListener('mousedown', this.boundOnMouseDown);
|
|
57
|
+
// keydown in capture phase so we can intercept before input handler
|
|
58
|
+
document.addEventListener('keydown', this.boundOnKeyDown, true);
|
|
59
|
+
this.canvas.addEventListener('contextmenu', this.boundOnContextMenu);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get soft switch state once and extract all mode flags
|
|
64
|
+
* Soft switch bits from emulator (see emulator.cpp getSoftSwitchState):
|
|
65
|
+
* Bit 0: TEXT mode
|
|
66
|
+
* Bit 1: MIXED mode
|
|
67
|
+
* Bit 2: PAGE2
|
|
68
|
+
* Bit 3: HIRES
|
|
69
|
+
* Bit 4: 80COL
|
|
70
|
+
* Bit 5: ALTCHARSET
|
|
71
|
+
* @returns {{textMode: boolean, col80: boolean, page2: boolean}}
|
|
72
|
+
*/
|
|
73
|
+
getDisplayMode() {
|
|
74
|
+
if (!this.wasmModule._getSoftSwitchState) {
|
|
75
|
+
return { textMode: false, col80: false, page2: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const state = this.wasmModule._getSoftSwitchState();
|
|
79
|
+
return {
|
|
80
|
+
textMode: (state & 0x01) !== 0, // Bit 0: TEXT mode
|
|
81
|
+
col80: (state & 0x10) !== 0, // Bit 4: 80COL mode
|
|
82
|
+
page2: (state & 0x04) !== 0 // Bit 2: PAGE2 (NOT bit 3 which is HIRES!)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if the emulator is in text mode
|
|
88
|
+
*/
|
|
89
|
+
isTextMode() {
|
|
90
|
+
return this.getDisplayMode().textMode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if 80-column mode is active
|
|
95
|
+
*/
|
|
96
|
+
is80ColumnMode() {
|
|
97
|
+
return this.getDisplayMode().col80;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert canvas pixel coordinates to character position.
|
|
102
|
+
* Mirrors the shader transform pipeline (curveUV → applyOverscan → applyScreenMargin)
|
|
103
|
+
* so the mouse mapping matches the curved on-screen content.
|
|
104
|
+
*/
|
|
105
|
+
pixelToChar(x, y) {
|
|
106
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
107
|
+
const params = this.renderer && this.renderer.crtParams || {};
|
|
108
|
+
|
|
109
|
+
// Convert mouse position to normalised UV (0–1)
|
|
110
|
+
let u = (x - rect.left) / rect.width;
|
|
111
|
+
let v = (y - rect.top) / rect.height;
|
|
112
|
+
|
|
113
|
+
// Apply CRT curvature (matches curveUV in crt.glsl)
|
|
114
|
+
const curvature = params.curvature || 0;
|
|
115
|
+
if (curvature > 0.001) {
|
|
116
|
+
const cx = u - 0.5;
|
|
117
|
+
const cy = v - 0.5;
|
|
118
|
+
const dist = cx * cx + cy * cy;
|
|
119
|
+
const distortion = dist * curvature * 0.5;
|
|
120
|
+
u += cx * distortion;
|
|
121
|
+
v += cy * distortion;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Apply overscan (matches applyOverscan in crt.glsl)
|
|
125
|
+
const overscan = params.overscan || 0;
|
|
126
|
+
if (overscan > 0.001) {
|
|
127
|
+
const borderSize = overscan * 0.1;
|
|
128
|
+
const scale = 1.0 - borderSize * 2.0;
|
|
129
|
+
u = (u - 0.5) / scale + 0.5;
|
|
130
|
+
v = (v - 0.5) / scale + 0.5;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Apply screen margin (matches applyScreenMargin in crt.glsl)
|
|
134
|
+
const margin = params.screenMargin || 0;
|
|
135
|
+
if (margin > 0.001) {
|
|
136
|
+
const scale = 1.0 / (1.0 - margin * 2.0);
|
|
137
|
+
u = (u - 0.5) * scale + 0.5;
|
|
138
|
+
v = (v - 0.5) * scale + 0.5;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// u,v are now contentUV — map to 560×384 texture space
|
|
142
|
+
const contentX = u * 560;
|
|
143
|
+
const contentY = v * 384;
|
|
144
|
+
|
|
145
|
+
const mode = this.getDisplayMode();
|
|
146
|
+
const cols = mode.col80 ? 80 : 40;
|
|
147
|
+
const charWidth = mode.col80 ? this.charWidth80 : this.charWidth40;
|
|
148
|
+
|
|
149
|
+
const col = Math.floor(contentX / charWidth);
|
|
150
|
+
const row = Math.floor(contentY / this.charHeight);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
row: Math.max(0, Math.min(this.rows - 1, row)),
|
|
154
|
+
col: Math.max(0, Math.min(cols - 1, col))
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Normalize selection so start is always before end
|
|
160
|
+
* @returns {{startRow, startCol, endRow, endCol}} Normalized coordinates
|
|
161
|
+
*/
|
|
162
|
+
normalizeSelection() {
|
|
163
|
+
if (!this.selectionStart || !this.selectionEnd) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let startRow = this.selectionStart.row;
|
|
168
|
+
let startCol = this.selectionStart.col;
|
|
169
|
+
let endRow = this.selectionEnd.row;
|
|
170
|
+
let endCol = this.selectionEnd.col;
|
|
171
|
+
|
|
172
|
+
if (startRow > endRow || (startRow === endRow && startCol > endCol)) {
|
|
173
|
+
[startRow, endRow] = [endRow, startRow];
|
|
174
|
+
[startCol, endCol] = [endCol, startCol];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { startRow, startCol, endRow, endCol };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get selected text as a string (delegates to C++ for screen memory reading and decoding)
|
|
182
|
+
*/
|
|
183
|
+
getSelectedText() {
|
|
184
|
+
const sel = this.normalizeSelection();
|
|
185
|
+
if (!sel) return '';
|
|
186
|
+
|
|
187
|
+
const resultPtr = this.wasmModule._readScreenText(
|
|
188
|
+
sel.startRow, sel.startCol, sel.endRow, sel.endCol
|
|
189
|
+
);
|
|
190
|
+
return resultPtr ? this.wasmModule.UTF8ToString(resultPtr) : '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Copy selected text to clipboard
|
|
195
|
+
*/
|
|
196
|
+
async copyToClipboard() {
|
|
197
|
+
const text = this.getSelectedText();
|
|
198
|
+
if (!text) return false;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await navigator.clipboard.writeText(text);
|
|
202
|
+
this.showCopyFeedback();
|
|
203
|
+
return true;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error('Failed to copy text:', err);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Show visual feedback when text is copied
|
|
212
|
+
*/
|
|
213
|
+
showCopyFeedback() {
|
|
214
|
+
const ctx = this.overlayCtx;
|
|
215
|
+
const copyFlashColor = getComputedStyle(document.documentElement)
|
|
216
|
+
.getPropertyValue('--selection-copy-flash').trim() || 'rgba(63, 185, 80, 0.5)';
|
|
217
|
+
ctx.fillStyle = copyFlashColor;
|
|
218
|
+
this.drawSelectionHighlight();
|
|
219
|
+
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
this.drawSelectionHighlight();
|
|
222
|
+
}, 150);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Draw the selection highlight on the overlay
|
|
227
|
+
*/
|
|
228
|
+
drawSelectionHighlight() {
|
|
229
|
+
const ctx = this.overlayCtx;
|
|
230
|
+
ctx.clearRect(0, 0, 560, 384);
|
|
231
|
+
|
|
232
|
+
const mode = this.getDisplayMode();
|
|
233
|
+
if (!mode.textMode) { this.uploadOverlay(); return; }
|
|
234
|
+
|
|
235
|
+
const sel = this.normalizeSelection();
|
|
236
|
+
if (!sel) { this.uploadOverlay(); return; }
|
|
237
|
+
|
|
238
|
+
const cols = mode.col80 ? 80 : 40;
|
|
239
|
+
const charWidth = mode.col80 ? this.charWidth80 : this.charWidth40;
|
|
240
|
+
|
|
241
|
+
const highlightColor = getComputedStyle(document.documentElement)
|
|
242
|
+
.getPropertyValue('--selection-highlight').trim() || 'rgba(88, 166, 255, 0.35)';
|
|
243
|
+
ctx.fillStyle = highlightColor;
|
|
244
|
+
|
|
245
|
+
for (let row = sel.startRow; row <= sel.endRow; row++) {
|
|
246
|
+
const colStart = (row === sel.startRow) ? sel.startCol : 0;
|
|
247
|
+
const colEnd = (row === sel.endRow) ? sel.endCol : cols - 1;
|
|
248
|
+
|
|
249
|
+
const x = colStart * charWidth;
|
|
250
|
+
const y = row * this.charHeight;
|
|
251
|
+
const width = (colEnd - colStart + 1) * charWidth;
|
|
252
|
+
const height = this.charHeight;
|
|
253
|
+
|
|
254
|
+
ctx.fillRect(x, y, width, height);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.uploadOverlay();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
uploadOverlay() {
|
|
261
|
+
if (this.renderer && this.renderer.updateSelectionTexture) {
|
|
262
|
+
this.renderer.updateSelectionTexture(this.overlay);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Move overlay to the canvas's current parent and re-measure.
|
|
268
|
+
* Called when the canvas is reparented (e.g. for full-page mode).
|
|
269
|
+
*/
|
|
270
|
+
reattach() {
|
|
271
|
+
// Overlay is offscreen; nothing to reparent
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clear the selection
|
|
276
|
+
*/
|
|
277
|
+
clearSelection() {
|
|
278
|
+
this.selectionStart = null;
|
|
279
|
+
this.selectionEnd = null;
|
|
280
|
+
this.isSelecting = false;
|
|
281
|
+
|
|
282
|
+
if (this.overlayCtx) {
|
|
283
|
+
this.overlayCtx.clearRect(0, 0, 560, 384);
|
|
284
|
+
this.uploadOverlay();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Event handlers
|
|
289
|
+
|
|
290
|
+
onMouseDown(e) {
|
|
291
|
+
if (!this.isTextMode()) return;
|
|
292
|
+
if (e.button !== 0) return;
|
|
293
|
+
|
|
294
|
+
const pos = this.pixelToChar(e.clientX, e.clientY);
|
|
295
|
+
this.selectionStart = pos;
|
|
296
|
+
this.selectionEnd = pos;
|
|
297
|
+
this.isSelecting = true;
|
|
298
|
+
|
|
299
|
+
// Add document-level listeners to track selection even outside canvas
|
|
300
|
+
document.addEventListener('mousemove', this.boundOnMouseMove);
|
|
301
|
+
document.addEventListener('mouseup', this.boundOnMouseUp);
|
|
302
|
+
|
|
303
|
+
this.drawSelectionHighlight();
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
onMouseMove(e) {
|
|
308
|
+
if (!this.isSelecting) return;
|
|
309
|
+
if (!this.isTextMode()) {
|
|
310
|
+
this.clearSelection();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const pos = this.pixelToChar(e.clientX, e.clientY);
|
|
315
|
+
this.selectionEnd = pos;
|
|
316
|
+
|
|
317
|
+
this.drawSelectionHighlight();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
onMouseUp(e) {
|
|
321
|
+
if (!this.isSelecting) return;
|
|
322
|
+
|
|
323
|
+
this.isSelecting = false;
|
|
324
|
+
|
|
325
|
+
// Remove document-level listeners
|
|
326
|
+
document.removeEventListener('mousemove', this.boundOnMouseMove);
|
|
327
|
+
document.removeEventListener('mouseup', this.boundOnMouseUp);
|
|
328
|
+
|
|
329
|
+
// Single click (no drag) - clear selection
|
|
330
|
+
if (this.selectionStart && this.selectionEnd &&
|
|
331
|
+
this.selectionStart.row === this.selectionEnd.row &&
|
|
332
|
+
this.selectionStart.col === this.selectionEnd.col) {
|
|
333
|
+
this.clearSelection();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
onKeyDown(e) {
|
|
338
|
+
// Ctrl+C / Cmd+C to copy selection
|
|
339
|
+
// Use e.code for reliable detection across platforms
|
|
340
|
+
const isCopyKey = e.code === 'KeyC' || e.key === 'c' || e.key === 'C';
|
|
341
|
+
if ((e.ctrlKey || e.metaKey) && isCopyKey) {
|
|
342
|
+
if (this.selectionStart && this.selectionEnd) {
|
|
343
|
+
this.copyToClipboard();
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
e.stopPropagation(); // Prevent key from reaching emulator
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Escape to clear selection
|
|
351
|
+
if (e.key === 'Escape' && this.selectionStart) {
|
|
352
|
+
this.clearSelection();
|
|
353
|
+
e.stopPropagation(); // Prevent key from reaching emulator
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
onContextMenu(e) {
|
|
358
|
+
if (!this.isTextMode()) return;
|
|
359
|
+
|
|
360
|
+
if (this.selectionStart && this.selectionEnd) {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
this.showContextMenu(e.clientX, e.clientY);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Show a simple context menu with copy option
|
|
368
|
+
*/
|
|
369
|
+
showContextMenu(x, y) {
|
|
370
|
+
const existing = document.querySelector('.text-select-context-menu');
|
|
371
|
+
if (existing) existing.remove();
|
|
372
|
+
|
|
373
|
+
const menu = document.createElement('div');
|
|
374
|
+
menu.className = 'text-select-context-menu';
|
|
375
|
+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
376
|
+
const copyShortcut = isMac ? '⌘C' : 'Ctrl+C';
|
|
377
|
+
menu.innerHTML = `
|
|
378
|
+
<button class="context-menu-item" data-action="copy">
|
|
379
|
+
<span>Copy</span>
|
|
380
|
+
<span class="shortcut">${copyShortcut}</span>
|
|
381
|
+
</button>
|
|
382
|
+
<button class="context-menu-item" data-action="select-all">
|
|
383
|
+
<span>Select All</span>
|
|
384
|
+
</button>
|
|
385
|
+
`;
|
|
386
|
+
|
|
387
|
+
// Position menu, ensuring it stays within viewport
|
|
388
|
+
const menuWidth = 150;
|
|
389
|
+
const menuHeight = 70;
|
|
390
|
+
const left = Math.min(x, window.innerWidth - menuWidth - 10);
|
|
391
|
+
const top = Math.min(y, window.innerHeight - menuHeight - 10);
|
|
392
|
+
|
|
393
|
+
menu.style.left = left + 'px';
|
|
394
|
+
menu.style.top = top + 'px';
|
|
395
|
+
|
|
396
|
+
document.body.appendChild(menu);
|
|
397
|
+
|
|
398
|
+
const handleClick = (e) => {
|
|
399
|
+
const item = e.target.closest('.context-menu-item');
|
|
400
|
+
if (!item) return;
|
|
401
|
+
|
|
402
|
+
const action = item.dataset.action;
|
|
403
|
+
if (action === 'copy') {
|
|
404
|
+
this.copyToClipboard();
|
|
405
|
+
} else if (action === 'select-all') {
|
|
406
|
+
this.selectAll();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
menu.remove();
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
menu.addEventListener('click', handleClick);
|
|
413
|
+
|
|
414
|
+
const closeMenu = (e) => {
|
|
415
|
+
if (!menu.contains(e.target)) {
|
|
416
|
+
menu.remove();
|
|
417
|
+
document.removeEventListener('mousedown', closeMenu);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
setTimeout(() => {
|
|
422
|
+
document.addEventListener('mousedown', closeMenu);
|
|
423
|
+
}, 0);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Select all text on screen
|
|
428
|
+
*/
|
|
429
|
+
selectAll() {
|
|
430
|
+
if (!this.isTextMode()) return;
|
|
431
|
+
|
|
432
|
+
const cols = this.is80ColumnMode() ? 80 : 40;
|
|
433
|
+
|
|
434
|
+
this.selectionStart = { row: 0, col: 0 };
|
|
435
|
+
this.selectionEnd = { row: this.rows - 1, col: cols - 1 };
|
|
436
|
+
|
|
437
|
+
this.drawSelectionHighlight();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Update overlay size and position when canvas resizes
|
|
442
|
+
*/
|
|
443
|
+
resize() {
|
|
444
|
+
// Overlay is offscreen; no DOM positioning needed
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Clean up resources and remove event listeners
|
|
449
|
+
*/
|
|
450
|
+
destroy() {
|
|
451
|
+
// Remove event listeners
|
|
452
|
+
this.canvas.removeEventListener('mousedown', this.boundOnMouseDown);
|
|
453
|
+
document.removeEventListener('mousemove', this.boundOnMouseMove);
|
|
454
|
+
document.removeEventListener('mouseup', this.boundOnMouseUp);
|
|
455
|
+
document.removeEventListener('keydown', this.boundOnKeyDown, true); // capture phase
|
|
456
|
+
this.canvas.removeEventListener('contextmenu', this.boundOnContextMenu);
|
|
457
|
+
|
|
458
|
+
// Remove any open context menu
|
|
459
|
+
const menu = document.querySelector('.text-select-context-menu');
|
|
460
|
+
if (menu) menu.remove();
|
|
461
|
+
}
|
|
462
|
+
}
|