diff --git a/manifold/.gitignore b/manifold/.gitignore index f1b33ac..6e14e9b 100644 --- a/manifold/.gitignore +++ b/manifold/.gitignore @@ -4,3 +4,6 @@ dist/ # Test artifacts test-results/ playwright-report/ + +# Dependencies +node_modules/ diff --git a/manifold/public/nisps.js b/manifold/public/nisps.js index 74c07fe..9c76fe9 100644 --- a/manifold/public/nisps.js +++ b/manifold/public/nisps.js @@ -6,7 +6,7 @@ var createNispsModule = (() => { function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmImports={a:__abort_js,c:__emscripten_memcpy_js,b:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["g"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["h"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["i"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["j"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["k"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["l"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["m"])(a0,a1,a2,a3,a4);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["n"])(a0);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["o"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["p"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["q"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["r"])(a0,a1);var _nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=(a0,a1,a2,a3)=>(_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["s"])(a0,a1,a2,a3);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["t"])(a0,a1);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["u"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["v"])(a0);var _nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=a0=>(_nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=wasmExports["w"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["x"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["y"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["z"])(a0);var _nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=a0=>(_nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=wasmExports["A"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["B"])(a0,a1);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["C"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["D"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["E"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["F"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=a0=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["G"])(a0);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["H"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["I"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["J"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["K"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["M"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["N"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["O"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["P"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["Q"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmImports={a:__abort_js,c:__emscripten_memcpy_js,b:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["g"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["h"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["i"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["j"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["k"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["l"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["m"])(a0,a1,a2,a3,a4);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["n"])(a0);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["o"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["p"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["q"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["r"])(a0,a1);var _nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=(a0,a1,a2,a3)=>(_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["s"])(a0,a1,a2,a3);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["t"])(a0,a1);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["u"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["v"])(a0);var _nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=a0=>(_nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=wasmExports["w"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["x"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["y"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["z"])(a0);var _nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=a0=>(_nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=wasmExports["A"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["B"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["D"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["E"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["G"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["H"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["I"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=a0=>(_nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=wasmExports["K"])(a0);var _nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=a0=>(_nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=wasmExports["L"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["M"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["N"])(a0,a1);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["O"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["P"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["Q"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["R"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=a0=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["S"])(a0);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["T"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["U"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["V"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["W"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["Y"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["Z"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["$"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["aa"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; return moduleRtn; diff --git a/manifold/public/nisps.wasm b/manifold/public/nisps.wasm index 02da865..d2226c2 100755 Binary files a/manifold/public/nisps.wasm and b/manifold/public/nisps.wasm differ diff --git a/manifold/src/console/ConsoleApp.tsx b/manifold/src/console/ConsoleApp.tsx index ef3b4cb..f1b9fd5 100644 --- a/manifold/src/console/ConsoleApp.tsx +++ b/manifold/src/console/ConsoleApp.tsx @@ -10,8 +10,10 @@ * `useEngineVersion` and re-derive `values` imperatively on render. * - Verdicts wire to the engine: commit → feedback.thumbsUp(); perturb → * feedback.thumbsDown(); reroll → randomise(); each followed by process(). - * - The default feedback mode is "Explore and place" → randomise_mlp (set on - * mount; per docs/redesign/rl-feedback-design.md). + * - The default feedback mode is "Explore and place" → the shared C++ core's + * FeedbackMode::ExploreAndPlace (set on mount; the controller forwards the + * Idle→Exploring→Placing lifecycle to engine.feedback.* — nisps/ml/feedback.hpp, + * per docs/redesign/rl-feedback-design.md). * - AltitudeNav switches `focus` via React state (in|split|out|composite), not * by navigating to separate HTML files. * - `c15` is labelled "Powerful Synth Engine" (in model.ts) — "C15" never shows. @@ -28,6 +30,7 @@ import { CompositeStage } from './CompositeStage'; import { SplitStage } from './SplitStage'; import { OutputStage } from './OutputStage'; import { InputMini } from './InputMini'; +import { ParticleStage } from './ParticleStage'; import { Manifold } from './Manifold'; import { ReadoutStrip } from './ReadoutStrip'; import { VerdictCluster } from './VerdictCluster'; @@ -676,7 +679,14 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp bottom: 0, }} > - {focus === 'composite' ? ( + {outputMode === 'particles' ? ( + setAxes((s) => ({ ...s, [k]: v }))} + /> + ) : focus === 'composite' ? ( )} - {/* corner overlay */} -
- MEMLNaut -
+ {/* corner overlay — hidden in Particle mode (top axis bar owns that row) */} + {outputMode !== 'particles' && ( +
+ MEMLNaut +
+ )} void; + axes: Axes; + setAxis: (k: keyof Axes, v: number) => void; +} + +export function ParticleStage({ pos, onMove, axes, setAxis }: ParticleStageProps) { + const engine = useEngine(); + const canvasRef = useRef(null); + const vizRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const viz = new FlowFieldVisualizer(canvas); + vizRef.current = viz; + + let raf = 0; + const tick = () => { + const outputs = engine?.getOutputs(); + if (outputs) viz.setParams(outputs); + viz.draw(); + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + + const ro = new ResizeObserver(() => viz.resize()); + ro.observe(canvas); + + return () => { + cancelAnimationFrame(raf); + ro.disconnect(); + vizRef.current = null; + }; + }, [engine]); + + return ( +
+ {/* Main view — the flow-field particle system */} + + + {/* Top horizontal macro-axis slider bar (Boldness / Memory / Precision) */} +
+ + MEMLNaut + + setAxis('boldness', v)} + style={{ flex: 1 }} + /> + setAxis('memory', v)} + accent="var(--accent-2)" + style={{ flex: 1 }} + /> + setAxis('precision', v)} + accent="var(--ok, var(--accent))" + style={{ flex: 1 }} + /> +
+ + {/* Bottom-left circular pad — drives the 2D input */} +
+ +
+
+ ); +} diff --git a/manifold/src/console/flow-field.ts b/manifold/src/console/flow-field.ts new file mode 100644 index 0000000..2ad27e2 --- /dev/null +++ b/manifold/src/console/flow-field.ts @@ -0,0 +1,354 @@ +/** + * flow-field.ts — Canvas2D flow-field particle system, a faithful TypeScript + * port of the a-immersive playground visualiser (`js/ui/visualizer.js`). + * + * Driven by the first 20 model outputs (each ∈ [0,1]); `setParams` maps them to + * the visual ranges. The simulation advances on its own clock, so particles keep + * flowing between inferences — only the *field* changes when the MLP outputs do. + */ + +// Simple value noise (no dependencies) — identical permutation scheme to the +// a-immersive port. The table is shuffled once at module load. +const PERM = new Uint8Array(512); +{ + const p = new Uint8Array(256); + for (let i = 0; i < 256; i++) p[i] = i; + for (let i = 255; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [p[i], p[j]] = [p[j], p[i]]; + } + for (let i = 0; i < 512; i++) PERM[i] = p[i & 255]; +} + +function fade(t: number): number { + return t * t * t * (t * (t * 6 - 15) + 10); +} +function lerp(a: number, b: number, t: number): number { + return a + t * (b - a); +} + +function grad(hash: number, x: number, y: number): number { + const h = hash & 3; + const u = h < 2 ? x : y; + const v = h < 2 ? y : x; + return ((h & 1) ? -u : u) + ((h & 2) ? -v : v); +} + +function noise2D(x: number, y: number): number { + const X = Math.floor(x) & 255; + const Y = Math.floor(y) & 255; + const xf = x - Math.floor(x); + const yf = y - Math.floor(y); + const u = fade(xf); + const v = fade(yf); + + const aa = PERM[PERM[X] + Y]; + const ab = PERM[PERM[X] + Y + 1]; + const ba = PERM[PERM[X + 1] + Y]; + const bb = PERM[PERM[X + 1] + Y + 1]; + + return lerp( + lerp(grad(aa, xf, yf), grad(ba, xf - 1, yf), u), + lerp(grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1), u), + v, + ); +} + +const TWO_PI = Math.PI * 2; + +interface Particle { + x: number; + y: number; + id: number; + age: number; + life: number; + vx: number; + vy: number; +} + +interface FlowParams { + angleOffset: number; + scale: number; + speed: number; + hueBase: number; + hueSpread: number; + particleSize: number; + fadeRate: number; + turbulence: number; + attractStrength: number; + attractRadius: number; + dispersionRate: number; + dispersionAmount: number; + particleLifetime: number; + respawnStyle: number; + advectionMode: number; + inertia: number; + drag: number; + repulsorStrength: number; + repulsorCount: number; + repulsorOrbitRate: number; +} + +export class FlowFieldVisualizer { + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + private particles: Particle[] = []; + private numParticles = 400; + private time = 0; + private width = 0; + private height = 0; + + private params: FlowParams = { + angleOffset: 0, // p0: flow direction + scale: 0.005, // p1: pattern size + speed: 2, // p2: particle speed + hueBase: 180, // p3: base colour + hueSpread: 60, // p4: colour variation + particleSize: 3, // p5: dot radius + fadeRate: 0.05, // p6: trail length + turbulence: 1, // p7: chaos + attractStrength: 0.8, // p8: pull toward screen centre + attractRadius: 200, // p9: radius where attraction is strongest + dispersionRate: 2.0, // p10: speed of outward dispersion pulses + dispersionAmount: 1.0, // p11: strength of outward dispersion + particleLifetime: 220, // p12: average frames before respawn + respawnStyle: 0.0, // p13: 0=random, 1=edge, 2=center-burst + advectionMode: 0.0, // p14: flow->orbit->radial blend + inertia: 0.2, // p15: velocity memory + drag: 0.02, // p16: velocity damping + repulsorStrength: 0.0, // p17: repulsor force amount + repulsorCount: 0, // p18: number of active repulsors + repulsorOrbitRate: 0.8, // p19: repulsor orbital speed + }; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('FlowFieldVisualizer: 2D context unavailable'); + this.ctx = ctx; + this.resize(); + this.initParticles(); + } + + resize(): void { + const rect = this.canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + this.canvas.width = Math.max(1, Math.round(rect.width * dpr)); + this.canvas.height = Math.max(1, Math.round(rect.height * dpr)); + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.scale(dpr, dpr); + this.width = rect.width; + this.height = rect.height; + // Clear to background so a resize doesn't leave stale trails. + this.ctx.fillStyle = '#0d0d0d'; + this.ctx.fillRect(0, 0, this.width, this.height); + } + + private initParticles(): void { + this.particles = []; + for (let i = 0; i < this.numParticles; i++) { + this.particles.push(this.makeParticle(i)); + } + this.ctx.fillStyle = '#0d0d0d'; + this.ctx.fillRect(0, 0, this.width, this.height); + } + + private makeParticle(id: number): Particle { + return { + x: Math.random() * this.width, + y: Math.random() * this.height, + id, + age: Math.floor(Math.random() * this.params.particleLifetime), + life: this.computeLifetime(), + vx: 0, + vy: 0, + }; + } + + private computeLifetime(): number { + const variance = 0.65 + Math.random() * 0.7; + return Math.max(10, Math.floor(this.params.particleLifetime * variance)); + } + + private respawnParticle(p: Particle): void { + const mode = Math.min(2, Math.floor(this.params.respawnStyle * 2.999)); + const { width, height } = this; + + if (mode === 1) { + // Edge respawn + const side = Math.floor(Math.random() * 4); + if (side === 0) { + p.x = Math.random() * width; + p.y = 0; + } + if (side === 1) { + p.x = width; + p.y = Math.random() * height; + } + if (side === 2) { + p.x = Math.random() * width; + p.y = height; + } + if (side === 3) { + p.x = 0; + p.y = Math.random() * height; + } + // Give edge spawns an inward impulse. + const towardCenterX = width * 0.5 - p.x; + const towardCenterY = height * 0.5 - p.y; + const inwardDist = Math.hypot(towardCenterX, towardCenterY) + 1e-6; + p.vx = (towardCenterX / inwardDist) * 2.0; + p.vy = (towardCenterY / inwardDist) * 2.0; + } else if (mode === 2) { + // Center-burst respawn + const angle = Math.random() * TWO_PI; + const r = Math.random() * Math.min(width, height) * 0.08; + p.x = width * 0.5 + Math.cos(angle) * r; + p.y = height * 0.5 + Math.sin(angle) * r; + p.vx = Math.cos(angle) * 2.5; + p.vy = Math.sin(angle) * 2.5; + } else { + // Random respawn + p.x = Math.random() * width; + p.y = Math.random() * height; + p.vx = (Math.random() * 2 - 1) * 0.5; + p.vy = (Math.random() * 2 - 1) * 0.5; + } + + p.age = 0; + p.life = this.computeLifetime(); + } + + /** Set parameters from the model output (first 20 values, each ∈ [0,1]). */ + setParams(outputs: ArrayLike | null | undefined): void { + if (!outputs || outputs.length < 20) return; + this.params.angleOffset = outputs[0] * TWO_PI; + this.params.scale = 0.001 + outputs[1] * 0.009; + this.params.speed = 0.5 + outputs[2] * 4.5; + this.params.hueBase = outputs[3] * 360; + this.params.hueSpread = outputs[4] * 120; + this.params.particleSize = 1 + outputs[5] * 5; + this.params.fadeRate = 0.01 + outputs[6] * 0.14; + this.params.turbulence = outputs[7] * 2; + this.params.attractStrength = 0.1 + outputs[8] * 2.9; + this.params.attractRadius = 40 + outputs[9] * 420; + this.params.dispersionRate = 0.2 + outputs[10] * 8; + this.params.dispersionAmount = outputs[11] * 3; + this.params.particleLifetime = 30 + outputs[12] * 470; + this.params.respawnStyle = outputs[13]; + this.params.advectionMode = outputs[14]; + this.params.inertia = outputs[15] * 0.98; + this.params.drag = outputs[16] * 0.35; + this.params.repulsorStrength = outputs[17] * 4.5; + this.params.repulsorCount = Math.floor(outputs[18] * 4.999); + this.params.repulsorOrbitRate = 0.1 + outputs[19] * 2.9; + } + + draw(): void { + const { ctx, width, height, params } = this; + if (width === 0 || height === 0) return; + this.time += 0.003; + + // Fade existing content (creates trails) + ctx.fillStyle = `rgba(13, 13, 13, ${params.fadeRate})`; + ctx.fillRect(0, 0, width, height); + + for (const p of this.particles) { + const cx = width * 0.5; + const cy = height * 0.5; + + // Sample flow field + const nx = p.x * params.scale; + const ny = p.y * params.scale; + const angle = noise2D(nx + this.time, ny) * TWO_PI + params.angleOffset; + const curl = noise2D(nx + 100, ny + 100 + this.time * 0.5) * params.turbulence; + + // Mix between three advection fields for larger visual mode changes. + const flowVx = Math.cos(angle + curl) * params.speed; + const flowVy = Math.sin(angle + curl) * params.speed; + const fromCenterX = p.x - cx; + const fromCenterY = p.y - cy; + const centerDist = Math.hypot(fromCenterX, fromCenterY) + 1e-6; + const radialX = fromCenterX / centerDist; + const radialY = fromCenterY / centerDist; + const orbitX = -radialY; + const orbitY = radialX; + const orbitVx = orbitX * params.speed; + const orbitVy = orbitY * params.speed; + const radialVx = radialX * params.speed; + const radialVy = radialY * params.speed; + + const modeBlend = params.advectionMode * 2; + let targetVx: number; + let targetVy: number; + if (modeBlend < 1) { + targetVx = lerp(flowVx, orbitVx, modeBlend); + targetVy = lerp(flowVy, orbitVy, modeBlend); + } else { + targetVx = lerp(orbitVx, radialVx, modeBlend - 1); + targetVy = lerp(orbitVy, radialVy, modeBlend - 1); + } + p.vx = p.vx * params.inertia + targetVx * (1 - params.inertia); + p.vy = p.vy * params.inertia + targetVy * (1 - params.inertia); + p.vx *= 1 - params.drag; + p.vy *= 1 - params.drag; + let nextX = p.x + p.vx; + let nextY = p.y + p.vy; + + // Central attractor keeps trajectories from sticking to the outer edges. + const dx = cx - nextX; + const dy = cy - nextY; + const dist = Math.hypot(dx, dy) + 1e-6; + const nxCenter = dx / dist; + const nyCenter = dy / dist; + const normalizedDist = Math.min(dist / params.attractRadius, 2); + const falloff = 1 / (1 + normalizedDist * normalizedDist); + nextX += nxCenter * params.attractStrength * falloff; + nextY += nyCenter * params.attractStrength * falloff; + + // Time-varying dispersion pushes particles outward near the centre. + const dispersionPulse = + 0.5 + 0.5 * Math.sin(this.time * params.dispersionRate + p.id * 0.07); + const dispersionForce = params.dispersionAmount * dispersionPulse * falloff; + nextX -= nxCenter * dispersionForce; + nextY -= nyCenter * dispersionForce; + + // Orbiting repulsor points carve dynamic voids and bursts. + const repulsorRadius = Math.min(width, height) * 0.28; + for (let r = 0; r < params.repulsorCount; r++) { + const phase = this.time * params.repulsorOrbitRate + (r / 4) * TWO_PI; + const wobble = 0.6 + 0.15 * r; + const rx = cx + Math.cos(phase * (1.0 + wobble)) * repulsorRadius; + const ry = cy + Math.sin(phase * (1.3 + wobble)) * repulsorRadius; + const repulseDx = nextX - rx; + const repulseDy = nextY - ry; + const distSq = repulseDx * repulseDx + repulseDy * repulseDy + 160; + const distInv = 1 / Math.sqrt(distSq); + const force = params.repulsorStrength * (650 / distSq); + nextX += repulseDx * distInv * force; + nextY += repulseDy * distInv * force; + } + + p.x = nextX; + p.y = nextY; + + // Wrap around edges + if (p.x < 0) p.x += width; + if (p.x > width) p.x -= width; + if (p.y < 0) p.y += height; + if (p.y > height) p.y -= height; + + p.age += 1; + if (p.age >= p.life) this.respawnParticle(p); + + // Colour based on particle id + hue params + const hue = (params.hueBase + (p.id / this.numParticles) * params.hueSpread) % 360; + const lightness = 50 + Math.sin(p.id * 0.1 + this.time) * 15; + + ctx.fillStyle = `hsl(${hue}, 75%, ${lightness}%)`; + ctx.beginPath(); + ctx.arc(p.x, p.y, params.particleSize, 0, TWO_PI); + ctx.fill(); + } + } +} diff --git a/manifold/src/engine/engine-api.ts b/manifold/src/engine/engine-api.ts index 9e5fada..9f25c94 100644 --- a/manifold/src/engine/engine-api.ts +++ b/manifold/src/engine/engine-api.ts @@ -35,6 +35,32 @@ export interface EngineFeedbackApi { exploring(): boolean; /** True while the controller has paused learning. */ learningPaused(): boolean; + + // ---- ExploreAndPlace lifecycle (shared C++ core; mode 'explore_and_place') -- + /** Idle→Exploring: snapshot the real net, randomise a scratchpad. */ + enterExplore(spread?: number): void; + /** Exploring→Idle: restore the real net, discard the scratchpad. */ + exitExplore(): void; + /** Exploring scratchpad op: re-randomise (undoable). */ + reroll(spread?: number): void; + /** Exploring scratchpad op: small bounded perturbation (undoable). */ + nudge(amount?: number): void; + /** Exploring scratchpad op: undo the last reroll/nudge. */ + undo(): void; + /** Exploring→Placing: freeze the scratchpad output at its current input. */ + like(): void; + /** Placing→Idle: restore the real net (caller then stores +1 + trains). */ + commitPlace(): void; + /** Placing→Exploring: back out without storing. */ + cancelPlace(): void; + /** True while Placing (the frozen output is held). */ + placing(): boolean; + /** ExploreState int: 0=Idle 1=Exploring 2=Placing. */ + exploreState(): number; + /** Scratchpad undo-ring depth available to pop. */ + undoDepth(): number; + /** The frozen placed / just-committed output (null if none). */ + placedOutput(): Float32Array | null; } export interface EngineAudioApi { @@ -93,6 +119,18 @@ export class EngineApi { setFocus: (mask) => this.iml.feedbackSetFocus(mask), exploring: () => this.iml.feedbackExploring(), learningPaused: () => this.iml.feedbackLearningPaused(), + enterExplore: (spread = this.spread_) => this.iml.feedbackEnterExplore(spread), + exitExplore: () => this.iml.feedbackExitExplore(), + reroll: (spread = this.spread_) => this.iml.feedbackReroll(spread), + nudge: (amount = 0.05) => this.iml.feedbackNudge(amount), + undo: () => this.iml.feedbackUndo(), + like: () => this.iml.feedbackLike(), + commitPlace: () => this.iml.feedbackCommitPlace(), + cancelPlace: () => this.iml.feedbackCancelPlace(), + placing: () => this.iml.feedbackPlacing(), + exploreState: () => this.iml.feedbackState(), + undoDepth: () => this.iml.feedbackUndoDepth(), + placedOutput: () => this.iml.feedbackPlacedOutput(), }; this.audio = { diff --git a/manifold/src/engine/types.ts b/manifold/src/engine/types.ts index 21c5a37..29e9d83 100644 --- a/manifold/src/engine/types.ts +++ b/manifold/src/engine/types.ts @@ -76,6 +76,24 @@ export interface NispsModule { // Returns 1 if `out` holds a static-bypass vector (skip process()); else 0. _nisps_ml_feedback_static_output(ml: number, out_ptr: number): number; + // ExploreAndPlace lifecycle (mode 3). Idle → Exploring → Placing → Idle. + // The shared C++ core owns the weight snapshot/scratchpad/undo; the CALLER + // owns example-storage + training (read placed_output post-commit, addExample + // at the chosen input, then train). See nisps/ml/feedback.hpp. + _nisps_ml_feedback_enter_explore(ml: number, spread: number): void; + _nisps_ml_feedback_exit_explore(ml: number): void; + _nisps_ml_feedback_reroll(ml: number, spread: number): void; + _nisps_ml_feedback_nudge(ml: number, amount: number): void; + _nisps_ml_feedback_undo(ml: number): void; + _nisps_ml_feedback_like(ml: number): void; // Exploring→Placing (freeze output) + _nisps_ml_feedback_commit_place(ml: number): void; // Placing→Idle (restore real net) + _nisps_ml_feedback_cancel_place(ml: number): void; // Placing→Exploring + _nisps_ml_feedback_placing(ml: number): number; // 1 = Placing + _nisps_ml_feedback_state(ml: number): number; // 0=Idle 1=Exploring 2=Placing + _nisps_ml_feedback_undo_depth(ml: number): number; + // Writes placed/committed output (outputSize floats) into out; returns 1 if written. + _nisps_ml_feedback_placed_output(ml: number, out_ptr: number): number; + // Engines. _nisps_engine_create(id_ptr: number, sample_rate: number): number; _nisps_engine_destroy(engine: number): void; @@ -125,18 +143,29 @@ export type EngineId = | 'analysis'; /** Feedback "Down Action" mode. Mirrors `nisps::ml::FeedbackMode`. */ -export type FeedbackMode = 'avoid' | 'randomise_outputs' | 'randomise_mlp'; +export type FeedbackMode = 'avoid' | 'randomise_outputs' | 'randomise_mlp' | 'explore_and_place'; export const FEEDBACK_MODE_TO_INT: Record = { avoid: 0, randomise_outputs: 1, randomise_mlp: 2, + explore_and_place: 3, }; export const FEEDBACK_MODE_FROM_INT: ReadonlyArray = [ 'avoid', 'randomise_outputs', 'randomise_mlp', + 'explore_and_place', +]; + +/** ExploreAndPlace lifecycle state. Mirrors `nisps::ml::ExploreState`. */ +export type ExploreState = 'idle' | 'exploring' | 'placing'; + +export const EXPLORE_STATE_FROM_INT: ReadonlyArray = [ + 'idle', + 'exploring', + 'placing', ]; /** Message protocol between main thread and `wasm-worker.ts`. */ diff --git a/manifold/src/engine/wasm-iml.ts b/manifold/src/engine/wasm-iml.ts index 2941655..612add7 100644 --- a/manifold/src/engine/wasm-iml.ts +++ b/manifold/src/engine/wasm-iml.ts @@ -545,6 +545,83 @@ export class WasmIML { return false; } + // ---- ExploreAndPlace lifecycle (shared C++ core; mode 'explore_and_place') -- + // The C++ core owns the weight snapshot / scratchpad / undo ring; THIS class + // only forwards calls + republishes weights. Example-storage + training stay + // with the caller (FeedbackController.ts), preserving the "caller owns + // training" contract. + + /** Idle→Exploring: snapshot the real net, randomise a scratchpad. */ + feedbackEnterExplore(spread: number): void { + this.module._nisps_ml_feedback_enter_explore(this.mlHandle, spread); + this.publishWeights_(); + } + + /** Exploring→Idle: restore the real net, discard the scratchpad. */ + feedbackExitExplore(): void { + this.module._nisps_ml_feedback_exit_explore(this.mlHandle); + this.publishWeights_(); + } + + /** Exploring scratchpad op: re-randomise (undoable). */ + feedbackReroll(spread: number): void { + this.module._nisps_ml_feedback_reroll(this.mlHandle, spread); + this.publishWeights_(); + } + + /** Exploring scratchpad op: small bounded perturbation (undoable). */ + feedbackNudge(amount: number): void { + this.module._nisps_ml_feedback_nudge(this.mlHandle, amount); + this.publishWeights_(); + } + + /** Exploring scratchpad op: undo the last reroll/nudge. */ + feedbackUndo(): void { + this.module._nisps_ml_feedback_undo(this.mlHandle); + this.publishWeights_(); + } + + /** Exploring→Placing: freeze the scratchpad output at its current input. */ + feedbackLike(): void { + this.module._nisps_ml_feedback_like(this.mlHandle); + } + + /** Placing→Idle: restore the real net. Caller then stores +1 + trains. */ + feedbackCommitPlace(): void { + this.module._nisps_ml_feedback_commit_place(this.mlHandle); + this.publishWeights_(); + } + + /** Placing→Exploring: back out without storing. */ + feedbackCancelPlace(): void { + this.module._nisps_ml_feedback_cancel_place(this.mlHandle); + } + + feedbackPlacing(): boolean { + return this.module._nisps_ml_feedback_placing(this.mlHandle) === 1; + } + + /** ExploreState: 0=Idle 1=Exploring 2=Placing. */ + feedbackState(): number { + return this.module._nisps_ml_feedback_state(this.mlHandle); + } + + feedbackUndoDepth(): number { + return this.module._nisps_ml_feedback_undo_depth(this.mlHandle); + } + + /** + * The frozen placed output (while Placing) or the just-committed output + * (after commit_place, until the next explore). Returns null if neither is + * available. The caller adds this as the +1 example label at the chosen + * input after commit. + */ + feedbackPlacedOutput(): Float32Array | null { + const ok = this.module._nisps_ml_feedback_placed_output(this.mlHandle, this.feedbackBuf.ptr); + if (ok !== 1) return null; + return new Float32Array(this.feedbackBuf.view.subarray(0, this.arch_.outputSize)); + } + // ------------------------------------------------------------------- // Weights I/O // ------------------------------------------------------------------- diff --git a/manifold/src/feedback/controller.ts b/manifold/src/feedback/controller.ts index a80053e..6dd6a33 100644 --- a/manifold/src/feedback/controller.ts +++ b/manifold/src/feedback/controller.ts @@ -26,6 +26,7 @@ */ import { SeededRng } from './rng'; +import type { FeedbackMode } from '../engine/types'; /** The two product feedback modes (rl-feedback-design §0). */ export type ProtoFeedbackMode = 'explore-and-place' | 'geometric-dislike'; @@ -66,6 +67,23 @@ export interface ControllerEngine { thumbsUp(): number; thumbsDown(speed?: number, spread?: number, pinMask?: Uint8Array): number; setFocus(mask: Uint8Array | null): void; + // ExploreAndPlace lifecycle — the SHARED C++ core (mode 'explore_and_place'). + // The controller drives these instead of its own getWeights/setWeights/ + // randomise scratchpad logic, so explore-and-place runs identically in the + // browser and on firmware. See nisps/ml/feedback.hpp. + setMode(mode: FeedbackMode): void; + enterExplore(spread?: number): void; + exitExplore(): void; + reroll(spread?: number): void; + nudge(amount?: number): void; + undo(): void; + like(): void; + commitPlace(): void; + cancelPlace(): void; + placing(): boolean; + exploreState(): number; // 0=Idle 1=Exploring 2=Placing + undoDepth(): number; + placedOutput(): Float32Array | null; }; } @@ -109,22 +127,18 @@ export class FeedbackController { private mode: ProtoFeedbackMode = 'explore-and-place'; private soloMode: ProtoSoloMode = 'mask-gradients'; - // ---- Mode-2 scratchpad session state ------------------------------- - /** The set-aside REAL trained net, restored on finalise/cancel. */ - private snapshot: Float32Array | null = null; + // ---- Mode-2 explore-and-place session state ------------------------ + // The SHARED C++ core (nisps/ml/feedback.hpp, mode 'explore_and_place') now + // owns the set-aside real net, the scratchpad, and the bounded undo ring. This + // controller is a thin driver: it forwards transitions to `engine.feedback.*` + // and tracks only the per-session ANCHOR LIST (multi-anchor warm-start is a + // caller-side feature — the core does one place-commit, the caller accumulates + // anchors and trains them all on finalise). private exploringFlag = false; - /** Undo stack of scratchpad weight snapshots (reroll + nudge are undoable). */ - private undoStack: Float32Array[] = []; /** Anchors placed this session (positives only — NEVER a dislike). */ private anchors: Anchor[] = []; /** True between place() and the manifold location pick. */ private pickingFlag = false; - /** - * The scratchpad output vector frozen at place() time, so the heard sound is - * held while the user aims at a location (rl-feedback-design §2.2 step 3, - * "place_begin freezes the current scratchpad output"). Copied/owned. - */ - private placedOutput: Float32Array | null = null; // ---- Solo / arm ---------------------------------------------------- /** Current arm mask (1=armed/soloed). null ⇒ none armed ⇒ train all. */ @@ -160,10 +174,13 @@ export class FeedbackController { setMode(mode: ProtoFeedbackMode): void { if (mode === this.mode) return; - // Switching mode aborts any active scratchpad session (mirrors the C++ - // `set_mode` which aborts active exploration first — findings §2). + // Switching mode aborts any active scratchpad session. For explore-and-place + // the SHARED C++ core owns the scratchpad, so delegate the teardown to it. if (this.exploringFlag) this.cancel(); this.mode = mode; + // Keep the C++ core's feedback mode in lockstep so the shared explore-and- + // place lifecycle is active when this mode is selected. + this.engine.feedback.setMode(mode === 'explore-and-place' ? 'explore_and_place' : 'avoid'); } getMode(): ProtoFeedbackMode { @@ -194,99 +211,58 @@ export class FeedbackController { // Mode 2 — "Explore & place" (DEFAULT, positive-only, NEVER a dislike) // =================================================================== + // The whole lifecycle below now delegates to the SHARED C++ core + // (engine.feedback.*) — there is NO TS scratchpad/snapshot/undo logic any + // more. The core owns the set-aside real net, the random scratchpad, and the + // bounded undo ring; this controller forwards the transitions and tracks only + // the per-session anchor list for the multi-anchor warm-start (caller-owned + // training). Behaviour matches nisps/ml/feedback.hpp + its ctest + the parity + // gate (native ≡ WASM at 1e-5). + /** - * ENTER explore (rl-feedback-design §2.2 step 1): snapshot the REAL weights, - * set them aside, then randomise() into a scratchpad net. Mark exploring. - * Idempotent re-entry while already exploring = a re-roll (step 2). + * ENTER explore: the core snapshots the REAL net and randomises a scratchpad. + * Re-entry while exploring = a re-roll ("meh, randomise…"). */ enterExplore(): void { if (this.exploringFlag) { - // Re-press while exploring re-rolls ("meh, randomise…" — §2.2 step 2). this.reroll(); return; } - // Snapshot the real trained net (byte round-trip via get/set weights). This - // is the SET-ASIDE net restored on finalise/cancel — it is NOT part of the - // scratchpad undo ring (undo stays inside the scratchpad; you leave the - // session via cancel/finalise, never by undoing back into the real net). - this.snapshot = this.engine.getWeights(); - this.undoStack = []; this.anchors = []; - this.placedOutput = null; this.pickingFlag = false; this.exploringFlag = true; - // Randomise into the first scratchpad candidate, then record it as the undo - // baseline (the history holds the LIVE candidate AFTER each op). - this.engine.randomise(this.spread); - this.recordCandidate(); + this.engine.feedback.enterExplore(this.spread); // core: snapshot + draw scratchpad + this.engine.process(); } - /** - * SCRATCHPAD OP: re-roll the whole net (§2.2 step 2). Undoable. The scratchpad - * is NEVER trained — this only generates a fresh candidate sound to audition. - */ + /** SCRATCHPAD OP: re-roll the scratchpad (core, undoable). Never trained. */ reroll(): void { if (!this.exploringFlag) return; - this.engine.randomise(this.spread); - this.recordCandidate(); + this.engine.feedback.reroll(this.spread); + this.engine.process(); } - /** - * SCRATCHPAD OP: nudge — a small bounded gaussian weight perturbation (§2.2 - * step 2). Undoable. Deterministic via the seeded RNG (NO Math.random). - * - * --- C++ GAP ----------------------------------------------------------- - * The firmware does this with `move_weights(speed, spread)` on its own - * `nisps::Rng`. Here we read the weights, add a small seeded gaussian, and - * write them back — the TS-achievable equivalent. Becomes - * `nisps_ml_feedback_nudge` driving the engine's Rng (rl-feedback-design §4). - * ---------------------------------------------------------------------- - */ + /** SCRATCHPAD OP: nudge — small bounded perturbation (core Rng, undoable). */ nudge(): void { if (!this.exploringFlag) return; - const w = this.engine.getWeights(); - // Bounded gaussian perturbation. No per-call allocation beyond the weights - // buffer the engine already returns (we mutate it in place then write back). - for (let i = 0; i < w.length; i++) { - w[i] += this.rng.nextGaussian(this.nudgeStddev); - } - this.engine.setWeights(w); + this.engine.feedback.nudge(this.nudgeStddev); this.engine.process(); - this.recordCandidate(); } - /** - * UNDO the last scratchpad op (reroll or nudge). Both are undoable (§2.2). The - * undo ring holds the live scratchpad candidate after each op; undo discards - * the current candidate and restores the previous one. The baseline (first - * candidate after enter) is kept so undo never leaves the scratchpad. - */ + /** UNDO the last scratchpad op (core bounded undo ring). */ undo(): void { if (!this.exploringFlag) return; - if (this.undoStack.length <= 1) return; // already at the baseline candidate - this.undoStack.pop(); // discard current candidate - const prev = this.undoStack[this.undoStack.length - 1]; - this.engine.setWeights(prev); + this.engine.feedback.undo(); this.engine.process(); } - /** Record the CURRENT live scratchpad weights as a new undo-ring entry. */ - private recordCandidate(): void { - this.undoStack.push(this.engine.getWeights()); - // Bound the ring to maxUndo+1 (the +1 is the kept baseline at index 0). - if (this.undoStack.length > this.maxUndo + 1) { - this.undoStack.splice(1, 1); - } - } - /** - * PLACE begin (§2.2 step 3): the user likes the current candidate. Freeze the - * scratchpad output so the heard sound is held while they aim, and enter the - * PICK-LOCATION state — the next manifold pointer-down chooses the location. + * PLACE begin: the user likes the current candidate. The core freezes the + * scratchpad output (held while aiming) and we enter the PICK-LOCATION state. */ place(): void { if (!this.exploringFlag) return; - this.placedOutput = new Float32Array(this.engine.getOutputs()); + this.engine.feedback.like(); // core: Exploring→Placing, freeze output this.pickingFlag = true; } @@ -295,64 +271,58 @@ export class FeedbackController { return this.pickingFlag; } - /** The frozen scratchpad output held during aiming (read-only; may be null). */ + /** The frozen scratchpad output held during aiming (from the core; may be null). */ getPlacedOutput(): Float32Array | null { - return this.placedOutput; + return this.engine.feedback.placedOutput(); } /** - * PLACE commit (§2.2 step 3): the user picked a location on the manifold. We - * move the scratchpad input there, run inference, capture the output the - * scratchpad produces AT THAT LOCATION, and store it as a positive anchor. - * - * Per the spec the captured output is "the output the scratchpad produces at - * the chosen location" (getOutputs() after setting the input there) — NOT the - * frozen audition vector. The frozen vector only kept the *audio* steady while - * aiming. Returns the new anchor count. + * PLACE commit: the user picked a location. We capture the output the + * scratchpad produces AT THAT LOCATION (the scratchpad net is still live while + * Placing), store it as a positive anchor, then commit the place — the core + * restores the real net. We immediately re-enter explore so the felt loop + * "place → randomise → place again" keeps going; finalise trains all anchors. */ placeCommit(x: number, y: number): number { if (!this.exploringFlag || !this.pickingFlag) return this.anchors.length; + // The scratchpad net is still live during Placing — read its output at the + // chosen location (per the spec, "the output the scratchpad produces at the + // chosen location", not the frozen audition vector). this.engine.setInput(x, y); this.engine.process(); const out = new Float32Array(this.engine.getOutputs()); - // Solo/arm respected at the EXAMPLE level: capture the arm mask so warm-start - // only asserts armed outputs ("don't-care on others" — §3.3 approximation). const mask = this.armMask ? new Uint8Array(this.armMask) : null; this.anchors.push({ input: [x, y], output: out, mask }); this.pickingFlag = false; - this.placedOutput = null; + // Commit the place in the core (restores the real net), then re-enter + // explore for the next sound in the same session. + this.engine.feedback.commitPlace(); + this.engine.feedback.enterExplore(this.spread); + this.engine.process(); return this.anchors.length; } - /** Cancel a pending place() without storing an anchor (back to auditioning). */ + /** Cancel a pending place() without storing (core: Placing→Exploring). */ cancelPlace(): void { + if (this.pickingFlag) this.engine.feedback.cancelPlace(); this.pickingFlag = false; - this.placedOutput = null; } /** - * RESOLVE / warm-start (§2.2 step 4): restore the set-aside REAL net, then - * warm-start it to interpolate ALL placed anchors by re-adding each as an - * example and training. ADDITIVE — anchors are added to the existing dataset - * (the user's prior thumbs-up likes are NOT clobbered). Exits exploring. + * RESOLVE / warm-start: exit explore (the core restores the set-aside REAL + * net), then warm-start it to interpolate ALL placed anchors by re-adding each + * as an example and training. ADDITIVE — prior likes are not clobbered. * - * --- C++ GAP ----------------------------------------------------------- - * The firmware warm-start trains anchors only on soloed dims via a gradient - * column-freeze (`train_masked`). Here we approximate that at the example - * level: when an anchor carries an arm mask we still add the FULL output - * vector (the engine's addExample takes a full label row), but we forward the - * mask to the engine's setFocus so move_weights/training freezes unarmed - * final-layer columns. True per-example gradient masking (`train_masked` - * consuming `Anchor.mask`) is the C++ step (rl-feedback-design §3.3). - * ---------------------------------------------------------------------- + * NOTE: per-anchor solo masking is still approximated at the example level + * (the engine's addExample takes a full label row); we forward the arm mask to + * the core's setFocus so training honours soloed columns. True per-example + * gradient masking is the future C++ `train_masked` step (rl-feedback §3.3). */ finalise(): number { if (!this.exploringFlag) return 0; - if (this.snapshot) { - this.engine.setWeights(this.snapshot); // restore the real net (warm start) - } + if (this.pickingFlag) this.engine.feedback.cancelPlace(); + this.engine.feedback.exitExplore(); // core: restore the real net (warm start) const placed = this.anchors.length; - // Re-assert the arm focus so training honours any soloed columns. this.engine.feedback.setFocus(this.armMask); for (const a of this.anchors) { this.engine.addExample([a.input[0], a.input[1]], Array.from(a.output)); @@ -366,24 +336,20 @@ export class FeedbackController { } /** - * CANCEL / undo whole session (§2.2 step 5): discard scratchpad + anchors, - * restore the set-aside real net. No anchor stored. + * CANCEL the whole session: the core restores the set-aside real net; we + * discard the anchors. No example stored. */ cancel(): void { if (!this.exploringFlag) return; - if (this.snapshot) { - this.engine.setWeights(this.snapshot); - this.engine.process(); - } + if (this.pickingFlag) this.engine.feedback.cancelPlace(); + this.engine.feedback.exitExplore(); // core: restore the real net + this.engine.process(); this.endSession(); } private endSession(): void { this.exploringFlag = false; this.pickingFlag = false; - this.placedOutput = null; - this.snapshot = null; - this.undoStack = []; this.anchors = []; } @@ -509,8 +475,8 @@ export class FeedbackController { exploring: this.exploringFlag, picking: this.pickingFlag, anchorCount: this.anchors.length, - // -1 for the entry-state baseline kept at index 0. - undoDepth: Math.max(0, this.undoStack.length - 1), + // Scratchpad undo depth now comes from the shared C++ core's undo ring. + undoDepth: this.exploringFlag ? this.engine.feedback.undoDepth() : 0, armedCount: armed, }; }