Monorepo for Aesthetic.Computer
aesthetic.computer
1// Headless AC - Shared functionality for bake.mjs and tape.mjs
2// Provides common AC system initialization, API creation, and utilities
3
4import { writeFileSync, readFileSync, existsSync } from 'fs';
5import { resolve, dirname, join } from 'path';
6import fs from 'fs';
7import path from 'path';
8import crypto from 'crypto';
9import { pathToFileURL, fileURLToPath } from 'url';
10import { PNG } from 'pngjs';
11import { timestamp, resetRainbowCache, resetZebraCache, getRainbowState, setRainbowState, getZebraState, setZebraState } from "../../../system/public/aesthetic.computer/lib/num.mjs";
12import chalk from 'chalk';
13import ora from 'ora';
14import { logInfo, logError, logWarning } from './logger.mjs';
15
16// Simple PNG encoding function
17function encodePNG(width, height, pixelBuffer) {
18 const png = new PNG({ width, height });
19
20 // Copy pixel buffer to PNG data
21 for (let i = 0; i < pixelBuffer.length; i++) {
22 png.data[i] = pixelBuffer[i];
23 }
24
25 return PNG.sync.write(png);
26}
27
28// Generate sixel format with full RGBA color support
29function pixelBufferToSixel(pixelBuffer, width, height, scale = 2) {
30 let sixel = '\x1bPq'; // Start sixel mode
31
32 const scaledWidth = width * scale;
33 const scaledHeight = height * scale;
34
35 const colors = new Map();
36 let colorIndex = 0;
37
38 // Pre-allocate band arrays to avoid repeated allocation
39 const bandArrays = new Map();
40
41 // Generate sixel data in 6-pixel high bands
42 const bandsCount = Math.ceil(scaledHeight / 6);
43
44 for (let band = 0; band < bandsCount; band++) {
45 bandArrays.clear();
46
47 for (let x = 0; x < scaledWidth; x++) {
48 for (let dy = 0; dy < 6; dy++) {
49 const scaledY = band * 6 + dy;
50 if (scaledY >= scaledHeight) break;
51
52 // Map back to original coordinates
53 const origX = Math.floor(x / scale);
54 const origY = Math.floor(scaledY / scale);
55
56 if (origX < width && origY < height) {
57 const i = (origY * width + origX) * 4;
58 const r = pixelBuffer[i];
59 const g = pixelBuffer[i + 1];
60 const b = pixelBuffer[i + 2];
61 const a = pixelBuffer[i + 3];
62
63 // Skip transparent pixels
64 if (a === 0) continue;
65
66 // Simple color key to reduce string operations
67 const colorKey = (r << 16) | (g << 8) | b;
68
69 if (!colors.has(colorKey)) {
70 colors.set(colorKey, colorIndex++);
71 sixel += `#${colors.get(colorKey)};2;${Math.round(r*100/255)};${Math.round(g*100/255)};${Math.round(b*100/255)}`;
72 }
73
74 const color = colors.get(colorKey);
75 if (!bandArrays.has(color)) {
76 bandArrays.set(color, new Array(scaledWidth).fill(0));
77 }
78 bandArrays.get(color)[x] |= (1 << dy);
79 }
80 }
81 }
82
83 // Output band data
84 for (const [color, pixels] of bandArrays) {
85 sixel += `#${color}`;
86 for (const pixel of pixels) {
87 sixel += String.fromCharCode(63 + pixel);
88 }
89 sixel += '$';
90 }
91
92 // Move to next band
93 if (band < bandsCount - 1) {
94 sixel += '-';
95 }
96 }
97
98 sixel += '\x1b\\'; // Exit sixel mode
99 return sixel;
100}
101
102// Base headless AC environment
103export class HeadlessAC {
104 constructor(width = 128, height = 128, options = {}) {
105 this.width = width;
106 this.height = height;
107 this.pixelBuffer = new Uint8Array(width * height * 4);
108 this.graph = null;
109 this.disk = null;
110 this.text = null;
111 this.typeface = null;
112 this.typeModule = null;
113 this.apiCalls = [];
114 this.currentColor = [255, 255, 255, 255];
115 this.firstLineColorApplied = false; // Track if first-line color has been applied
116 this.hasDrawnFirstFrame = false; // Track if we've drawn the first frame for accumulation pieces
117 this.kidlispInstance = null; // Will be initialized when API is created
118 this.kidlispState = null; // Will hold state to restore
119 this.density = options.density || null; // Custom density parameter
120 this.outputDir = options.outputDir || null; // Directory for saving embedded layer buffers
121
122 // Performance tracking options
123 this.detailedTiming = options.detailedTiming || false;
124 this.operationTimings = new Map();
125
126 // Embedded layer restoration
127 this.deferredEmbeddedLayers = null; // Will hold embedded layers for restoration after KidLisp setup
128
129 // Deterministic random management (persisted across frames)
130 this.originalMathRandom = Math.random;
131 this.mathRandomOverrideInstalled = false;
132 this.randomState = null;
133
134 // Performance optimizations
135 this.enableV8Optimizations();
136 this.initializeRandomSystem();
137
138 // Initialize with opaque black (like normal AC environment)
139 // This ensures proper alpha blending behavior for semi-transparent elements
140 for (let i = 0; i < this.pixelBuffer.length; i += 4) {
141 this.pixelBuffer[i] = 0; // R
142 this.pixelBuffer[i + 1] = 0; // G
143 this.pixelBuffer[i + 2] = 0; // B
144 this.pixelBuffer[i + 3] = 255; // A (opaque)
145 }
146 }
147
148 // Restore embedded layers after KidLisp API is set up
149 restoreEmbeddedLayers() {
150 if (!this.deferredEmbeddedLayers || !this.kidlispInstance || !this.KidLisp) {
151 console.log(`🎬 HEADLESS DEBUG: Skipping embedded layer restoration - not ready yet`);
152 return;
153 }
154
155 console.log(`🎬 HEADLESS DEBUG: Restoring ${this.deferredEmbeddedLayers.length} embedded layer definitions from previous frame`);
156
157 // Set a flag to prevent the KidLisp module() function from clearing embedded layers
158 // This is critical for stateless frame-by-frame rendering
159 this.kidlispInstance.preserveEmbeddedLayers = true;
160
161 // FIXED: Properly restore layers with their rendered content and KidLisp instances
162 this.kidlispInstance.embeddedLayers = this.deferredEmbeddedLayers.map(layerMeta => {
163 // Restore the pixel buffer from disk if available
164 const buffer = layerMeta.buffer ? {
165 width: layerMeta.buffer.width,
166 height: layerMeta.buffer.height,
167 pixels: layerMeta.buffer.filename ?
168 this.loadEmbeddedLayerBuffer(layerMeta.buffer.filename) :
169 new Uint8ClampedArray(layerMeta.buffer.width * layerMeta.buffer.height * 4) // Empty buffer
170 } : null;
171
172 // Debug: Check if buffer has actual pixel data
173 if (buffer && buffer.pixels) {
174 let nonZeroPixels = 0;
175 for (let i = 0; i < Math.min(100, buffer.pixels.length); i += 4) {
176 if (buffer.pixels[i] !== 0 || buffer.pixels[i+1] !== 0 || buffer.pixels[i+2] !== 0) {
177 nonZeroPixels++;
178 }
179 }
180 console.log(`🔍 BUFFER DEBUG: Layer ${layerMeta.cacheId} has ${nonZeroPixels}/25 non-black pixels in first 100 bytes`);
181 } else {
182 console.log(`❌ BUFFER DEBUG: Layer ${layerMeta.cacheId} has NO buffer!`);
183 }
184
185 // Create a minimal KidLisp instance for the embedded layer
186 // This is required for updateEmbeddedLayer to work properly
187 let kidlispInstance = null;
188 console.log(`🔧 HEADLESS DEBUG: Attempting to restore KidLisp instance for ${layerMeta.cacheId}, hasMetaState=${!!layerMeta.kidlispInstanceState}, hasKidLispClass=${!!this.KidLisp}`);
189
190 if (this.KidLisp) {
191 // Create a new KidLisp instance for the embedded layer
192 kidlispInstance = new this.KidLisp();
193
194 // Restore minimal state for embedded layer instance
195 if (layerMeta.kidlispInstanceState) {
196 kidlispInstance.frameCount = layerMeta.kidlispInstanceState.frameCount || 0;
197 kidlispInstance.localFrameCount = layerMeta.kidlispInstanceState.localFrameCount || 0;
198 }
199
200 // Set the source code for the layer
201 if (layerMeta.sourceCode) {
202 kidlispInstance.currentSource = layerMeta.sourceCode;
203 // Parse the source code to create the parsedCode structure
204 try {
205 kidlispInstance.parsedCode = kidlispInstance.parse(layerMeta.sourceCode);
206 } catch (parseError) {
207 console.warn(`⚠️ Could not parse source code for layer ${layerMeta.cacheId}:`, parseError.message);
208 }
209 }
210
211 console.log(`✅ HEADLESS DEBUG: Successfully restored KidLisp instance for layer: ${layerMeta.cacheId}`);
212 } else {
213 console.log(`❌ HEADLESS DEBUG: Could not restore KidLisp instance for ${layerMeta.cacheId} - missing requirements`);
214 }
215
216 return {
217 ...layerMeta,
218 buffer: buffer,
219 kidlispInstance: kidlispInstance,
220 // Restore parsed code if available
221 parsedCode: kidlispInstance ? kidlispInstance.parsedCode : layerMeta.parsedCode
222 };
223 });
224
225 // Populate cache with references to the restored layers
226 // This is CRITICAL - without this, the system will recreate layers every frame
227 if (this.kidlispInstance.embeddedLayers) {
228 for (const layer of this.kidlispInstance.embeddedLayers) {
229 if (layer.layerKey || layer.cacheId) {
230 const cacheKey = layer.layerKey || layer.cacheId;
231 this.kidlispInstance.embeddedLayerCache.set(cacheKey, layer);
232 console.log(`🎯 HEADLESS DEBUG: Restored layer cache entry: ${cacheKey}`);
233 }
234 }
235 }
236
237 // IMPORTANT: Reset the preservation flag AFTER restoration is complete
238 // This allows the KidLisp code to run normally after the initial restoration
239 // The layers are now in the cache and will be reused via the cache lookup
240 this.kidlispInstance.preserveEmbeddedLayers = false;
241 console.log(`🔓 HEADLESS: Preservation flag reset, layers now managed via cache`);
242
243 // Clear deferred layers
244 this.deferredEmbeddedLayers = null;
245 }
246
247 // Save embedded layer buffer to disk (similar to background buffer)
248 saveEmbeddedLayerBuffer(layerId, pixelData) {
249 if (!this.outputDir) {
250 console.warn('⚠️ No output directory set for saving embedded layer buffers');
251 return null;
252 }
253
254 try {
255 const filename = `embedded-layer-${layerId}.bin`;
256 const filepath = path.join(this.outputDir, filename);
257 fs.writeFileSync(filepath, pixelData);
258 console.log(`💾 Saved embedded layer buffer: ${filename} (${pixelData.length} bytes)`);
259 return filename;
260 } catch (error) {
261 console.warn(`⚠️ Failed to save embedded layer buffer for ${layerId}:`, error.message);
262 return null;
263 }
264 }
265
266 // Load embedded layer buffer from disk
267 loadEmbeddedLayerBuffer(filename) {
268 if (!this.outputDir || !filename) {
269 return null;
270 }
271
272 try {
273 const filepath = path.join(this.outputDir, filename);
274 if (fs.existsSync(filepath)) {
275 const bufferData = fs.readFileSync(filepath);
276 console.log(`📥 Loaded embedded layer buffer: ${filename} (${bufferData.length} bytes)`);
277 return new Uint8ClampedArray(bufferData);
278 }
279 } catch (error) {
280 console.warn(`⚠️ Failed to load embedded layer buffer ${filename}:`, error.message);
281 }
282 return null;
283 }
284
285 // Set KidLisp state for restoration
286 setKidlispState(state) {
287 this.kidlispState = state;
288
289 // If we have a KidLisp instance, restore the state immediately
290 if (this.kidlispInstance && state) {
291 // Basic state restoration
292 if (state.onceExecuted) {
293 this.kidlispInstance.onceExecuted = new Set(state.onceExecuted);
294 }
295 if (state.currentSource !== undefined) {
296 this.kidlispInstance.currentSource = state.currentSource;
297 }
298 if (state.firstLineColor !== undefined) {
299 this.kidlispInstance.firstLineColor = state.firstLineColor;
300 }
301 if (state.scrollFuzzDirection !== undefined) {
302 this.kidlispInstance.scrollFuzzDirection = state.scrollFuzzDirection;
303 }
304
305 // Critical timing state restoration
306 if (state.lastSecondExecutions) {
307 this.kidlispInstance.lastSecondExecutions = { ...state.lastSecondExecutions };
308 }
309 if (state.sequenceCounters) {
310 this.kidlispInstance.sequenceCounters = new Map(Object.entries(state.sequenceCounters));
311 }
312 if (state.frameCount !== undefined) {
313 this.kidlispInstance.frameCount = state.frameCount;
314 }
315 if (state.instantTriggersExecuted) {
316 this.kidlispInstance.instantTriggersExecuted = { ...state.instantTriggersExecuted };
317 }
318
319 // Advanced timing state restoration
320 if (state.timingStates) {
321 this.kidlispInstance.timingStates = new Map(Object.entries(state.timingStates));
322 }
323 if (state.activeTimingExpressions) {
324 this.kidlispInstance.activeTimingExpressions = new Map(Object.entries(state.activeTimingExpressions));
325 }
326
327 // Store embedded layers for later restoration (after KidLisp class is available)
328 if (state.embeddedLayers) {
329 console.log(`🎬 HEADLESS DEBUG: Deferring restoration of ${state.embeddedLayers.length} embedded layer definitions (will restore after KidLisp API setup)`);
330 this.deferredEmbeddedLayers = state.embeddedLayers;
331 }
332
333 // Initialize empty cache - will be populated by restoreEmbeddedLayers
334 this.kidlispInstance.embeddedLayerCache = new Map();
335
336 // Ink and visual state restoration
337 if (state.inkState !== undefined) {
338 this.kidlispInstance.inkState = state.inkState;
339 }
340 if (state.inkStateSet !== undefined) {
341 this.kidlispInstance.inkStateSet = state.inkStateSet;
342 }
343
344 // Baked layers state restoration
345 if (state.bakedLayers) {
346 this.kidlispInstance.bakedLayers = [...state.bakedLayers];
347 }
348 if (state.bakeCallCount !== undefined) {
349 this.kidlispInstance.bakeCallCount = state.bakeCallCount;
350 }
351
352 // Local environment restoration
353 if (state.localEnv) {
354 this.kidlispInstance.localEnv = { ...state.localEnv };
355 }
356
357 // Rainbow and zebra state restoration
358 if (state.rainbowState) {
359 setRainbowState(state.rainbowState);
360 }
361 if (state.zebraState) {
362 setZebraState(state.zebraState);
363 }
364
365 console.log(`🔄 Restored KidLisp state: frame=${this.kidlispInstance.frameCount}, timing entries=${Object.keys(this.kidlispInstance.lastSecondExecutions).length}, sequence counters=${this.kidlispInstance.sequenceCounters.size}`);
366 }
367 }
368
369 // Get current KidLisp state for saving
370 getKidlispState() {
371 if (this.kidlispInstance) {
372 return {
373 // Basic state (already captured)
374 onceExecuted: Array.from(this.kidlispInstance.onceExecuted), // Convert Set to Array for JSON
375 currentSource: this.kidlispInstance.currentSource,
376 firstLineColor: this.kidlispInstance.firstLineColor,
377 scrollFuzzDirection: this.kidlispInstance.scrollFuzzDirection,
378
379 // Critical timing state for frame continuity
380 lastSecondExecutions: this.kidlispInstance.lastSecondExecutions || {},
381 sequenceCounters: this.kidlispInstance.sequenceCounters ? Object.fromEntries(this.kidlispInstance.sequenceCounters) : {},
382 frameCount: this.kidlispInstance.frameCount || 0,
383 instantTriggersExecuted: this.kidlispInstance.instantTriggersExecuted || {},
384
385 // Advanced timing state
386 timingStates: this.kidlispInstance.timingStates ? Object.fromEntries(this.kidlispInstance.timingStates) : {},
387 activeTimingExpressions: this.kidlispInstance.activeTimingExpressions ? Object.fromEntries(this.kidlispInstance.activeTimingExpressions) : {},
388
389 // Ink and visual state
390 inkState: this.kidlispInstance.inkState,
391 inkStateSet: this.kidlispInstance.inkStateSet || false,
392
393 // Baked layers state
394 bakedLayers: this.kidlispInstance.bakedLayers || [],
395 bakeCallCount: this.kidlispInstance.bakeCallCount || 0,
396
397 // Embedded layers state (CRITICAL for multi-frame rendering)
398 // FIXED: Serialize layers WITH pixel buffers to maintain rendered content
399 embeddedLayers: this.kidlispInstance.embeddedLayers ? this.kidlispInstance.embeddedLayers.map(layer => ({
400 id: layer.id,
401 x: layer.x,
402 y: layer.y,
403 width: layer.width,
404 height: layer.height,
405 alpha: layer.alpha,
406 source: layer.source,
407 sourceCode: layer.sourceCode,
408 hasBeenEvaluated: layer.hasBeenEvaluated,
409 lastFrameEvaluated: layer.lastFrameEvaluated,
410 lastRenderTime: layer.lastRenderTime,
411 cacheId: layer.cacheId,
412 layerKey: layer.layerKey,
413 localFrameCount: layer.localFrameCount || 0,
414 timingPattern: layer.timingPattern,
415 // Save buffer to disk and store metadata only
416 buffer: layer.buffer ? {
417 width: layer.buffer.width,
418 height: layer.buffer.height,
419 filename: this.saveEmbeddedLayerBuffer(layer.cacheId || layer.id || `layer_${Date.now()}`, layer.buffer.pixels)
420 } : null,
421 // Save complete KidLisp instance state including timing for persistence
422 kidlispInstanceState: layer.kidlispInstance ? {
423 frameCount: layer.kidlispInstance.frameCount || 0,
424 localFrameCount: layer.localFrameCount || 0,
425 lastSecondExecutions: layer.kidlispInstance.lastSecondExecutions || [],
426 sequenceCounters: layer.kidlispInstance.sequenceCounters ?
427 Object.fromEntries(layer.kidlispInstance.sequenceCounters) : {},
428 timingStates: layer.kidlispInstance.timingStates ?
429 Object.fromEntries(layer.kidlispInstance.timingStates) : {}
430 } : null
431 })) : [],
432 // Save embedded layer cache structure WITHOUT pixel data but with metadata
433 embeddedLayerCache: this.kidlispInstance.embeddedLayerCache ?
434 Object.fromEntries(
435 Array.from(this.kidlispInstance.embeddedLayerCache.entries()).map(([key, layer]) => [
436 key,
437 {
438 ...layer,
439 buffer: layer.buffer ? {
440 width: layer.buffer.width,
441 height: layer.buffer.height,
442 filename: layer.buffer.filename || `${key}.bin`
443 } : null,
444 kidlispInstance: layer.kidlispInstance ? {
445 frameCount: layer.kidlispInstance.frameCount || 0,
446 localFrameCount: layer.kidlispInstance.localFrameCount || 0,
447 lastSecondExecutions: layer.kidlispInstance.lastSecondExecutions || [],
448 sequenceCounters: layer.kidlispInstance.sequenceCounters ?
449 Object.fromEntries(layer.kidlispInstance.sequenceCounters) : {},
450 timingStates: layer.kidlispInstance.timingStates ?
451 Object.fromEntries(layer.kidlispInstance.timingStates) : {}
452 } : null
453 }
454 ])
455 ) : {},
456
457 // Local environment (cleaned of pixel buffers)
458 localEnv: this.cleanEnvForSerialization(this.kidlispInstance.localEnv || {}),
459
460 // Rainbow and zebra color cycling state
461 rainbowState: getRainbowState(),
462 zebraState: getZebraState()
463 };
464 }
465 return {};
466 }
467
468 // Deterministic random helpers -------------------------------------------------
469 initializeRandomSystem(state = null) {
470 this.randomState = this.normalizeRandomState(state);
471 this.installMathRandomOverride();
472 }
473
474 installMathRandomOverride() {
475 if (this.mathRandomOverrideInstalled) {
476 return;
477 }
478 const self = this;
479 Math.random = function() {
480 return self.nextRandom();
481 };
482 this.mathRandomOverrideInstalled = true;
483 }
484
485 normalizeRandomState(state) {
486 let seed;
487 let sequence;
488 if (state && typeof state.seed === 'number') {
489 seed = state.seed >>> 0;
490 sequence = typeof state.sequence === 'number' ? state.sequence >>> 0 : 0;
491 } else {
492 seed = this.generateRandomSeed();
493 sequence = 0;
494 }
495 return { seed, sequence };
496 }
497
498 generateRandomSeed() {
499 try {
500 return crypto.randomBytes(4).readUInt32LE(0);
501 } catch (error) {
502 const fallback = Number((BigInt(Date.now()) ^ process.hrtime.bigint()) & BigInt(0xffffffff));
503 return fallback >>> 0;
504 }
505 }
506
507 nextRandom() {
508 if (!this.randomState) {
509 this.randomState = this.normalizeRandomState();
510 }
511 let seed = (this.randomState.seed + 0x6D2B79F5) >>> 0;
512 this.randomState.seed = seed;
513
514 let t = Math.imul(seed ^ (seed >>> 15), seed | 1);
515 t ^= t + Math.imul(t ^ (t >>> 7), (61 | t));
516 const result = ((t ^ (t >>> 14)) >>> 0) / 4294967296;
517
518 this.randomState.sequence = ((this.randomState.sequence || 0) + 1) >>> 0;
519 return result;
520 }
521
522 setRandomState(state) {
523 this.randomState = this.normalizeRandomState(state);
524 this.installMathRandomOverride();
525 }
526
527 getRandomState() {
528 if (!this.randomState) {
529 this.randomState = this.normalizeRandomState();
530 }
531 return { ...this.randomState };
532 }
533
534 // Clean environment objects of pixel buffer data for serialization
535 cleanEnvForSerialization(env) {
536 if (!env || typeof env !== 'object') return env;
537
538 const cleaned = {};
539 for (const [key, value] of Object.entries(env)) {
540 if (key === 'screen' && value && value.pixels) {
541 cleaned[key] = {
542 width: value.width,
543 height: value.height
544 // Exclude pixel buffer to avoid massive serialization payloads
545 };
546 } else if (value && typeof value === 'object' && !Array.isArray(value)) {
547 cleaned[key] = this.cleanEnvForSerialization(value);
548 } else {
549 cleaned[key] = value;
550 }
551 }
552 return cleaned;
553 }
554
555 // Enable V8 optimizations for performance
556 enableV8Optimizations() {
557 // Hint to V8 that these are hot functions
558 if (global.gc) {
559 logInfo('💫 V8 optimizations: Garbage collection available');
560 }
561
562 // REDUCED ALLOCATIONS: Only allocate scratch buffers if really needed
563 // this.scratchBuffer = new Uint8Array(this.width * this.height * 4);
564 this.colorCache = new Map();
565
566 logInfo(`🚀 Performance mode: Pixel buffer ${this.width}x${this.height} (${(this.pixelBuffer.length / 1024 / 1024).toFixed(2)}MB)`);
567 }
568
569 async initializeAC() {
570 const spinner = ora(chalk.blue('🔧 Setting up full AC environment...')).start();
571
572 try {
573 // 🚀 Enable V8 performance optimizations
574 if (typeof global.gc === 'function') {
575 global.gc(); // Clean up before starting
576 console.log('💫 Garbage collection available - optimizing memory');
577 }
578
579 // Optimize Node.js for graphics workloads
580 if (process.env.NODE_ENV !== 'production') {
581 process.env.NODE_ENV = 'production'; // Enable V8 optimizations
582 }
583
584 // Set up browser-like globals for Node.js
585 global.window = global.window || global;
586 global.document = global.document || {
587 createElement: () => ({}),
588 body: { appendChild: () => {} }
589 };
590
591 // Add requestAnimationFrame and other timing functions
592 global.requestAnimationFrame = global.requestAnimationFrame || ((callback) => {
593 return setTimeout(() => callback(Date.now()), 16);
594 });
595 global.cancelAnimationFrame = global.cancelAnimationFrame || clearTimeout;
596
597 // Add performance API - deterministic timing support
598 global.performance = global.performance || {
599 now: () => global.ac?.api?.simulationTime || Date.now() // Use deterministic time if available
600 };
601
602 if (!global.navigator) {
603 try {
604 global.navigator = {
605 userAgent: 'HeadlessAC/1.0',
606 platform: 'HeadlessAC',
607 language: 'en-US',
608 onLine: true,
609 cookieEnabled: false
610 };
611 } catch (e) {
612 if (global.navigator) {
613 Object.assign(global.navigator, {
614 userAgent: 'HeadlessAC/1.0',
615 platform: 'HeadlessAC'
616 });
617 }
618 }
619 }
620
621 // Set up global.location with optional density parameter
622 const searchParams = this.density ? `?density=${this.density}` : '';
623 global.location = global.location || {
624 href: `http://localhost:8888${searchParams}`,
625 origin: 'http://localhost:8888',
626 protocol: 'http:',
627 host: 'localhost:8888',
628 pathname: '/',
629 search: searchParams,
630 hash: ''
631 };
632
633 // Import and initialize graph.mjs
634 spinner.text = 'Loading graph.mjs...';
635 const graphModule = await import("../../../system/public/aesthetic.computer/lib/graph.mjs");
636 this.graph = graphModule;
637
638 // Create pixel buffer for graph.mjs
639 const buffer = {
640 width: this.width,
641 height: this.height,
642 pixels: this.pixelBuffer
643 };
644
645 this.graph.setBuffer(buffer);
646 spinner.text = 'Pixel buffer initialized...';
647
648 // 🧪 EXPERIMENTAL: Setup block processing if requested
649 if (this.options && this.options.useBlockProcessing) {
650 this.graph.setBlockProcessing(true);
651 spinner.text = 'Block processing enabled...';
652 }
653
654 // Import the full disk.mjs system
655 spinner.text = 'Loading disk.mjs...';
656 const diskModule = await import("../../../system/public/aesthetic.computer/lib/disk.mjs");
657 this.disk = diskModule;
658
659 // Try to import text module for font debugging
660 try {
661 spinner.text = 'Loading text system...';
662 const textModule = await import("../../../system/public/aesthetic.computer/lib/text.mjs");
663 this.text = textModule;
664 logInfo(chalk.green('✅ Text module loaded:'), Object.keys(textModule));
665 } catch (error) {
666 logWarning(chalk.yellow('⚠️ Text module not found:', error.message));
667 }
668
669 // Load the type system and create a typeface
670 try {
671 spinner.text = 'Loading font system...';
672 const typeModule = await import("../../../system/public/aesthetic.computer/lib/type.mjs");
673 this.typeModule = typeModule;
674
675 // Create a typeface instance for font_1
676 this.typeface = new typeModule.Typeface("font_1");
677
678 // Override the load method to properly filter out non-string values
679 const originalLoad = this.typeface.load.bind(this.typeface);
680 this.typeface.load = async function($preload, needsPaintCallback) {
681 if (this.name === "font_1") {
682 // Filter entries to only include actual glyph paths (string values, not prefixed with "glyph")
683 const glyphsToLoad = Object.entries(this.data).filter(
684 ([g, loc]) => !g.startsWith("glyph") && typeof loc === 'string' && loc !== 'false'
685 );
686 const promises = glyphsToLoad.map(([glyph, location], i) => {
687 return $preload(
688 `aesthetic.computer/disks/drawings/${this.name}/${location}.json`,
689 )
690 .then((res) => {
691 this.glyphs[glyph] = res;
692 })
693 .catch((err) => {
694 // Silently handle missing glyph files - some glyphs may not exist
695 });
696 });
697 await Promise.all(promises);
698 } else {
699 // For other fonts, use the original method
700 return originalLoad($preload, needsPaintCallback);
701 }
702 };
703
704 // Create a mock $preload function for loading glyph data
705 const mockPreload = async (path) => {
706 try {
707 // Convert AC path to actual file path using absolute path resolution
708 const __filename = fileURLToPath(import.meta.url);
709 const __dirname = dirname(__filename);
710 const actualPath = resolve(__dirname, '../../../system/public', path);
711
712 // Read JSON file directly
713 const jsonData = readFileSync(actualPath, 'utf8');
714 return JSON.parse(jsonData);
715 } catch (error) {
716 console.warn(`⚠️ Could not load glyph: ${path}`, error.message);
717 return null;
718 }
719 };
720
721 // Load the font glyphs
722 await this.typeface.load(mockPreload);
723 logInfo(chalk.green('✅ Font system loaded with glyphs:'), Object.keys(this.typeface.glyphs).length, 'characters');
724
725 // Debug first few glyphs to see what we loaded
726 const glyphKeys = Object.keys(this.typeface.glyphs).slice(0, 5);
727 for (const key of glyphKeys) {
728 const glyph = this.typeface.glyphs[key];
729 if (glyph && Array.isArray(glyph)) {
730 console.log(`🔍 Glyph "${key}" (char ${key.charCodeAt(0)}): ${glyph.length} commands`);
731 if (glyph.length > 0) {
732 console.log(`📝 First command:`, glyph[0]);
733 }
734 }
735 }
736
737 } catch (error) {
738 console.log(chalk.yellow('⚠️ Font system load failed:', error.message));
739 this.typeface = null;
740 }
741
742 spinner.succeed(chalk.green('🚀 AC system loaded successfully'));
743
744 } catch (error) {
745 spinner.fail(chalk.red('💥 Error loading AC system'));
746 console.error(chalk.red(error));
747 throw error;
748 }
749 }
750
751 async createAPI() {
752 const self = this;
753
754 function logCall(name, args) {
755 // DISABLED: Skip API call logging during video recording to reduce memory pressure
756 // self.apiCalls.push({ name, args: Array.from(args), timestamp: Date.now() });
757 }
758
759 // Performance timing wrapper for graph operations
760 function timeGraphOperation(operationName, graphFunc, ...args) {
761 const result = graphFunc(...args);
762
763 // DISABLED: Skip timing logs during video recording to save memory
764 // Only track for critical operations or debugging
765 if (self.detailedTiming && operationName === 'critical_debug_only') {
766 const startTime = process.hrtime.bigint();
767 const endTime = process.hrtime.bigint();
768 const duration = Number(endTime - startTime) / 1000000;
769 console.log(`⚡ ${operationName}: ${duration.toFixed(3)}ms`);
770 }
771
772 return result;
773 }
774
775 // Try to use real disk.mjs API if available
776 if (this.disk && this.graph) {
777 logInfo('🔧 Creating graph-based API...');
778
779 const api = {};
780
781 // Basic properties
782 api.screen = {
783 width: this.width,
784 height: this.height,
785 pixels: this.pixelBuffer
786 };
787 api.pen = { x: 0, y: 0 };
788
789 // Drawing functions that call graph.mjs directly
790 api.wipe = function(...rawArgs) {
791 logCall('wipe', rawArgs);
792
793 // Support optional options object as last parameter (e.g. { force: true })
794 const args = [...rawArgs];
795 let options = {};
796 if (args.length > 0) {
797 const maybeOptions = args[args.length - 1];
798 if (maybeOptions && typeof maybeOptions === 'object' && !Array.isArray(maybeOptions)) {
799 options = { ...maybeOptions };
800 args.pop();
801 }
802 }
803
804 if (args.length > 0) {
805 const foundColor = timeGraphOperation('findColor', self.graph.findColor.bind(self.graph), ...args);
806 if (foundColor && typeof foundColor.length === 'number') {
807 self.currentColor = Array.from(foundColor);
808 }
809 timeGraphOperation('color', self.graph.color.bind(self.graph), ...foundColor);
810 }
811
812 const skipForBackground = self.backgroundBufferRestored && !options.force;
813 const skipForFirstLine = options.firstLineOnce && self.firstLineColorApplied;
814 const shouldSkipClear = skipForBackground || skipForFirstLine;
815
816 if (shouldSkipClear) {
817 if (skipForFirstLine) {
818 console.log('🎨 Skipping clear - first-line wipe already applied this render');
819 } else {
820 console.log('🎨 Skipping clear - background buffer was restored');
821 }
822 } else {
823 timeGraphOperation('clear', self.graph.clear.bind(self.graph));
824 if (options.firstLineOnce) {
825 self.firstLineColorApplied = true;
826 }
827 }
828
829 if (options.firstLineOnce && !self.firstLineColorApplied) {
830 // Ensure we flag the first-line wipe even if skip path was taken
831 self.firstLineColorApplied = true;
832 }
833
834 return api;
835 };
836
837 api.ink = function(...args) {
838 logCall('ink', args);
839 const foundColor = timeGraphOperation('findColor', self.graph.findColor.bind(self.graph), ...args);
840 if (foundColor && typeof foundColor.length === 'number') {
841 self.currentColor = Array.from(foundColor);
842 }
843 timeGraphOperation('color', self.graph.color.bind(self.graph), ...foundColor);
844 return api;
845 };
846
847 api.line = function(...args) {
848 logCall('line', args);
849 timeGraphOperation('line', self.graph.line.bind(self.graph), ...args);
850 return api;
851 };
852
853 api.circle = function(...args) {
854 logCall('circle', args);
855 timeGraphOperation('circle', self.graph.circle.bind(self.graph), ...args);
856 return api;
857 };
858
859 api.write = function(...args) {
860 logCall('write', args);
861
862 // Use the real AC typeface system if available
863 if (self.typeface && Object.keys(self.typeface.glyphs).length > 0) {
864 try {
865 // Create a mock $ object with the functions typeface.print needs
866 const mockAPI = {
867 screen: { width: self.width, height: self.height },
868 inkrn: () => {
869 // Return the actual current color from graph system
870 if (self.graph && self.graph.c) {
871 return self.graph.c.slice(); // Return copy of current color
872 }
873 return [255, 255, 255]; // Fallback to white
874 },
875 ink: (...color) => {
876 if (color.length > 0) {
877 const foundColor = self.graph.findColor(...color);
878 self.graph.color(...foundColor);
879 }
880 return mockAPI;
881 },
882 box: (x, y, w, h) => {
883 self.graph.box(x, y, w, h);
884 return mockAPI;
885 },
886 printLine: (text, font, x, y, blockWidth, size, xOffset, thickness, rotation, fontData) => {
887 // Use graph.mjs printLine function directly
888 if (self.graph && self.graph.printLine) {
889 self.graph.printLine(text, font, x, y, blockWidth, size, xOffset, thickness, rotation, fontData);
890 } else {
891 console.warn('⚠️ graph.printLine not available');
892 }
893 return mockAPI;
894 },
895 num: {
896 randIntRange: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
897 }
898 };
899
900 // Parse arguments like AC's write function
901 let x, y, text, size = 0, pos = {};
902 if (args.length >= 4) {
903 [text, x, y, size] = args;
904 pos = { x, y };
905 } else if (args.length >= 3) {
906 [text, x, y] = args;
907 pos = { x, y };
908 } else if (args.length === 2) {
909 [text, pos] = args;
910 } else if (args.length === 1) {
911 text = args[0];
912 }
913
914 // Call the real typeface print method with proper size
915 self.typeface.print(mockAPI, pos, size, text);
916
917 } catch (error) {
918 console.warn('⚠️ Typeface rendering failed, using fallback:', error.message);
919 // Fallback to simple text rendering
920 if (args.length >= 3) {
921 self.renderSimpleText(args[0], args[1], args[2]);
922 }
923 }
924 } else {
925 // Fallback to simple text rendering if no typeface available
926 if (args.length >= 3) {
927 self.renderSimpleText(args[0], args[1], args[2]);
928 }
929 }
930
931 return api;
932 };
933
934 api.rect = function(...args) {
935 logCall('rect', args);
936 timeGraphOperation('box', self.graph.box.bind(self.graph), ...args);
937 return api;
938 };
939
940 api.box = function(...args) {
941 logCall('box', args);
942 timeGraphOperation('box', self.graph.box.bind(self.graph), ...args);
943 return api;
944 };
945
946 api.point = function(...args) {
947 logCall('point', args);
948 timeGraphOperation('point', self.graph.point.bind(self.graph), ...args);
949 return api;
950 };
951
952 api.plot = function(...args) {
953 logCall('plot', args);
954 timeGraphOperation('point', self.graph.point.bind(self.graph), ...args);
955 return api;
956 };
957
958 api.repeat = function(count, callback) {
959 logCall('repeat', [count]);
960 if (typeof count === 'number' && typeof callback === 'function') {
961 for (let i = 0; i < count; i++) {
962 // Force rainbow advancement for each iteration to create different colors
963 resetRainbowCache();
964 callback();
965 }
966 } else {
967 console.warn('⚠️ repeat expects (count, callback)');
968 }
969 return api;
970 };
971
972 // Add transformation functions that work with graph.mjs system
973 api.spin = function(...args) {
974 logCall('spin', args);
975 console.log(`🔄 Spin: ${args.join(', ')}`);
976
977 // Call the real graph.mjs spin function
978 if (self.graph && self.graph.spin) {
979 timeGraphOperation('spin', self.graph.spin.bind(self.graph), ...args);
980 } else {
981 console.log('⚠️ Graph spin not available, using fallback');
982 if (args.length > 0) {
983 self.applySpinTransformation(args[0]);
984 }
985 }
986 return api;
987 };
988
989 api.zoom = function(...args) {
990 logCall('zoom', args);
991 console.log(`🔍 Zoom: ${args.join(', ')}`);
992
993 // Call the real graph.mjs zoom function
994 if (self.graph && self.graph.zoom) {
995 timeGraphOperation('zoom', self.graph.zoom.bind(self.graph), ...args);
996 } else {
997 console.log('⚠️ Graph zoom not available, using fallback');
998 if (args.length > 0) {
999 self.applyZoomTransformation(args[0]);
1000 }
1001 }
1002 return api;
1003 };
1004
1005 api.contrast = function(...args) {
1006 logCall('contrast', args);
1007 console.log(`🎨 Contrast: ${args.join(', ')}`);
1008
1009 // Call the real graph.mjs contrast function
1010 if (self.graph && self.graph.contrast) {
1011 timeGraphOperation('contrast', self.graph.contrast.bind(self.graph), ...args);
1012 } else {
1013 console.log('⚠️ Graph contrast not available, using fallback');
1014 if (args.length > 0) {
1015 self.applyContrastTransformation(args[0]);
1016 }
1017 }
1018 return api;
1019 };
1020
1021 api.blur = function(...args) {
1022 logCall('blur', args);
1023 const logMessage = `Blur: ${args.join(', ')}`;
1024 if (self.logger && typeof self.logger === 'function') {
1025 self.logger('blur', logMessage);
1026 } else {
1027 console.log(`🌀 ${logMessage}`);
1028 }
1029
1030 // Call the real graph.mjs blur function
1031 if (self.graph && self.graph.blur) {
1032 self.graph.blur(...args);
1033 } else {
1034 console.log('⚠️ Graph blur not available, using fallback');
1035 if (args.length > 0) {
1036 self.applyBlurTransformation(args[0]);
1037 }
1038 }
1039 return api;
1040 };
1041
1042 api.scroll = function(...args) {
1043 logCall('scroll', args);
1044 logInfo(`📜 Scroll: ${args.join(', ')}`);
1045
1046 // Call the real graph.mjs scroll function
1047 if (self.graph && self.graph.scroll) {
1048 timeGraphOperation('scroll', self.graph.scroll.bind(self.graph), ...args);
1049 } else {
1050 console.log('⚠️ Graph scroll not available, using fallback');
1051 if (args.length >= 2) {
1052 self.applyScrollTransformation(args[0], args[1]);
1053 }
1054 }
1055 return api;
1056 };
1057
1058 // Add comprehensive num utilities needed by AC disks
1059 api.num = {
1060 radians: (degrees) => degrees * (Math.PI / 180),
1061 degrees: (radians) => radians * (180 / Math.PI),
1062 randInt: (max) => Math.floor(Math.random() * max),
1063 randIntRange: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
1064 map: (value, start1, stop1, start2, stop2) => {
1065 return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
1066 },
1067 lerp: (start, stop, amount) => start + (stop - start) * amount,
1068 clamp: (value, min, max) => Math.max(min, Math.min(max, value)),
1069 dist: (x1, y1, x2, y2) => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2),
1070 timestamp: () => api.simulationTime || Date.now() // Use deterministic time if available
1071 };
1072
1073 // Add clock utilities for timing-based animations
1074 api.clock = {
1075 time: () => api.simulationTime ? new Date(api.simulationTime) : new Date()
1076 };
1077
1078 // Initialize simulation time tracking
1079 api.simulationTime = null; // Will be set by tape.mjs for deterministic recording
1080
1081 // Set up global access for deterministic timing
1082 global.ac = global.ac || {};
1083 global.ac.api = api;
1084
1085 // Add KidLisp-specific functions that are missing
1086
1087 // Flood function - fills connected areas with current ink color
1088 api.flood = function(...args) {
1089 logCall('flood', args);
1090 console.log(`🌊 Flood: ${args.join(', ')}`);
1091
1092 // Basic flood fill implementation
1093 if (self.graph && self.graph.flood) {
1094 console.log(`🔧 FLOOD DEBUG: Using graph.flood, api.screen sample before:`, Array.from(api.screen.pixels.slice(0, 20)));
1095 const floodArgs = [...args];
1096 if (floodArgs.length <= 2 && self.currentColor && typeof self.currentColor.length === 'number') {
1097 floodArgs.push(Array.from(self.currentColor));
1098 }
1099 self.graph.flood(...floodArgs);
1100 console.log(`🔧 FLOOD DEBUG: Using graph.flood, api.screen sample after:`, Array.from(api.screen.pixels.slice(0, 20)));
1101 } else {
1102 console.log('⚠️ Graph flood not available, using simple fill');
1103 // Simple fill the entire screen as fallback
1104 const [r, g, b, a] = self.currentColor || [255, 255, 255, 255];
1105 for (let i = 0; i < self.pixelBuffer.length; i += 4) {
1106 self.pixelBuffer[i] = r;
1107 self.pixelBuffer[i + 1] = g;
1108 self.pixelBuffer[i + 2] = b;
1109 self.pixelBuffer[i + 3] = a;
1110 }
1111 }
1112 return api;
1113 };
1114
1115 // Color name functions - KidLisp uses these as background setters
1116 const createBackgroundShortcut = (colorName) => {
1117 return function(...args) {
1118 logCall(colorName, args);
1119 const wipeArgs = args.length > 0 ? args : [colorName];
1120 return api.wipe(...wipeArgs, { force: true });
1121 };
1122 };
1123
1124 api.black = createBackgroundShortcut('black');
1125 api.salmon = createBackgroundShortcut('salmon');
1126 api.white = createBackgroundShortcut('white');
1127 api.gray = createBackgroundShortcut('gray');
1128
1129 // Random choice helpers - mirror KidLisp's choose/? behavior
1130 api.choose = function(...choices) {
1131 logCall('choose', choices);
1132 if (choices.length === 0) {
1133 return undefined;
1134 }
1135 const randomIndex = Math.floor(Math.random() * choices.length);
1136 return choices[randomIndex];
1137 };
1138
1139 api['?'] = function(...choices) {
1140 logCall('?', choices);
1141 if (choices.length > 0) {
1142 if (api.help && typeof api.help.choose === 'function') {
1143 return api.help.choose(...choices);
1144 }
1145 return api.choose(...choices);
1146 }
1147 // KidLisp treats bare ? as contextual randomness handled by callers
1148 return undefined;
1149 };
1150
1151 // Width and height variables that KidLisp pieces often use
1152 api.w = self.width;
1153 api.h = self.height;
1154 api.width = self.width;
1155 api.height = self.height;
1156
1157 // Page function for switching drawing buffers (needed by KidLisp embed system)
1158 api.page = function(buffer) {
1159 if (buffer && buffer.width && buffer.height && buffer.pixels) {
1160 // Switch to the new buffer
1161 api.screen = {
1162 width: buffer.width,
1163 height: buffer.height,
1164 pixels: buffer.pixels
1165 };
1166 // Update the graph drawing context
1167 if (self.graph && self.graph.setBuffer) {
1168 self.graph.setBuffer(buffer);
1169 console.log(`🔧 PAGE DEBUG: Graph setBuffer called, buffer sample:`, Array.from(buffer.pixels.slice(0, 20)));
1170 }
1171 console.log(`📄 Switched to buffer: ${buffer.width}x${buffer.height}`);
1172 console.log(`🔍 PAGE DEBUG: api.screen.pixels sample:`, Array.from(api.screen.pixels.slice(0, 20)));
1173 } else {
1174 console.warn('⚠️ page() called with invalid buffer:', buffer);
1175 }
1176 };
1177
1178 // Write function for text rendering (used by embedded KidLisp pieces)
1179 api.write = function(...args) {
1180 logCall('write', args);
1181 const logMessage = `Write: ${args[0] || ''}`;
1182 if (self.logger && typeof self.logger === 'function') {
1183 self.logger('write', logMessage);
1184 } else {
1185 // console.log(`📝 ${logMessage}`); // Commented out for performance during recording
1186 }
1187
1188 // Use the real AC typeface system if available
1189 if (self.typeface && Object.keys(self.typeface.glyphs).length > 0) {
1190 try {
1191 // Create a mock $ object with the functions typeface.print needs
1192 const mockAPI = {
1193 screen: { width: self.width, height: self.height },
1194 inkrn: () => {
1195 // Return the actual current color from graph system
1196 if (self.graph && self.graph.c) {
1197 return self.graph.c.slice(); // Return copy of current color
1198 }
1199 return [255, 255, 255]; // Fallback to white
1200 },
1201 ink: (...color) => {
1202 if (color.length > 0) {
1203 const foundColor = self.graph.findColor(...color);
1204 self.graph.color(...foundColor);
1205 }
1206 return mockAPI;
1207 },
1208 box: (x, y, w, h) => {
1209 self.graph.box(x, y, w, h);
1210 return mockAPI;
1211 },
1212 printLine: (text, font, x, y, blockWidth, size, xOffset, thickness, rotation, fontData) => {
1213 // Use graph.mjs printLine function directly
1214 if (self.graph && self.graph.printLine) {
1215 self.graph.printLine(text, font, x, y, blockWidth, size, xOffset, thickness, rotation, fontData);
1216 } else {
1217 console.warn('⚠️ graph.printLine not available');
1218 }
1219 return mockAPI;
1220 },
1221 num: {
1222 randIntRange: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
1223 }
1224 };
1225
1226 // Parse arguments like AC's write function
1227 let x, y, text, size = 0, pos = {};
1228 if (args.length >= 4) {
1229 [text, x, y, size] = args;
1230 pos = { x, y };
1231 } else if (args.length >= 3) {
1232 [text, x, y] = args;
1233 pos = { x, y };
1234 } else if (args.length === 2) {
1235 [text, pos] = args;
1236 } else if (args.length === 1) {
1237 text = args[0];
1238 }
1239
1240 // Call the real typeface print method with proper size
1241 self.typeface.print(mockAPI, pos, size, text);
1242
1243 } catch (error) {
1244 console.warn('⚠️ Typeface rendering failed, using fallback:', error.message);
1245 }
1246 } else {
1247 console.warn('⚠️ No typeface system available for text rendering');
1248 }
1249 };
1250
1251 // Paste function for compositing embedded buffers back to main canvas
1252 api.paste = function(sourceBuffer, x = 0, y = 0) {
1253 logCall('paste', [sourceBuffer ? `${sourceBuffer.width}x${sourceBuffer.height}` : 'invalid', x, y]);
1254 if (sourceBuffer && sourceBuffer.pixels && api.screen && api.screen.pixels) {
1255 console.log(`🎨 Paste: Compositing ${sourceBuffer.width}x${sourceBuffer.height} buffer at (${x},${y})`);
1256 // For headless, we'd need to implement actual pixel compositing here
1257 // For now, just log that we're doing the paste operation
1258 console.log(`🎨 Paste: First pixel of source:`, sourceBuffer.pixels[0], sourceBuffer.pixels[1], sourceBuffer.pixels[2], sourceBuffer.pixels[3]);
1259 } else {
1260 console.warn('⚠️ Paste called with invalid buffers');
1261 }
1262 };
1263
1264 // Paste function for compositing embedded buffers (critical for KidLisp embed system)
1265 api.paste = function(sourceBuffer, x = 0, y = 0) {
1266 logCall('paste', [`${sourceBuffer?.width}x${sourceBuffer?.height}`, x, y]);
1267
1268 if (!sourceBuffer || !sourceBuffer.pixels || !api.screen || !api.screen.pixels) {
1269 console.warn('⚠️ paste() called with invalid buffers');
1270 return;
1271 }
1272
1273 console.log(`🎨 Pasting ${sourceBuffer.width}x${sourceBuffer.height} buffer to (${x}, ${y})`);
1274
1275 // Simple pixel-level compositing - copy sourceBuffer to api.screen at position (x, y)
1276 const srcWidth = sourceBuffer.width;
1277 const srcHeight = sourceBuffer.height;
1278 const dstWidth = api.screen.width;
1279 const dstHeight = api.screen.height;
1280
1281 for (let sy = 0; sy < srcHeight; sy++) {
1282 for (let sx = 0; sx < srcWidth; sx++) {
1283 const dx = x + sx;
1284 const dy = y + sy;
1285
1286 // Skip pixels outside destination bounds
1287 if (dx < 0 || dy < 0 || dx >= dstWidth || dy >= dstHeight) continue;
1288
1289 const srcIndex = (sy * srcWidth + sx) * 4;
1290 const dstIndex = (dy * dstWidth + dx) * 4;
1291
1292 // Copy RGBA values
1293 api.screen.pixels[dstIndex] = sourceBuffer.pixels[srcIndex]; // R
1294 api.screen.pixels[dstIndex + 1] = sourceBuffer.pixels[srcIndex + 1]; // G
1295 api.screen.pixels[dstIndex + 2] = sourceBuffer.pixels[srcIndex + 2]; // B
1296 api.screen.pixels[dstIndex + 3] = sourceBuffer.pixels[srcIndex + 3]; // A
1297 }
1298 }
1299
1300 console.log(`✅ Paste completed: ${srcWidth}x${srcHeight} → (${x}, ${y})`);
1301 };
1302
1303 // Note: embed function should be provided by KidLisp's global environment
1304 // via getGlobalEnv(), not the API. If $code evaluation isn't working,
1305 // the issue is likely that the global environment isn't properly set up.
1306
1307 // Add text API that references the actual text module functions
1308 // This mirrors how $commonApi.text is structured in disk.mjs but uses the loaded modules
1309 api.text = {
1310 // Use actual text module functions
1311 capitalize: self.text ? self.text.capitalize : (str) => str.charAt(0).toUpperCase() + str.slice(1),
1312 reverse: self.text ? self.text.reverse : (str) => str.split('').reverse().join(''),
1313
1314 // These mirror the disk.mjs $commonApi.text implementation
1315 width: (text) => {
1316 if (Array.isArray(text)) text = text.join(" ");
1317 return text.length * 6; // blockWidth = 6
1318 },
1319
1320 height: (text) => {
1321 return 10; // blockHeight = 10
1322 },
1323
1324 // Use the actual box function logic from disk.mjs but adapted for headless
1325 box: function(text, pos = { x: 0, y: 0 }, bounds, scale = 1, wordWrap = true) {
1326 // Try to call the actual implementation from disk.mjs if available
1327 if (self.disk && self.disk.$commonApi && self.disk.$commonApi.text && self.disk.$commonApi.text.box) {
1328 console.log(`🎯 Using real disk.mjs text.box for "${text}" scale=${scale}`);
1329 return self.disk.$commonApi.text.box(text, pos, bounds, scale, wordWrap);
1330 }
1331
1332 // Fallback to simplified version
1333 console.log(`⚠️ Using fallback text.box for "${text}" scale=${scale}`);
1334 if (!text) {
1335 console.warn("⚠️ No text for `box`.");
1336 return;
1337 }
1338
1339 // Use the same logic as disk.mjs $commonApi.text.box
1340 pos = { ...pos };
1341 let run = 0;
1342
1343 // Use actual scale without adjustment to match text rendering
1344 const adjustedScale = scale; // Use the same scale as text rendering
1345 const tf = self.typeface || { blockWidth: 6, blockHeight: 10 };
1346 const blockWidth = tf.blockWidth * Math.abs(adjustedScale);
1347
1348 const lines = [[]];
1349 let line = 0;
1350
1351 if (bounds === undefined) bounds = (text.length + 2) * blockWidth;
1352
1353 function newLine() {
1354 run = 0;
1355 line += 1;
1356 lines[line] = [];
1357 }
1358
1359 // Simplified word wrapping logic matching disk.mjs
1360 if (wordWrap) {
1361 const words = text.split(" ");
1362 words.forEach((word, wordIndex) => {
1363 const wordLen = word.length * blockWidth;
1364 const spaceWidth = blockWidth;
1365 const spaceNeeded = run > 0 ? spaceWidth : 0;
1366
1367 if (run + spaceNeeded + wordLen >= bounds) newLine();
1368 lines[line].push(word);
1369 run += wordLen + (wordIndex < words.length - 1 ? spaceWidth : 0);
1370 });
1371 } else {
1372 lines[0] = [text];
1373 }
1374
1375 const blockHeight = tf.blockHeight * adjustedScale; // Full line height including spacing
1376
1377 // For single-line text, use just the character height without extra line spacing
1378 let height;
1379 if (lines.length === 1) {
1380 // Single line: use the actual blockHeight (which should be the glyph height)
1381 height = blockHeight;
1382 } else {
1383 // Multiple lines: add a small gap between lines (like +1px per line for spacing)
1384 const lineSpacing = adjustedScale; // 1px scaled up
1385 height = lines.length * blockHeight + (lines.length - 1) * lineSpacing;
1386 }
1387
1388 let maxLineWidth = 0;
1389 lines.forEach(line => {
1390 if (line.length > 0) {
1391 const lineText = line.join(' ');
1392 const lineWidth = lineText.length * blockWidth;
1393 maxLineWidth = Math.max(maxLineWidth, lineWidth);
1394 }
1395 });
1396
1397 const box = { x: pos.x, y: pos.y, width: maxLineWidth, height };
1398 return { pos, box, lines };
1399 }
1400 };
1401
1402 console.log('✅ Text API exposed using actual text module functions: capitalize, reverse, width, height, box');
1403
1404 // Add KidLisp integration by creating a direct instance
1405 try {
1406 // Import KidLisp directly from the kidlisp module
1407 const { KidLisp } = await import("../../../system/public/aesthetic.computer/lib/kidlisp.mjs");
1408
1409 // Store KidLisp class for use in embedded layer restoration
1410 self.KidLisp = KidLisp;
1411
1412 if (!self.kidlispInstance) {
1413 self.kidlispInstance = new KidLisp();
1414
1415 // Override Date.now() for the KidLisp instance to use simulation time
1416 const originalDateNow = Date.now;
1417 self.kidlispInstance.getSimulationTime = () => {
1418 return typeof self.simulationTime === 'number'
1419 ? self.simulationTime
1420 : originalDateNow();
1421 };
1422
1423 // Patch the KidLisp instance to use simulation time
1424 self.kidlispInstance.originalDateNow = originalDateNow;
1425
1426 // Reset timing state since we're switching to simulation time
1427 self.kidlispInstance.lastSecondExecutions = {};
1428 self.kidlispInstance.sequenceCounters = new Map();
1429 console.log('🔄 Reset KidLisp timing state for simulation mode');
1430
1431 // Restore KidLisp state if available - use the enhanced state restoration
1432 if (self.kidlispState) {
1433 console.log('🔄 Restoring comprehensive KidLisp state from previous frame');
1434
1435 // Use the setKidlispState method which handles all state restoration
1436 self.setKidlispState(self.kidlispState);
1437
1438 console.log('✅ Complete KidLisp state restored');
1439 }
1440
1441 // Note: setAPI will be called in the kidlisp function with the current API
1442 }
1443
1444 api.kidlisp = function(x = 0, y = 0, width, height, source, options = {}) {
1445 logCall('kidlisp', [x, y, width, height, source, options]);
1446
1447 console.log('DEBUG: API object keys:', Object.keys(api));
1448 console.log('DEBUG: API screen:', api.screen);
1449 console.log('DEBUG: Self width/height:', self.width, self.height);
1450
1451 // Set default width/height to screen dimensions if not provided
1452 if (width === undefined) width = self.width;
1453 if (height === undefined) height = self.height;
1454
1455 // Update the KidLisp instance with the current API before evaluation
1456 self.kidlispInstance.setAPI(api);
1457
1458 // Now that KidLisp API is set up, restore any deferred embedded layers
1459 self.restoreEmbeddedLayers();
1460
1461 // Simulate KidLisp sim function behavior to properly advance frame count and rainbow cache
1462 // This MUST happen before any KidLisp evaluation to ensure proper color cycling
1463 self.kidlispInstance.frameCount++; // Increment frame counter for timing functions
1464 resetRainbowCache(); // Reset rainbow cache for new frame to ensure color cycling
1465
1466 // Set KidLisp context in graph system for dynamic fade evaluation
1467 if (self.graph && self.graph.setKidLispContext) {
1468 self.graph.setKidLispContext(self.kidlispInstance, api, self.kidlispInstance.localEnv);
1469 }
1470
1471 // Execute the KidLisp code with proper first-line color detection
1472 try {
1473 // Parse the source code first
1474 console.log(`DEBUG: KidLisp source before parsing:`, JSON.stringify(source));
1475 self.kidlispInstance.parse(source);
1476 console.log(`DEBUG: KidLisp AST after parsing:`, self.kidlispInstance.ast);
1477
1478 if (self.kidlispInstance.ast) {
1479 // Detect and apply first-line color if needed (only on frame 0, like "once wipe purple")
1480 self.kidlispInstance.detectFirstLineColor();
1481 if (self.kidlispInstance.firstLineColor && (api.frameIndex === undefined || api.frameIndex === 0)) {
1482 console.log(`🎨 Detected first-line color: ${self.kidlispInstance.firstLineColor} (applying on frame 0 only)`);
1483 api.wipe(self.kidlispInstance.firstLineColor, { firstLineOnce: true });
1484 if (typeof globalThis.storePersistentFirstLineColor === 'function') {
1485 globalThis.storePersistentFirstLineColor(self.kidlispInstance.firstLineColor);
1486 }
1487 } else if (self.kidlispInstance.firstLineColor) {
1488 console.log(`🎨 First-line color ${self.kidlispInstance.firstLineColor} detected but skipping wipe (frame ${api.frameIndex})`);
1489 }
1490
1491 // Evaluate using the main KidLisp evaluation system
1492
1493 // Pass the AST directly - if it's already an array, use it as the body
1494 const astToEvaluate = self.kidlispInstance.ast.body || self.kidlispInstance.ast;
1495
1496 const result = self.kidlispInstance.evaluate(
1497 astToEvaluate,
1498 api,
1499 self.kidlispInstance.localEnv
1500 );
1501
1502 console.log(`🎯 KidLisp evaluation result:`, result);
1503 return result;
1504 }
1505 } catch (error) {
1506 console.error('KidLisp execution error:', error);
1507 return null;
1508 }
1509 };
1510
1511 console.log('✅ KidLisp API exposed using direct KidLisp instance');
1512 } catch (error) {
1513 console.warn('⚠️ Failed to load KidLisp module:', error.message);
1514 }
1515
1516 // Add direct references for destructuring patterns used by some disks
1517 api.api = api; // Self reference for when destructured as { api }
1518
1519 return api;
1520 }
1521
1522 // Fallback basic API
1523 console.log('🔄 Using basic API fallback');
1524 return this.createBasicAPI(logCall);
1525 }
1526
1527 createBasicAPI(logCall) {
1528 return {
1529 screen: { width: this.width, height: this.height },
1530 pen: { x: 0, y: 0 },
1531
1532 wipe: (...args) => {
1533 logCall('wipe', args);
1534 // Simple clear to black
1535 for (let i = 0; i < this.pixelBuffer.length; i += 4) {
1536 this.pixelBuffer[i] = 0;
1537 this.pixelBuffer[i + 1] = 0;
1538 this.pixelBuffer[i + 2] = 0;
1539 this.pixelBuffer[i + 3] = 255;
1540 }
1541 return this;
1542 },
1543
1544 ink: (...args) => {
1545 logCall('ink', args);
1546 return this;
1547 },
1548
1549 write: (...args) => {
1550 logCall('write', args);
1551 return this;
1552 },
1553
1554 // Page function for basic API (needed by KidLisp embed system)
1555 page: (buffer) => {
1556 logCall('page', [buffer ? `${buffer.width}x${buffer.height}` : 'invalid']);
1557 if (buffer && buffer.width && buffer.height && buffer.pixels) {
1558 console.log(`📄 Basic API: Switched to buffer: ${buffer.width}x${buffer.height}`);
1559 // For basic API, we just acknowledge the page switch but don't change much
1560 // The real work happens in the KidLisp embed system
1561 } else {
1562 console.warn('⚠️ Basic API: page() called with invalid buffer:', buffer);
1563 }
1564 return this;
1565 },
1566
1567 // Paste function for compositing embedded buffers
1568 paste: (sourceBuffer, x = 0, y = 0) => {
1569 logCall('paste', [sourceBuffer ? `${sourceBuffer.width}x${sourceBuffer.height}` : 'invalid', x, y]);
1570 if (sourceBuffer && sourceBuffer.pixels) {
1571 console.log(`🎨 Basic API: Paste ${sourceBuffer.width}x${sourceBuffer.height} at (${x},${y})`);
1572 } else {
1573 console.warn('⚠️ Basic API: paste() called with invalid buffer');
1574 }
1575 return this;
1576 },
1577
1578 // Paste function for basic API (needed by KidLisp embed system)
1579 paste: (sourceBuffer, x = 0, y = 0) => {
1580 logCall('paste', [`${sourceBuffer?.width}x${sourceBuffer?.height}`, x, y]);
1581
1582 if (!sourceBuffer || !sourceBuffer.pixels) {
1583 console.warn('⚠️ Basic API: paste() called with invalid sourceBuffer');
1584 return this;
1585 }
1586
1587 console.log(`🎨 Basic API: Pasting ${sourceBuffer.width}x${sourceBuffer.height} buffer to (${x}, ${y})`);
1588
1589 // For the basic API, we can't do much actual compositing since we don't have sophisticated buffer management
1590 // But we log it so we know the paste is being called
1591 console.log(`✅ Basic API: Paste acknowledged (no actual compositing in basic mode)`);
1592 return this;
1593 },
1594
1595 // Add transformation functions for basic API as well
1596 spin: (...args) => {
1597 logCall('spin', args);
1598 console.log(`🔄 Spin: ${args.join(', ')}`);
1599 return this;
1600 },
1601
1602 zoom: (...args) => {
1603 logCall('zoom', args);
1604 console.log(`🔍 Zoom: ${args.join(', ')}`);
1605 return this;
1606 },
1607
1608 contrast: (...args) => {
1609 logCall('contrast', args);
1610 console.log(`🎨 Contrast: ${args.join(', ')}`);
1611 return this;
1612 },
1613
1614 scroll: (...args) => {
1615 logCall('scroll', args);
1616 console.log(`📜 Scroll: ${args.join(', ')}`);
1617 return this;
1618 },
1619
1620 // Add clock utilities for timing-based animations
1621 clock: {
1622 time: () => {
1623 const simulationTime = this.api?.simulationTime || Date.now();
1624 return new Date(simulationTime);
1625 }
1626 },
1627
1628 // Add num utilities
1629 num: {
1630 radians: (degrees) => degrees * (Math.PI / 180),
1631 degrees: (radians) => radians * (180 / Math.PI),
1632 randInt: (max) => Math.floor(Math.random() * max),
1633 randIntRange: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
1634 map: (value, start1, stop1, start2, stop2) => {
1635 return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
1636 },
1637 lerp: (start, stop, amount) => start + (stop - start) * amount,
1638 clamp: (value, min, max) => Math.max(min, Math.min(max, value)),
1639 dist: (x1, y1, x2, y2) => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2),
1640 timestamp: Date.now()
1641 },
1642
1643 // Add basic text API fallback
1644 text: {
1645 box: (text, pos = { x: 0, y: 0 }, bounds, scale = 1, wordWrap = true) => {
1646 // Basic text box calculation
1647 const blockWidth = 6 * Math.abs(scale);
1648 const blockHeight = 10 * Math.abs(scale);
1649
1650 if (!text) {
1651 console.warn("⚠️ No text for basic `text.box`.");
1652 return { pos, box: { x: pos.x, y: pos.y, width: 0, height: 0 }, lines: [] };
1653 }
1654
1655 // Simple line breaking for basic mode
1656 const lines = text.toString().split('\\n');
1657 let maxWidth = 0;
1658
1659 lines.forEach(line => {
1660 maxWidth = Math.max(maxWidth, line.length * blockWidth);
1661 });
1662
1663 const height = lines.length * blockHeight;
1664
1665 return {
1666 pos,
1667 box: { x: pos.x, y: pos.y, width: maxWidth, height },
1668 lines: lines.map(line => [line])
1669 };
1670 },
1671 width: (text) => text ? text.toString().length * 6 : 0,
1672 height: (text) => 10
1673 }
1674 };
1675 }
1676
1677 // Simple text rendering fallback
1678 renderSimpleText(text, x, y) {
1679 const str = text.toString();
1680 const charWidth = 6;
1681 const charHeight = 10;
1682
1683 for (let i = 0; i < str.length; i++) {
1684 const charX = x + (i * charWidth);
1685
1686 // Draw simple 6x10 character outline
1687 for (let dy = 0; dy < charHeight; dy++) {
1688 for (let dx = 0; dx < charWidth; dx++) {
1689 // Simple character shape
1690 const shouldDraw = (dy === 0 || dy === charHeight - 1) ||
1691 (dx === 0) || (dx === charWidth - 3);
1692
1693 if (shouldDraw && this.graph && this.graph.point) {
1694 this.graph.point(charX + dx, y + dy);
1695 }
1696 }
1697 }
1698 }
1699 }
1700
1701 // Pixel-level transformation methods
1702 applySpinTransformation(steps) {
1703 if (!this.pixelBuffer) return;
1704
1705 const buffer = new Uint8Array(this.pixelBuffer);
1706 const centerX = this.width / 2;
1707 const centerY = this.height / 2;
1708 const angle = (steps || 0) * 0.01; // Convert steps to radians
1709
1710 // Simple rotation transformation
1711 for (let y = 0; y < this.height; y++) {
1712 for (let x = 0; x < this.width; x++) {
1713 const dx = x - centerX;
1714 const dy = y - centerY;
1715
1716 // Rotate point
1717 const rotX = Math.cos(angle) * dx - Math.sin(angle) * dy + centerX;
1718 const rotY = Math.sin(angle) * dx + Math.cos(angle) * dy + centerY;
1719
1720 // Check bounds and copy pixel
1721 if (rotX >= 0 && rotX < this.width && rotY >= 0 && rotY < this.height) {
1722 const srcIdx = (Math.floor(rotY) * this.width + Math.floor(rotX)) * 4;
1723 const dstIdx = (y * this.width + x) * 4;
1724
1725 if (srcIdx >= 0 && srcIdx < buffer.length - 3) {
1726 this.pixelBuffer[dstIdx] = buffer[srcIdx];
1727 this.pixelBuffer[dstIdx + 1] = buffer[srcIdx + 1];
1728 this.pixelBuffer[dstIdx + 2] = buffer[srcIdx + 2];
1729 this.pixelBuffer[dstIdx + 3] = buffer[srcIdx + 3];
1730 }
1731 }
1732 }
1733 }
1734 }
1735
1736 applyZoomTransformation(level) {
1737 if (!this.pixelBuffer || !level) return;
1738
1739 const buffer = new Uint8Array(this.pixelBuffer);
1740 const centerX = this.width / 2;
1741 const centerY = this.height / 2;
1742 const zoom = level || 1.0;
1743
1744 // Simple zoom transformation
1745 for (let y = 0; y < this.height; y++) {
1746 for (let x = 0; x < this.width; x++) {
1747 const dx = x - centerX;
1748 const dy = y - centerY;
1749
1750 // Scale point
1751 const srcX = dx / zoom + centerX;
1752 const srcY = dy / zoom + centerY;
1753
1754 // Check bounds and copy pixel
1755 if (srcX >= 0 && srcX < this.width && srcY >= 0 && srcY < this.height) {
1756 const srcIdx = (Math.floor(srcY) * this.width + Math.floor(srcX)) * 4;
1757 const dstIdx = (y * this.width + x) * 4;
1758
1759 if (srcIdx >= 0 && srcIdx < buffer.length - 3) {
1760 this.pixelBuffer[dstIdx] = buffer[srcIdx];
1761 this.pixelBuffer[dstIdx + 1] = buffer[srcIdx + 1];
1762 this.pixelBuffer[dstIdx + 2] = buffer[srcIdx + 2];
1763 this.pixelBuffer[dstIdx + 3] = buffer[srcIdx + 3];
1764 }
1765 }
1766 }
1767 }
1768 }
1769
1770 applyContrastTransformation(level) {
1771 if (!this.pixelBuffer) return;
1772
1773 const contrast = level || 1.0;
1774 const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
1775
1776 for (let i = 0; i < this.pixelBuffer.length; i += 4) {
1777 // Apply contrast to RGB channels
1778 this.pixelBuffer[i] = Math.max(0, Math.min(255, factor * (this.pixelBuffer[i] - 128) + 128));
1779 this.pixelBuffer[i + 1] = Math.max(0, Math.min(255, factor * (this.pixelBuffer[i + 1] - 128) + 128));
1780 this.pixelBuffer[i + 2] = Math.max(0, Math.min(255, factor * (this.pixelBuffer[i + 2] - 128) + 128));
1781 // Keep alpha unchanged
1782 }
1783 }
1784
1785 applyBlurTransformation(radius) {
1786 if (!this.pixelBuffer) return;
1787
1788 const blurRadius = Math.max(1, Math.floor(radius || 1));
1789 const tempBuffer = new Uint8Array(this.pixelBuffer);
1790
1791 // Simple box blur implementation
1792 for (let y = 0; y < this.height; y++) {
1793 for (let x = 0; x < this.width; x++) {
1794 let r = 0, g = 0, b = 0, a = 0;
1795 let count = 0;
1796
1797 // Sample surrounding pixels
1798 for (let dy = -blurRadius; dy <= blurRadius; dy++) {
1799 for (let dx = -blurRadius; dx <= blurRadius; dx++) {
1800 const nx = x + dx;
1801 const ny = y + dy;
1802
1803 if (nx >= 0 && nx < this.width && ny >= 0 && ny < this.height) {
1804 const srcIndex = (ny * this.width + nx) * 4;
1805 r += tempBuffer[srcIndex];
1806 g += tempBuffer[srcIndex + 1];
1807 b += tempBuffer[srcIndex + 2];
1808 a += tempBuffer[srcIndex + 3];
1809 count++;
1810 }
1811 }
1812 }
1813
1814 // Average and write back
1815 const dstIndex = (y * this.width + x) * 4;
1816 this.pixelBuffer[dstIndex] = Math.floor(r / count);
1817 this.pixelBuffer[dstIndex + 1] = Math.floor(g / count);
1818 this.pixelBuffer[dstIndex + 2] = Math.floor(b / count);
1819 this.pixelBuffer[dstIndex + 3] = Math.floor(a / count);
1820 }
1821 }
1822 }
1823
1824 applyScrollTransformation(dx, dy) {
1825 if (!this.pixelBuffer) return;
1826
1827 const buffer = new Uint8Array(this.pixelBuffer);
1828 const scrollX = Math.floor(dx || 0);
1829 const scrollY = Math.floor(dy || 0);
1830
1831 // Clear buffer first
1832 this.pixelBuffer.fill(0);
1833
1834 // Copy pixels with offset
1835 for (let y = 0; y < this.height; y++) {
1836 for (let x = 0; x < this.width; x++) {
1837 const srcX = x - scrollX;
1838 const srcY = y - scrollY;
1839
1840 if (srcX >= 0 && srcX < this.width && srcY >= 0 && srcY < this.height) {
1841 const srcIdx = (srcY * this.width + srcX) * 4;
1842 const dstIdx = (y * this.width + x) * 4;
1843
1844 this.pixelBuffer[dstIdx] = buffer[srcIdx];
1845 this.pixelBuffer[dstIdx + 1] = buffer[srcIdx + 1];
1846 this.pixelBuffer[dstIdx + 2] = buffer[srcIdx + 2];
1847 this.pixelBuffer[dstIdx + 3] = buffer[srcIdx + 3];
1848 }
1849 }
1850 }
1851 }
1852
1853 async loadPiece(piecePath) {
1854 // Convert relative path to absolute file URL for ES module import
1855 const absolutePath = resolve(piecePath);
1856 const fileURL = pathToFileURL(absolutePath).href;
1857
1858 // Load the piece module
1859 const pieceModule = await import(fileURL);
1860 console.log(`📦 Loaded piece with functions: ${Object.keys(pieceModule).join(', ')}`);
1861
1862 // Check for AC disk structure (boot/sim/paint) vs simple api structure
1863 if (pieceModule.boot && pieceModule.paint) {
1864 console.log('🔧 AC disk detected - initializing with boot/sim/paint lifecycle...');
1865
1866 // Initialize the disk with boot function
1867 if (pieceModule.boot) {
1868 const api = await this.createAPI();
1869 pieceModule.boot(api);
1870 }
1871
1872 // Create a wrapper that handles sim + paint
1873 return (api) => {
1874 if (pieceModule.sim) {
1875 pieceModule.sim(api);
1876 }
1877 pieceModule.paint(api);
1878 };
1879 }
1880
1881 // Handle simple api.paint structure
1882 if (!pieceModule.paint && !pieceModule.api?.paint) {
1883 throw new Error('Piece must export a paint function, api.paint, or AC disk structure (boot/paint)');
1884 }
1885
1886 return pieceModule.paint || pieceModule.api?.paint;
1887 }
1888
1889 async loadPieceModule(piecePath) {
1890 // Convert relative path to absolute file URL for ES module import
1891 const absolutePath = resolve(piecePath);
1892 const fileURL = pathToFileURL(absolutePath).href;
1893
1894 // Load and return the raw piece module for separate sim/paint access
1895 const pieceModule = await import(fileURL);
1896 console.log(`📦 Loaded piece with functions: ${Object.keys(pieceModule).join(', ')}`);
1897
1898 return pieceModule;
1899 }
1900
1901 savePNG(basePath) {
1902 try {
1903 if (this.pixelBuffer) {
1904 const ts = timestamp();
1905 const filename = `${basePath}-${ts}.png`;
1906 const pngData = encodePNG(this.width, this.height, this.pixelBuffer);
1907 writeFileSync(filename, pngData);
1908 console.log(`🖼️ PNG saved to: ${filename}`);
1909
1910 return { filename, timestamp: ts };
1911 } else {
1912 throw new Error('No pixel buffer available - AC system not properly initialized');
1913 }
1914 } catch (error) {
1915 console.error(chalk.red('💥 Error saving PNG:'), error);
1916 throw error;
1917 }
1918 }
1919
1920 displayInTerminal() {
1921 console.log(`📺 Displaying ${this.width}x${this.height} pixels in terminal...`);
1922
1923 try {
1924 const sixelData = pixelBufferToSixel(this.pixelBuffer, this.width, this.height, 2);
1925
1926 // Output with a simple timeout mechanism
1927 const timeoutId = setTimeout(() => {
1928 console.log(chalk.yellow('\n⚠️ Sixel output timeout'));
1929 }, 1000);
1930
1931 process.stdout.write(sixelData);
1932 clearTimeout(timeoutId);
1933
1934 } catch (error) {
1935 console.log(chalk.yellow('⚠️ Sixel output error:', error.message));
1936 }
1937 }
1938
1939 getStats() {
1940 const uniqueAPIs = [...new Set(this.apiCalls.map(call => call.name))];
1941 return {
1942 apiCalls: this.apiCalls.length,
1943 uniqueAPIs: uniqueAPIs.length,
1944 apis: uniqueAPIs
1945 };
1946 }
1947
1948 // Support deterministic time injection and random seeding
1949 setSimulationTime(timeMs) {
1950 this.simulationTime = timeMs;
1951
1952 // Create deterministic random based on simulation time
1953 this.deterministicRandom = this.createSeededRandom(timeMs);
1954
1955 // Update API time functions to use simulation time
1956 if (this.api && this.api.time) {
1957 this.api.time = () => new Date(this.simulationTime);
1958 }
1959 if (this.api && this.api.clock) {
1960 // KidLisp expects api.clock.time() to return a Date object
1961 this.api.clock.time = () => new Date(this.simulationTime);
1962 console.log(`🕐 Clock API updated with simulation time: ${this.simulationTime}ms -> ${new Date(this.simulationTime).toISOString()}`);
1963 }
1964 if (this.api && this.api.perf) {
1965 this.api.perf = () => this.simulationTime;
1966 }
1967
1968 // COMPREHENSIVE TIME OVERRIDE: Override global time functions that KidLisp might use
1969 if (this.api) {
1970 // Override Date.now and performance.now through the API
1971 this.api.Date = {
1972 now: () => this.simulationTime,
1973 ...Date
1974 };
1975
1976 // Override performance timing
1977 this.api.performance = {
1978 now: () => this.simulationTime,
1979 ...performance
1980 };
1981
1982 // Add simulation time as explicit API
1983 this.api.simulationTime = () => this.simulationTime;
1984
1985 // Make sure all time APIs return consistent values
1986 this.api.now = () => this.simulationTime;
1987 this.api.timestamp = () => this.simulationTime;
1988 }
1989
1990 // Update random functions to use deterministic random
1991 if (this.api && this.api.randInt) {
1992 this.api.randInt = (max) => Math.floor(this.deterministicRandom() * max);
1993 }
1994 if (this.api && this.api.randIntRange) {
1995 this.api.randIntRange = (min, max) => Math.floor(this.deterministicRandom() * (max - min + 1)) + min;
1996 }
1997 }
1998
1999 // Print performance summary of graph operations
2000 printPerformanceSummary() {
2001 if (this.operationTimings.size === 0) {
2002 console.log('📊 No timing data recorded');
2003 return;
2004 }
2005
2006 console.log('\n📊 Graph Operation Performance Summary:');
2007 console.log('=' .repeat(50));
2008
2009 // Sort by total time spent
2010 const sortedOperations = Array.from(this.operationTimings.entries())
2011 .sort((a, b) => b[1].total - a[1].total);
2012
2013 let totalTime = 0;
2014 for (const [operation, stats] of sortedOperations) {
2015 totalTime += stats.total;
2016 }
2017
2018 for (const [operation, stats] of sortedOperations) {
2019 const avgTime = stats.total / stats.count;
2020 const percentage = ((stats.total / totalTime) * 100).toFixed(1);
2021 const avgStr = avgTime >= 1 ? `${avgTime.toFixed(2)}ms` : `${avgTime.toFixed(3)}ms`;
2022 const totalStr = stats.total >= 1 ? `${stats.total.toFixed(2)}ms` : `${stats.total.toFixed(3)}ms`;
2023 const maxStr = stats.max >= 1 ? `${stats.max.toFixed(2)}ms` : `${stats.max.toFixed(3)}ms`;
2024 const minStr = stats.min === Infinity ? '0.000ms' : (stats.min >= 1 ? `${stats.min.toFixed(2)}ms` : `${stats.min.toFixed(3)}ms`);
2025
2026 console.log(`⚡ ${operation.padEnd(12)} | ${stats.count.toString().padStart(4)} calls | ${avgStr.padStart(8)} avg | ${totalStr.padStart(8)} total (${percentage}%) | max: ${maxStr} | min: ${minStr}`);
2027 }
2028
2029 console.log('=' .repeat(50));
2030 const totalStr = totalTime >= 1 ? `${totalTime.toFixed(2)}ms` : `${totalTime.toFixed(3)}ms`;
2031 console.log(`💫 Total graph time: ${totalStr} across ${Array.from(this.operationTimings.values()).reduce((sum, stats) => sum + stats.count, 0)} operations`);
2032 }
2033
2034 // Simple seeded random number generator (LCG)
2035 createSeededRandom(seed) {
2036 let state = seed % 2147483647;
2037 if (state <= 0) state += 2147483646;
2038
2039 return function() {
2040 state = (state * 16807) % 2147483647;
2041 return (state - 1) / 2147483646;
2042 };
2043 }
2044}