Monorepo for Aesthetic.Computer aesthetic.computer
at main 2044 lines 80 kB view raw
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}