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.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,342 @@
1
+ /*
2
+ * screen-window.js - Main emulator screen window
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { BaseWindow } from "../windows/base-window.js";
9
+
10
+ export class ScreenWindow extends BaseWindow {
11
+ constructor(renderer, textSelection) {
12
+ super({
13
+ id: "screen-window",
14
+ title: "Monitor",
15
+ minWidth: 284, // 280 min canvas + 4px padding/border
16
+ minHeight: 244, // 210 min canvas + ~34px header
17
+ defaultWidth: 480,
18
+ defaultHeight: 394,
19
+ closable: false,
20
+ focusCanvas: true,
21
+ });
22
+
23
+ this.renderer = renderer;
24
+ this.textSelection = textSelection;
25
+ this._viewportLocked = false;
26
+ }
27
+
28
+ renderContent() {
29
+ return '<div class="screen-window-content"></div>';
30
+ }
31
+
32
+ /**
33
+ * After create(), inject the charset toggle into the header and
34
+ * set up a ResizeObserver so the canvas tracks container size.
35
+ */
36
+ onContentRendered() {
37
+ const charsetSwitch = document.createElement("div");
38
+ charsetSwitch.className = "screen-window-charset-switch";
39
+ charsetSwitch.title = "Character Set (US/UK)";
40
+ charsetSwitch.innerHTML = `
41
+ <span class="charset-label">US</span>
42
+ <label class="charset-toggle">
43
+ <input type="checkbox" id="screen-window-charset-toggle" />
44
+ <span class="charset-slider"></span>
45
+ </label>
46
+ <span class="charset-label">UK</span>
47
+ `;
48
+
49
+ // Viewport lock button
50
+ this._lockBtn = document.createElement("button");
51
+ this._lockBtn.className = "screen-window-lock";
52
+ this._lockBtn.title = "Fit to viewport";
53
+ this._lockBtn.innerHTML = `
54
+ <svg class="lock-icon-unlocked" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
55
+ <path d="M4 7V5a4 4 0 0 1 8 0v1h-2V5a2 2 0 0 0-4 0v2H3a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4z"/>
56
+ </svg>
57
+ <svg class="lock-icon-locked" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
58
+ <path d="M4 7V5a4 4 0 0 1 8 0v2h1a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h1zm2-2v2h4V5a2 2 0 0 0-4 0z"/>
59
+ </svg>
60
+ `;
61
+
62
+ // Insert charset switch and lock button into header
63
+ this.headerElement.appendChild(charsetSwitch);
64
+ this.headerElement.appendChild(this._lockBtn);
65
+
66
+ // Prevent clicks from starting a window drag
67
+ charsetSwitch.addEventListener("mousedown", (e) => {
68
+ e.stopPropagation();
69
+ });
70
+ this._lockBtn.addEventListener("mousedown", (e) => {
71
+ e.stopPropagation();
72
+ });
73
+ this._lockBtn.addEventListener("click", () => {
74
+ this.setViewportLocked(!this._viewportLocked);
75
+ });
76
+
77
+ // Observe the content container so _fitCanvas runs whenever
78
+ // the window is resized, restored, arranged, etc.
79
+ const container = this.contentElement.querySelector(
80
+ ".screen-window-content",
81
+ );
82
+ if (container) {
83
+ this._resizeObserver = new ResizeObserver(() => this._fitCanvas());
84
+ this._resizeObserver.observe(container);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Set viewport-lock state and immediately resize if locking on.
90
+ */
91
+ setViewportLocked(locked) {
92
+ this._viewportLocked = locked;
93
+ if (this._lockBtn) {
94
+ this._lockBtn.classList.toggle("active", locked);
95
+ this._lockBtn.title = locked ? "Unlock from viewport" : "Fit to viewport";
96
+ }
97
+ if (locked) {
98
+ this.constrainToViewport();
99
+ }
100
+ if (this.onStateChange) this.onStateChange();
101
+ }
102
+
103
+ /**
104
+ * Move #screen canvas from #monitor-frame into this window's content area.
105
+ */
106
+ attachCanvas() {
107
+ const canvas = document.getElementById("screen");
108
+ if (!canvas) return;
109
+
110
+ const container = this.contentElement.querySelector(
111
+ ".screen-window-content",
112
+ );
113
+ if (!container) return;
114
+
115
+ // Clear any inline sizing
116
+ canvas.style.width = "";
117
+ canvas.style.height = "";
118
+ canvas.style.marginTop = "";
119
+
120
+ container.appendChild(canvas);
121
+
122
+ if (this.textSelection) {
123
+ this.textSelection.reattach();
124
+ }
125
+
126
+ this._fitCanvas();
127
+ }
128
+
129
+ /**
130
+ * Move #screen canvas back into #monitor-frame (used by full-page mode).
131
+ */
132
+ detachCanvas() {
133
+ const canvas = document.getElementById("screen");
134
+ if (!canvas) return;
135
+
136
+ const frame = document.getElementById("monitor-frame");
137
+ if (!frame) return;
138
+
139
+ frame.appendChild(canvas);
140
+
141
+ // Clear inline styles
142
+ canvas.style.width = "";
143
+ canvas.style.height = "";
144
+
145
+ if (this.textSelection) {
146
+ this.textSelection.reattach();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * After showing, fit the canvas to the content area.
152
+ */
153
+ show() {
154
+ super.show();
155
+ requestAnimationFrame(() => this._fitCanvas());
156
+ }
157
+
158
+ /**
159
+ * Get window state for persistence (adds viewportLocked).
160
+ */
161
+ getState() {
162
+ const base = super.getState();
163
+ base.viewportLocked = this._viewportLocked;
164
+ return base;
165
+ }
166
+
167
+ /**
168
+ * Restore persisted state.
169
+ */
170
+ restoreState(state) {
171
+ if (state.viewportLocked) {
172
+ this.setViewportLocked(true);
173
+ }
174
+ super.restoreState(state);
175
+ }
176
+
177
+ /**
178
+ * Constrain to viewport. When viewport-locked, fill the available
179
+ * area and centre. The canvas handles its own 4:3 aspect ratio
180
+ * via _fitCanvas.
181
+ */
182
+ constrainToViewport() {
183
+ if (!this.element) return;
184
+
185
+ if (this._viewportLocked) {
186
+ const vpW = window.innerWidth;
187
+ const vpH = window.innerHeight;
188
+ const header = document.querySelector("header");
189
+ const footer = document.querySelector("footer");
190
+ const minTop = header ? header.offsetHeight : 0;
191
+ const footerH = footer ? footer.offsetHeight : 0;
192
+ const margin = 8;
193
+
194
+ const w = vpW - margin * 2;
195
+ const h = vpH - minTop - footerH - margin * 2;
196
+ const x = margin;
197
+ const y = minTop + margin;
198
+
199
+ this.element.style.width = `${w}px`;
200
+ this.element.style.height = `${h}px`;
201
+ this.element.style.left = `${x}px`;
202
+ this.element.style.top = `${y}px`;
203
+ this.currentWidth = w;
204
+ this.currentHeight = h;
205
+ this.currentX = x;
206
+ this.currentY = y;
207
+
208
+ this.lastViewportWidth = vpW;
209
+ this.lastViewportHeight = vpH;
210
+ } else {
211
+ super.constrainToViewport();
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Override resize to enforce 4:3 aspect ratio on the content area.
217
+ * Whichever dimension the user drags drives the other so the window
218
+ * always holds an exact 4:3 content rectangle.
219
+ */
220
+ resize(e) {
221
+ const dir = this.resizeDirection;
222
+
223
+ // Let the base class compute unconstrained new dimensions
224
+ super.resize(e);
225
+
226
+ // Enforce 4:3 on the content area (window height = content + header)
227
+ const headerHeight = this.headerElement
228
+ ? this.headerElement.offsetHeight
229
+ : 0;
230
+ const ASPECT = 4 / 3; // width / height
231
+
232
+ // Anchor edges: resizing from n keeps bottom fixed, from w keeps right fixed
233
+ const bottom = this.currentY + this.currentHeight;
234
+ const right = this.currentX + this.currentWidth;
235
+
236
+ let newWidth = this.currentWidth;
237
+ let newHeight;
238
+
239
+ if (dir === "n" || dir === "s") {
240
+ // Pure vertical drag: width follows height
241
+ const contentHeight = this.currentHeight - headerHeight;
242
+ newWidth = Math.round(contentHeight * ASPECT);
243
+ newHeight = this.currentHeight;
244
+ } else {
245
+ // Has horizontal component (e, w, corners): height follows width
246
+ const targetContentHeight = Math.round(this.currentWidth / ASPECT);
247
+ newHeight = targetContentHeight + headerHeight;
248
+ }
249
+
250
+ // Enforce minimums while maintaining ratio
251
+ if (newWidth < this.minWidth) {
252
+ newWidth = this.minWidth;
253
+ newHeight = Math.round(newWidth / ASPECT) + headerHeight;
254
+ }
255
+ if (newHeight < this.minHeight) {
256
+ newHeight = this.minHeight;
257
+ newWidth = Math.round((newHeight - headerHeight) * ASPECT);
258
+ }
259
+
260
+ // Adjust position so the anchored edge stays fixed
261
+ let newLeft = this.currentX;
262
+ let newTop = this.currentY;
263
+
264
+ if (dir.includes("n")) {
265
+ newTop = bottom - newHeight;
266
+ }
267
+ if (dir.includes("w")) {
268
+ newLeft = right - newWidth;
269
+ }
270
+
271
+ // Apply the aspect-corrected dimensions and position
272
+ this.element.style.width = `${newWidth}px`;
273
+ this.element.style.height = `${newHeight}px`;
274
+ this.element.style.left = `${newLeft}px`;
275
+ this.element.style.top = `${newTop}px`;
276
+ this.currentWidth = newWidth;
277
+ this.currentHeight = newHeight;
278
+ this.currentX = newLeft;
279
+ this.currentY = newTop;
280
+
281
+ this._fitCanvas();
282
+ }
283
+
284
+ /**
285
+ * After resize completes, do a final canvas fit.
286
+ */
287
+ handleMouseUp(e) {
288
+ const wasResizing = this.isResizing;
289
+ super.handleMouseUp(e);
290
+ if (wasResizing) {
291
+ this._fitCanvas();
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Compute the largest 4:3 rectangle that fits within the content
297
+ * container and apply it to the canvas display size and renderer.
298
+ */
299
+ _fitCanvas() {
300
+ const container = this.contentElement?.querySelector(
301
+ ".screen-window-content",
302
+ );
303
+ const canvas = document.getElementById("screen");
304
+ if (!container || !canvas) return;
305
+
306
+ const cw = container.clientWidth;
307
+ const ch = container.clientHeight;
308
+ if (cw <= 0 || ch <= 0) return;
309
+
310
+ let w, h;
311
+ if ((cw * 3) / 4 <= ch) {
312
+ w = cw;
313
+ h = (cw * 3) / 4;
314
+ } else {
315
+ h = ch;
316
+ w = (ch * 4) / 3;
317
+ }
318
+ w = Math.floor(w);
319
+ h = Math.floor(h);
320
+
321
+ canvas.style.width = `${w}px`;
322
+ canvas.style.height = `${h}px`;
323
+
324
+ if (this.renderer) {
325
+ this.renderer.resize(w, h);
326
+ }
327
+ if (this.textSelection) {
328
+ this.textSelection.resize();
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Clean up ResizeObserver.
334
+ */
335
+ destroy() {
336
+ if (this._resizeObserver) {
337
+ this._resizeObserver.disconnect();
338
+ this._resizeObserver = null;
339
+ }
340
+ super.destroy();
341
+ }
342
+ }