1/** 2 * Interactive ngspice worker â vendored + extended from 3 * `ejkreboot/ngspice-xspice-wasm` (MIT, 2026). The original repo provides 4 * a batch-only client; we vendor the WASM build as-is and add new message 5 * types for mixed-mode (event-driven) coupling between an MCU simulator 6 * and the analog SPICE solver. 7 * 8 * Original batch API (preserved for backward-compat with non-interactive 9 * callers): 10 * 'init', 'run', 'reset' 11 * 12 * New interactive message types (Phase 1a of the mixed-mode simulator 13 * roadmap â see `project/sim-mixedmode/phase-01-mixed-mode-coupling.md` 14 * in the velxio-prod repo): 15 * 'loadNetlist' â send a netlist to ngspice without running yet 16 * (so subsequent commands can drive analysis manually) 17 * 'command' â send a raw ngspice command string and return stdout 18 * captured during its execution (`alter`, `tran`, 19 * `display`, etc.) 20 * 'readVec' â read a vector's current value(s) by name 21 * 22 * Not yet implemented (Phase 1b â needs investigation of single-thread 23 * WASM workarounds for ngspice background mode): 24 * 'bgRun', 'bgHalt', 'bgResume' â true continuation across alter calls. 25 * Current workaround at the client level: chain `tran` invocations 26 * with shrinking dt, each one alter-ing the source from the previous 27 * state. See NgSpiceInteractive.ts for the workaround impl. 28 */ 29 30const MODEL_FILES = [ 31 'analog.cm', 32 'digital.cm', 33 'spice2poly.cm', 34 'table.cm', 35 'tlines.cm', 36 'xtradev.cm', 37 'xtraevt.cm', 38]; 39 40// vecvaluesall struct offsets (used by onData callback during simulation) 41const VECVALUESALL_COUNT_OFFSET = 0; 42const VECVALUESALL_VALUES_OFFSET = 8; 43const VECVALUES_NAME_OFFSET = 0; 44const VECVALUES_REAL_OFFSET = 8; 45const VECVALUES_IS_SCALE_OFFSET = 24; 46 47// vector_info struct offsets (used for post-simulation data extraction) 48const VECTOR_INFO_NAME_OFFSET = 0; 49const VECTOR_INFO_TYPE_OFFSET = 4; 50const VECTOR_INFO_FLAGS_OFFSET = 8; 51const VECTOR_INFO_REALDATA_OFFSET = 12; 52const VECTOR_INFO_IMAGDATA_OFFSET = 16; 53const VECTOR_INFO_LENGTH_OFFSET = 20; 54 55// vecinfoall struct offsets (used by onDataInit callback) 56const VECINFOALL_COUNT_OFFSET = 16; 57const VECINFOALL_VECS_OFFSET = 20; 58const VECINFO_NAME_OFFSET = 4; 59 60// ngspice vector type flag for complex data 61const VF_COMPLEX = 0x400; 62 63// Analysis types that produce scalar results (no sweep) 64const SCALAR_ANALYSIS_TYPES = new Set(['op', 'tf', 'sens']); 65 66let moduleConfig = null; 67let moduleReady = null; 68let filesystemReady = false; 69let api = null; 70let callbackPointers = null; 71let ngspiceInitialized = false; 72let currentRun = null; 73 74function postDebug(event, details = {}) { 75 self.postMessage({ 76 type: 'debug', 77 requestId: currentRun?.requestId, 78 event, 79 details, 80 }); 81} 82 83self.addEventListener('message', async (event) => { 84 const data = event.data || {}; 85 86 try { 87 // ââ Original batch API (preserved) ââââââââââââââââââââââââââââââ 88 if (data.type === 'init') { 89 await ensureSession(data.config || {}); 90 self.postMessage({ type: 'ready', requestId: data.requestId }); 91 return; 92 } 93 94 if (data.type === 'run') { 95 await runSimulation(data.requestId, data.netlist || ''); 96 return; 97 } 98 99 if (data.type === 'reset') { 100 resetNgspice(); 101 self.postMessage({ type: 'reset-done', requestId: data.requestId }); 102 return; 103 } 104 105 // ââ New interactive API (Phase 1a) ââââââââââââââââââââââââââââââ 106 if (data.type === 'loadNetlist') { 107 await handleLoadNetlist(data.requestId, data.netlist || ''); 108 return; 109 } 110 111 if (data.type === 'command') { 112 handleCommand(data.requestId, data.command || ''); 113 return; 114 } 115 116 if (data.type === 'readVec') { 117 handleReadVec(data.requestId, data.name || ''); 118 return; 119 } 120 121 if (data.type === 'listVectors') { 122 handleListVectors(data.requestId); 123 return; 124 } 125 126 throw new Error(`Unknown message type: ${data.type}`); 127 } catch (error) { 128 self.postMessage({ 129 type: 'error', 130 requestId: data.requestId,
131 message: error instanceof Error ? error.message : String(error), 132 }); 133 } 134}); 135 136// ââ Interactive handlers (Phase 1a) âââââââââââââââââââââââââââââââââ 137 138/** 139 * Load a netlist into ngspice without immediately running an analysis. 140 * The netlist may include `.tran`, `.dc`, etc. directives that fire 141 * automatically when ngspice processes them â or it may be purely 142 * structural (components, sources, models) with analysis triggered 143 * later via separate `command('tran ...')` calls. 144 */ 145async function handleLoadNetlist(requestId, netlist) { 146 if (!netlist.trim()) { 147 throw new Error('Netlist is empty.'); 148 } 149 resetNgspice(); 150 await ensureSession(moduleConfig || {}); 151 const lines = buildCircuitLines(netlist); 152 const allocations = allocateCStringArray(lines); 153 try { 154 const rc = api.circ(allocations.arrayPointer); 155 if (rc !== 0) { 156 throw new Error(`ngSpice_Circ failed with status ${rc}.`); 157 } 158 self.postMessage({ type: 'loaded', requestId }); 159 } finally { 160 freeCStringArray(allocations); 161 } 162} 163 164/** 165 * Send a raw command string to ngspice. Stdout / stderr lines captured 166 * during the command's execution are buffered and returned in the 167 * response, plus also emitted as 'stdout' / 'stderr' events (compatible 168 * with the existing onPrint callback flow used by batch mode). 169 */ 170function handleCommand(requestId, command) { 171 if (!command.trim()) { 172 throw new Error('Command is empty.'); 173 } 174 const capture = beginCommandCapture(requestId); 175 try { 176 const rc = api.command(command); 177 self.postMessage({ 178 type: 'command-result', 179 requestId, 180 rc, 181 stdout: capture.stdout.slice(), 182 stderr: capture.stderr.slice(), 183 }); 184 } finally { 185 endCommandCapture(capture); 186 } 187} 188 189/** 190 * Read the current state of a named vector. For DC operating point this 191 * is a single scalar; for `.tran` it's the entire time series of samples 192 * captured so far. 193 */ 194function handleReadVec(requestId, vectorName) { 195 const infoPtr = api.getVecInfo(vectorName); 196 if (!infoPtr) { 197 throw new Error(`Vector '${vectorName}' not found. Run an analysis first.`); 198 } 199 const data = readVectorData(infoPtr, /* readImag */ true); 200 const transferables = [data.real.buffer]; 201 if (data.imag) transferables.push(data.imag.buffer); 202 self.postMessage( 203 { 204 type: 'vec', 205 requestId, 206 name: vectorName, 207 real: data.real, 208 imag: data.imag, 209 complex: data.complex, 210 unit: data.unit, 211 }, 212 transferables, 213 ); 214} 215 216/** 217 * Enumerate every vector in the current plot. Phase 1d #6. 218 * `ngSpice_AllVecs` returns a NULL-terminated char** array; we walk 219 * it until the first 0 pointer and decode each cstring. Names are 220 * case-preserved â getVecInfo lookups care about case. 221 */ 222function handleListVectors(requestId) { 223 const plot = api.curPlot && api.curPlot(); 224 const names = []; 225 if (plot) { 226 const arrPtr = api.allVecs(plot); 227 if (arrPtr) { 228 for (let i = 0; i < 4096; i++) { 229 const ptr = HEAPU32[(arrPtr >> 2) + i]; 230 if (!ptr) break; 231 names.push(Module.UTF8ToString(ptr)); 232 } 233 } 234 } 235 self.postMessage({ type: 'vector-list', requestId, names }); 236} 237 238// ââ Command-capture machinery (Phase 1a) ââââââââââââââââââââââââââââ 239// 240// onPrint pushes to currentRun.log.stdout when a batch run is active. 241// For interactive commands we need our own capture buffer so the 242// caller of `command()` gets the lines that command produced. 243 244let activeCommandCapture = null; 245 246function beginCommandCapture(requestId) { 247 activeCommandCapture = { 248 requestId, 249 stdout: [], 250 stderr: [], 251 }; 252 return activeCommandCapture; 253} 254 255function endCommandCapture(capture) { 256 if (activeCommandCapture === capture) { 257 activeCommandCapture = null; 258 } 259} 260 261// freeCStringArray and allocateCStringArray are defined further down in 262// the file as part of the existing batch-mode infrastructure â reused 263// here. 264 265async function runSimulation(requestId, netlist) { 266 if (!netlist.trim()) { 267 throw new Error('Netlist is empty.'); 268 } 269 270 if (currentRun) { 271 throw new Error('A library simulation is already in progress.'); 272 } 273 274 resetNgspice(); 275 await ensureSession(moduleConfig || {}); 276 277 currentRun = { 278 requestId, 279 finalTime: extractTranFinalTime(netlist), 280 lastProgress: 0, 281 lastCurrentTime: 0, 282 lastEmitAt: 0, 283 timeVectorName: 'time', 284 }; 285 286 postDebug('run-start', { finalTime: currentRun.finalTime, netlistLines: netlist.split(/\r?\n/).length }); 287 288 self.postMessage({ type: 'status', requestId, message: 'Submitting circuit to shared ngspiceâ¦' }); 289 290 const circuitLines = buildCircuitLines(netlist); 291 const allocations = allocateCStringArray(circuitLines); 292 293 try { 294 const rc = api.circ(allocations.arrayPointer); 295 if (rc !== 0) { 296 throw new Error(`ngSpice_Circ failed with status ${rc}.`); 297 } 298 299 emitProgress(true); 300 301 const analyses = extractAllAnalyses(); 302 const transferables = collectTransferables(analyses); 303 304 self.postMessage( 305 { 306 type: 'done', 307 requestId, 308 exitCode: rc, 309 finalTime: currentRun.finalTime, 310 progress: currentRun.lastProgress, 311 analyses, 312 }, 313 transferables, 314 ); 315 } finally { 316 freeCStringArray(allocations); 317 currentRun = null; 318 } 319} 320 321// --------------------------------------------------------------------------- 322// Post-simulation data extraction 323// --------------------------------------------------------------------------- 324 325function extractAllAnalyses() { 326 const plotNames = readAllPlotNames(); 327 const analyses = []; 328 329 for (const plotName of plotNames) { 330 if (plotName === 'const') { 331 continue; 332 } 333
334 const analysis = extractPlotAnalysis(plotName); 335 if (analysis) { 336 analyses.push(analysis); 337 } 338 } 339 340 postDebug('extract-analyses', { plotCount: plotNames.length, analysisCount: analyses.length }); 341 return analyses; 342} 343 344function readAllPlotNames() { 345 const plotsPtr = api.allPlots(); 346 if (!plotsPtr) { 347 return []; 348 } 349 350 const names = []; 351 let offset = plotsPtr; 352 while (true) { 353 const strPtr = HEAPU32[offset >> 2]; 354 if (!strPtr) { 355 break; 356 } 357 names.push(Module.UTF8ToString(strPtr)); 358 offset += 4; 359 } 360 return names; 361} 362 363function readAllVecNames(plotName) { 364 const vecsPtr = api.allVecs(plotName); 365 if (!vecsPtr) { 366 return []; 367 } 368 369 const names = []; 370 let offset = vecsPtr; 371 while (true) { 372 const strPtr = HEAPU32[offset >> 2]; 373 if (!strPtr) { 374 break; 375 } 376 names.push(Module.UTF8ToString(strPtr)); 377 offset += 4; 378 } 379 return names; 380} 381 382function extractPlotAnalysis(plotName) { 383 const vecNames = readAllVecNames(plotName); 384 if (vecNames.length === 0) { 385 return null; 386 } 387 388 const analysisType = detectAnalysisType(plotName); 389 const isComplex = analysisType === 'ac'; 390 const isScalar = SCALAR_ANALYSIS_TYPES.has(analysisType); 391 392 let sweepVec = null; 393 const dataVecs = []; 394 395 for (const vecName of vecNames) { 396 const qualifiedName = `${plotName}.${vecName}`; 397 const infoPtr = api.getVecInfo(qualifiedName); 398 if (!infoPtr) { 399 continue; 400 } 401 402 const vecData = readVectorData(infoPtr, isComplex); 403 if (!vecData) { 404 continue; 405 } 406 407 const typeFlags = HEAP32[(infoPtr + VECTOR_INFO_TYPE_OFFSET) >> 2]; 408 // SV_TIME=1, SV_FREQUENCY=2 are scale (independent) vectors 409 const svType = typeFlags & 0xFF; 410 const isScale = svType === 1 || svType === 2; 411 412 if (isScale) { 413 sweepVec = { name: vecName, ...vecData }; 414 } else { 415 dataVecs.push({ name: vecName, ...vecData }); 416 } 417 } 418 419 if (isScalar) { 420 return buildScalarResult(analysisType, plotName, sweepVec, dataVecs); 421 } 422 423 return buildVectorResult(analysisType, plotName, sweepVec, dataVecs, isComplex); 424} 425 426function readVectorData(infoPtr, readImag) { 427 const length = HEAP32[(infoPtr + VECTOR_INFO_LENGTH_OFFSET) >> 2]; 428 if (length <= 0) { 429 return null; 430 } 431 432 const realDataPtr = HEAPU32[(infoPtr + VECTOR_INFO_REALDATA_OFFSET) >> 2]; 433 const compDataPtr = HEAPU32[(infoPtr + VECTOR_INFO_IMAGDATA_OFFSET) >> 2]; 434 435 // For complex vectors (AC analysis), ngspice stores data as an array of 436 // ngcomplex_t structs ({double real; double imag;}) in the compdata field, 437 // with realdata set to NULL. 438 if (!realDataPtr && compDataPtr && readImag) { 439 const real = new Float64Array(length); 440 const imag = new Float64Array(length); 441 const baseIdx = compDataPtr >> 3; // byte offset to float64 index 442 for (let i = 0; i < length; i++) { 443 real[i] = HEAPF64[baseIdx + i * 2]; 444 imag[i] = HEAPF64[baseIdx + i * 2 + 1]; 445 } 446 return { real, imag, length }; 447 } 448 449 if (!realDataPtr) { 450 return null; 451 } 452 453 const real = new Float64Array(length); 454 real.set(HEAPF64.subarray(realDataPtr >> 3, (realDataPtr >> 3) + length)); 455 456 let imag = null; 457 if (readImag && compDataPtr) { 458 // If both realdata and compdata exist, compdata holds ngcomplex_t structs 459 imag = new Float64Array(length); 460 const baseIdx = compDataPtr >> 3; 461 for (let i = 0; i < length; i++) { 462 imag[i] = HEAPF64[baseIdx + i * 2 + 1]; 463 } 464 } 465 466 return { real, imag, length }; 467} 468 469function buildScalarResult(analysisType, plotName, sweepVec, dataVecs) { 470 const scalars = {}; 471 472 if (sweepVec) { 473 for (let i = 0; i < sweepVec.length; i++) { 474 scalars[sweepVec.name] = sweepVec.real[0]; 475 } 476 } 477 478 for (const vec of dataVecs) { 479 scalars[vec.name] = vec.real[0]; 480 } 481 482 return { 483 type: analysisType, 484 sweep: null, 485 vectors: [], 486 scalars, 487 meta: { plotName }, 488 }; 489} 490 491function buildVectorResult(analysisType, plotName, sweepVec, dataVecs, isComplex) { 492 const sweep = sweepVec 493 ? { 494 name: sweepVec.name, 495 unit: inferUnit(sweepVec.name), 496 values: sweepVec.real, 497 } 498 : null; 499 500 const vectors = dataVecs.map((vec) => ({ 501 name: vec.name, 502 unit: inferUnit(vec.name), 503 real: vec.real, 504 imag: isComplex ? vec.imag : null, 505 complex: isComplex, 506 })); 507 508 return { 509 type: analysisType, 510 sweep, 511 vectors, 512 scalars: null, 513 meta: { plotName }, 514 }; 515} 516 517function detectAnalysisType(plotName) { 518 const match = plotName.match(/^(tran|ac|dc|op|noise|sens|tf|pz)/i); 519 if (match) { 520 return match[1].toLowerCase(); 521 } 522 return 'unknown'; 523} 524 525function inferUnit(vectorName) { 526 const lower = vectorName.toLowerCase(); 527 if (lower === 'time') return 's'; 528 if (lower === 'frequency') return 'Hz'; 529 if (/^v\(/.test(lower) || /^v_/.test(lower)) return 'V'; 530 if (/^i\(/.test(lower) || /^i_/.test(lower)) return 'A'; 531 if (/^p\(/.test(lower)) return 'W'; 532 return ''; 533} 534 535function collectTransferables(analyses) { 536 const buffers = new Set(); 537 538 for (const analysis of analyses) { 539 if (analysis.sweep) { 540 buffers.add(analysis.sweep.values.buffer); 541 } 542 543 for (const vec of analysis.vectors) { 544 buffers.add(vec.real.buffer); 545 if (vec.imag) { 546 buffers.add(vec.imag.buffer); 547 } 548 } 549 } 550 551 return [...buffers]; 552} 553 554// --------------------------------------------------------------------------- 555// Netlist preprocessing 556// --------------------------------------------------------------------------- 557 558function buildCircuitLines(netlist) { 559 const lines = netlist.replace(/\r/g, '').split('\n'); 560 const filtered = []; 561 let inControl = false;
562 563 for (const line of lines) { 564 const trimmed = line.trim().toLowerCase(); 565 566 if (/^\.control\b/.test(trimmed)) { 567 inControl = true; 568 filtered.push(line); 569 continue; 570 } 571 572 if (/^\.endc\b/.test(trimmed)) { 573 inControl = false; 574 filtered.push(line); 575 continue; 576 } 577 578 if (inControl) { 579 if (/^save\b/.test(trimmed) || /^wrdt\b/.test(trimmed)) { 580 continue; 581 } 582 filtered.push(line); 583 continue; 584 } 585 586 if (/^\s*\.save\b/i.test(line) || /^\s*\.wrdt\b/i.test(line)) { 587 continue; 588 } 589 590 filtered.push(line); 591 } 592 593 const hasEnd = filtered.some((line) => /^\s*\.end\s*$/i.test(line)); 594 if (!hasEnd) { 595 filtered.push('.end'); 596 } 597 return filtered; 598} 599 600// --------------------------------------------------------------------------- 601// Session & module setup 602// --------------------------------------------------------------------------- 603 604async function ensureSession(config) { 605 await ensureModule(config); 606 607 if (!filesystemReady) { 608 self.postMessage({ type: 'status', requestId: config.requestId, message: 'Staging library assetsâ¦' }); 609 await stageFilesystem(); 610 filesystemReady = true; 611 } 612 613 if (!callbackPointers) { 614 registerCallbacks(); 615 } 616 617 if (!ngspiceInitialized) { 618 initializeNgspice(); 619 } 620} 621 622async function ensureModule(config) { 623 if (!moduleReady) { 624 moduleConfig = normalizeConfig(config); 625 moduleReady = new Promise((resolve, reject) => { 626 self.Module = { 627 noInitialRun: true, 628 locateFile: (path) => { 629 if (path.endsWith('.wasm')) { 630 return resolveAssetUrl(moduleConfig.assetBaseUrl, moduleConfig.wasmFile); 631 } 632 return path; 633 }, 634 print: (text) => { 635 if (currentRun) { 636 self.postMessage({ type: 'stdout', requestId: currentRun.requestId, line: text }); 637 } 638 }, 639 printErr: (text) => { 640 if (currentRun && !text.includes('keepRuntimeAlive() is set')) { 641 self.postMessage({ type: 'stderr', requestId: currentRun.requestId, line: text }); 642 } 643 }, 644 onRuntimeInitialized: () => { 645 bindApi(); 646 resolve(); 647 }, 648 }; 649 650 try { 651 importScripts(resolveAssetUrl(moduleConfig.assetBaseUrl, moduleConfig.moduleScript)); 652 } catch (error) { 653 reject(error); 654 } 655 }); 656 } 657 658 return moduleReady; 659} 660 661function bindApi() { 662 api = { 663 init: Module.cwrap('ngSpice_Init', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number']), 664 command: Module.cwrap('ngSpice_Command', 'number', ['string']), 665 circ: Module.cwrap('ngSpice_Circ', 'number', ['number']), 666 curPlot: Module.cwrap('ngSpice_CurPlot', 'string', []), 667 allPlots: Module.cwrap('ngSpice_AllPlots', 'number', []), 668 allVecs: Module.cwrap('ngSpice_AllVecs', 'number', ['string']), 669 getVecInfo: Module.cwrap('ngGet_Vec_Info', 'number', ['string']), 670 reset: Module.cwrap('ngSpice_Reset', 'number', []), 671 nospiceinit: Module.cwrap('ngSpice_nospiceinit', 'number', []), 672 setInputPath: Module.cwrap('ngCM_Input_Path', 'number', ['string']), 673 }; 674} 675 676function registerCallbacks() { 677 callbackPointers = { 678 print: Module.addFunction(onPrint, 'iiii'), 679 status: Module.addFunction(onStatus, 'iiii'), 680 exit: Module.addFunction(onControlledExit, 'iiiiii'), 681 data: Module.addFunction(onData, 'iiiii'), 682 dataInit: Module.addFunction(onDataInit, 'iiii'), 683 bg: Module.addFunction(onBackground, 'iiii'), 684 }; 685} 686 687function initializeNgspice() { 688 api.nospiceinit(); 689 const rc = api.init( 690 callbackPointers.print, 691 callbackPointers.status, 692 callbackPointers.exit, 693 callbackPointers.data, 694 callbackPointers.dataInit, 695 callbackPointers.bg, 696 0, 697 ); 698 699 if (rc !== 0) { 700 throw new Error(`ngSpice_Init failed with status ${rc}.`); 701 } 702 703 api.setInputPath('/'); 704 api.command('set xspice_enabled'); 705 api.command('source /spinit'); 706 ngspiceInitialized = true; 707} 708 709function resetNgspice() { 710 if (!ngspiceInitialized || !api) { 711 return; 712 } 713 714 api.reset(); 715 ngspiceInitialized = false; 716} 717 718async function stageFilesystem() { 719 ensurePath('/usr/local/lib/ngspice'); 720 ensurePath('/usr/local/share/ngspice/scripts'); 721 722 for (const [index, name] of MODEL_FILES.entries()) { 723 self.postMessage({ 724 type: 'status', 725 message: `Loading code model ${index + 1}/${MODEL_FILES.length}: ${name}`, 726 }); 727 const data = await fetchBinary(name); 728 FS.writeFile(`/usr/local/lib/ngspice/${name}`, new Uint8Array(data)); 729 } 730 731 const spinitText = await fetchText('spinit'); 732 FS.writeFile('/usr/local/share/ngspice/scripts/spinit', spinitText); 733 FS.writeFile('/spinit', spinitText); 734} 735 736// --------------------------------------------------------------------------- 737// Ngspice callbacks (invoked during simulation) 738// --------------------------------------------------------------------------- 739 740function onPrint(messagePtr) { 741 const text = Module.UTF8ToString(messagePtr); 742 const isStderr = text.startsWith('stderr '); 743 const isStdout = text.startsWith('stdout '); 744 const body = isStderr ? text.slice(7) : isStdout ? text.slice(7) : text; 745 const channel = isStderr ? 'stderr' : 'stdout'; 746 747 // Interactive command capture: route stdout/stderr from a command() 748 // call into the per-command buffer AND emit a live event. 749 if (activeCommandCapture) { 750 if (isStderr) { 751 activeCommandCapture.stderr.push(body); 752 } else { 753 activeCommandCapture.stdout.push(body); 754 } 755 self.postMessage({ type: channel, requestId: activeCommandCapture.requestId, line: body }); 756 return 0; 757 } 758 759 // Batch run path â unchanged from the original worker. 760 if (currentRun) { 761 self.postMessage({ type: channel, requestId: currentRun.requestId, line: body }); 762 } 763 return 0; 764} 765 766function onStatus(messagePtr) { 767 if (currentRun) { 768 const message = Module.UTF8ToString(messagePtr); 769 self.postMessage({ type: 'status', requestId: currentRun.requestId, message }); 770 } 771 return 0; 772} 773 774function onControlledExit(status, immediate, fromQuit) { 775 if (currentRun && status !== 0 && !fromQuit) { 776 self.postMessage({ 777 type: 'stderr', 778 requestId: currentRun.requestId, 779 line: `shared ngspice requested exit ${status} (immediate=${Boolean(immediate)})`, 780 }); 781 } 782 return 0; 783} 784 785function onData(vecvaluesAllPtr, vectorCount) { 786 const currentTime = readCurrentTimeFromData(vecvaluesAllPtr, vectorCount); 787 postDebug('data-callback', { 788 vecvaluesAllPtr, 789 vectorCount, 790 currentTime, 791 timeVectorName: currentRun?.timeVectorName, 792 }); 793 emitProgress(false, currentTime); 794 return 0; 795} 796 797function onDataInit(vecinfoAllPtr) { 798 if (currentRun) { 799 const timeVectorName = findTimeVectorName(vecinfoAllPtr); 800 if (timeVectorName) { 801 currentRun.timeVectorName = timeVectorName; 802 } 803 postDebug('data-init-callback', { 804 vecinfoAllPtr, 805 timeVectorName: currentRun.timeVectorName, 806 }); 807 } 808 809 if (currentRun) { 810 self.postMessage({ type: 'status', requestId: currentRun.requestId, message: 'Transient vectors initialized.' }); 811 } 812 return 0; 813} 814
815function onBackground() { 816 return 0; 817} 818 819// --------------------------------------------------------------------------- 820// Progress tracking helpers 821// --------------------------------------------------------------------------- 822 823function emitProgress(force, currentTimeOverride = null) { 824 if (!currentRun || !currentRun.finalTime || currentRun.finalTime <= 0) { 825 return; 826 } 827 828 const currentTime = currentTimeOverride ?? readLatestVectorValue(currentRun.timeVectorName || 'time'); 829 if (currentTime === null) { 830 return; 831 } 832 833 const now = Date.now(); 834 const progress = Math.min(Math.max(currentTime / currentRun.finalTime, currentRun.lastProgress), 1); 835 if (!force && progress - currentRun.lastProgress < 0.001 && now - currentRun.lastEmitAt < 80) { 836 return; 837 } 838 839 currentRun.lastProgress = progress; 840 currentRun.lastCurrentTime = currentTime; 841 currentRun.lastEmitAt = now; 842 self.postMessage({ 843 type: 'progress', 844 requestId: currentRun.requestId, 845 currentTime, 846 finalTime: currentRun.finalTime, 847 progress, 848 }); 849} 850 851function readLatestVectorValue(vectorName) { 852 let vectorInfoPtr = api.getVecInfo(vectorName); 853 if (!vectorInfoPtr && !vectorName.includes('.')) { 854 const currentPlot = api.curPlot?.(); 855 if (currentPlot) { 856 vectorInfoPtr = api.getVecInfo(`${currentPlot}.${vectorName}`); 857 } 858 } 859 860 if (!vectorInfoPtr) { 861 return null; 862 } 863 864 const realDataPtr = HEAPU32[(vectorInfoPtr + VECTOR_INFO_REALDATA_OFFSET) >> 2]; 865 const length = HEAP32[(vectorInfoPtr + VECTOR_INFO_LENGTH_OFFSET) >> 2]; 866 if (!realDataPtr || length <= 0) { 867 return null; 868 } 869 870 return HEAPF64[(realDataPtr >> 3) + length - 1]; 871} 872 873function readCurrentTimeFromData(vecvaluesAllPtr, vectorCount) { 874 if (!vecvaluesAllPtr) { 875 return null; 876 } 877 878 const count = vectorCount || HEAP32[(vecvaluesAllPtr + VECVALUESALL_COUNT_OFFSET) >> 2]; 879 const valuesPtr = HEAPU32[(vecvaluesAllPtr + VECVALUESALL_VALUES_OFFSET) >> 2]; 880 if (!valuesPtr || count <= 0) { 881 return null; 882 } 883 884 for (let index = 0; index < count; index += 1) { 885 const vecvaluePtr = HEAPU32[(valuesPtr >> 2) + index]; 886 if (!vecvaluePtr) { 887 continue; 888 } 889 890 const namePtr = HEAPU32[(vecvaluePtr + VECVALUES_NAME_OFFSET) >> 2]; 891 const name = namePtr ? Module.UTF8ToString(namePtr) : ''; 892 const isScale = HEAPU8[vecvaluePtr + VECVALUES_IS_SCALE_OFFSET] !== 0; 893 if (!isScale && name !== 'time' && name !== currentRun?.timeVectorName) { 894 continue; 895 } 896 897 return HEAPF64[(vecvaluePtr + VECVALUES_REAL_OFFSET) >> 3]; 898 } 899 900 return null; 901} 902 903function findTimeVectorName(vecinfoAllPtr) { 904 if (!vecinfoAllPtr) { 905 return null; 906 } 907 908 const count = HEAP32[(vecinfoAllPtr + VECINFOALL_COUNT_OFFSET) >> 2]; 909 const vecsPtr = HEAPU32[(vecinfoAllPtr + VECINFOALL_VECS_OFFSET) >> 2]; 910 if (!vecsPtr || count <= 0) { 911 return null; 912 } 913 914 for (let index = 0; index < count; index += 1) { 915 const vecinfoPtr = HEAPU32[(vecsPtr >> 2) + index]; 916 if (!vecinfoPtr) { 917 continue; 918 } 919 920 const namePtr = HEAPU32[(vecinfoPtr + VECINFO_NAME_OFFSET) >> 2]; 921 if (!namePtr) { 922 continue; 923 } 924 925 const name = Module.UTF8ToString(namePtr); 926 if (name === 'time') { 927 return name; 928 } 929 } 930 931 return null; 932} 933 934// --------------------------------------------------------------------------- 935// Netlist parsing helpers 936// --------------------------------------------------------------------------- 937 938function extractTranFinalTime(netlist) { 939 for (const line of netlist.split(/\r?\n/)) { 940 if (!/^\s*\.?tran\b/i.test(line)) { 941 continue; 942 } 943 944 const tokens = line.trim().split(/\s+/); 945 if (tokens.length < 3) { 946 postDebug('tran-parse-skipped', { line, reason: 'too-few-tokens' }); 947 return null; 948 } 949 950 const finalTime = parseScaledNumber(tokens[2]); 951 postDebug('tran-parse-result', { line, tokens, finalTime }); 952 return finalTime; 953 } 954 955 postDebug('tran-parse-missed', { reason: 'no-tran-line-found' }); 956 return null; 957} 958 959function parseScaledNumber(token) { 960 const match = token.trim().match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)([a-zA-Z]+)?$/); 961 if (!match) { 962 return null; 963 } 964 965 const value = Number(match[1]); 966 const suffix = (match[2] || '').toLowerCase(); 967 const multipliers = { 968 t: 1e12, 969 g: 1e9, 970 meg: 1e6, 971 k: 1e3, 972 m: 1e-3, 973 u: 1e-6, 974 n: 1e-9, 975 p: 1e-12, 976 f: 1e-15, 977 }; 978 979 if (!suffix) { 980 return value; 981 } 982 983 return value * (multipliers[suffix] || 1); 984} 985 986// --------------------------------------------------------------------------- 987// Utility: memory allocation, URLs, filesystem 988// --------------------------------------------------------------------------- 989 990function allocateCStringArray(lines) { 991 const pointerSize = 4; 992 const stringPointers = lines.map((line) => allocateCString(line)); 993 const arrayPointer = _malloc((stringPointers.length + 1) * pointerSize); 994
995 stringPointers.forEach((pointer, index) => { 996 HEAPU32[(arrayPointer >> 2) + index] = pointer; 997 }); 998 HEAPU32[(arrayPointer >> 2) + stringPointers.length] = 0; 999 1000 return { arrayPointer, stringPointers }; 1001} 1002 1003function freeCStringArray({ arrayPointer, stringPointers }) { 1004 stringPointers.forEach((pointer) => _free(pointer)); 1005 _free(arrayPointer); 1006} 1007 1008function allocateCString(value) { 1009 const length = Module.lengthBytesUTF8(value) + 1; 1010 const pointer = _malloc(length); 1011 Module.stringToUTF8(value, pointer, length); 1012 return pointer; 1013} 1014 1015function normalizeConfig(config) { 1016 return { 1017 assetBaseUrl: config.assetBaseUrl || './', 1018 moduleScript: config.moduleScript || 'ngspice-lib.js', 1019 wasmFile: config.wasmFile || 'ngspice-lib.wasm', 1020 }; 1021} 1022 1023function resolveAssetUrl(basePath, fileName) { 1024 return new URL(`${trimTrailingSlash(basePath)}/${fileName}`, self.location.href).toString(); 1025} 1026 1027function trimTrailingSlash(value) { 1028 return value.replace(/\/$/, ''); 1029} 1030 1031async function fetchBinary(fileName) { 1032 const response = await fetch(resolveAssetUrl(moduleConfig.assetBaseUrl, fileName)); 1033 if (!response.ok) { 1034 throw new Error(`Failed to fetch ${fileName}: ${response.status} ${response.statusText}`); 1035 } 1036 return response.arrayBuffer(); 1037} 1038 1039async function fetchText(fileName) { 1040 const response = await fetch(resolveAssetUrl(moduleConfig.assetBaseUrl, fileName)); 1041 if (!response.ok) { 1042 throw new Error(`Failed to fetch ${fileName}: ${response.status} ${response.statusText}`); 1043 } 1044 return response.text(); 1045} 1046 1047function ensurePath(path) { 1048 const parts = path.split('/').filter(Boolean); 1049 let current = '/'; 1050 for (const part of parts) { 1051 FS.createPath(current, part, true, true); 1052 current = current === '/' ? `/${part}` : `${current}/${part}`; 1053 } 1054}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.