@bitefoundry/retrieviewer 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/ASSET-PROVENANCE.md +11 -0
  2. package/ASSETS-LICENSE.md +6 -0
  3. package/ATTRIBUTION.md +8 -0
  4. package/DESKTOP-PLATFORMS.md +1 -1
  5. package/EMBEDDING.md +15 -9
  6. package/INSTALLING.md +2 -1
  7. package/LICENSE +16 -16
  8. package/NATIVE-RUNTIME.md +89 -0
  9. package/PREREQUISITES.md +7 -0
  10. package/PUPPY-MEMORY.md +70 -0
  11. package/README.ko.md +13 -1
  12. package/README.md +14 -1
  13. package/RELEASING.md +36 -1
  14. package/THIRD-PARTY-NOTICES.md +8 -0
  15. package/WINDOWS-COMPANION.md +15 -6
  16. package/braille.mjs +2 -2
  17. package/integrations/native-windows/CMakeLists.txt +15 -0
  18. package/integrations/native-windows/THIRD-PARTY-LICENSES.txt +77 -0
  19. package/integrations/native-windows/application.manifest +8 -0
  20. package/integrations/native-windows/bin/win32-x64/retrieviewer-native.exe +0 -0
  21. package/integrations/native-windows/desktop.mjs +45 -0
  22. package/integrations/native-windows/host.cpp +84 -0
  23. package/integrations/native-windows/manifest.json +14 -0
  24. package/integrations/native-windows/native-control.hpp +41 -0
  25. package/integrations/native-windows/native-window.hpp +128 -0
  26. package/integrations/native-windows/runtime.bundle.mjs +4694 -0
  27. package/integrations/native-windows/runtime.mjs +55 -0
  28. package/integrations/native-windows/sources.json +23 -0
  29. package/integrations/purrviewer/ART.md +289 -0
  30. package/integrations/purrviewer/ASSETS-LICENSE.md +21 -0
  31. package/integrations/purrviewer/LICENSE +24 -0
  32. package/integrations/purrviewer/art/ginger.bin.gz +0 -0
  33. package/integrations/purrviewer/art/ginger.json +1707 -0
  34. package/integrations/purrviewer/art/ginger.rvz +0 -0
  35. package/integrations/purrviewer/src/contact.mjs +63 -0
  36. package/integrations/purrviewer/src/habitat.mjs +41 -0
  37. package/integrations/purrviewer/src/life.mjs +280 -0
  38. package/integrations/purrviewer/src/purrviewer-core.mjs +130 -0
  39. package/integrations/purrviewer/src/purrviewer.mjs +37 -0
  40. package/integrations/purrviewer/src/walk-cycle.mjs +3 -0
  41. package/integrations/purrviewer/src/world.mjs +216 -0
  42. package/integrations/purrviewer/vendor/retrieviewer/LICENSE-CODE +21 -0
  43. package/integrations/purrviewer/vendor/retrieviewer/braille.mjs +73 -0
  44. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-file.mjs +15 -0
  45. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-indexed.mjs +78 -0
  46. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-predictive.mjs +170 -0
  47. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-store.mjs +114 -0
  48. package/integrations/windows-overlay/desktop.mjs +11 -3
  49. package/integrations/windows-overlay/worker.mjs +9 -2
  50. package/package.json +53 -13
  51. package/pet-indexed-manifest.json +106 -0
  52. package/puppy-clean.mjs +2 -2
  53. package/puppy-companion-comfort-q5.rvz +0 -0
  54. package/puppy-companion-interactions-v3-q5.rvz +0 -0
  55. package/puppy-companion-sprites.mjs +2 -2
  56. package/puppy-frame-file.mjs +15 -0
  57. package/puppy-frame-indexed.mjs +78 -0
  58. package/puppy-frame-predictive.mjs +1 -1
  59. package/puppy-frame-store.mjs +6 -0
  60. package/puppy-paper-actions-v9-q5.rvz +0 -0
  61. package/puppy-paper-actions.mjs +2 -2
  62. package/puppy-paper-approach-q5.rvz +0 -0
  63. package/puppy-paper-arrival-seat-q5.rvz +0 -0
  64. package/puppy-paper-behaviors-q5.rvz +0 -0
  65. package/puppy-paper-behaviors.mjs +1 -1
  66. package/puppy-paper-contact.mjs +2 -2
  67. package/puppy-paper-rear-gallop-q5.rvz +0 -0
  68. package/puppy-paper-seat-q5.rvz +0 -0
  69. package/puppy-paper-seat.mjs +2 -2
  70. package/puppy-paper.mjs +12 -12
  71. package/puppy-stride-tweens-q5.rvz +0 -0
  72. package/retrieviewer-cli.mjs +10 -6
  73. package/retrieviewer.d.mts +1 -1
  74. package/retrieviewer.mjs +35 -14
  75. package/puppy-companion-comfort-q5.bin.gz +0 -0
  76. package/puppy-companion-interactions-v3-q5.bin.gz +0 -0
  77. package/puppy-paper-actions-v9-q5.bin.gz +0 -0
  78. package/puppy-paper-approach-q5.bin.gz +0 -0
  79. package/puppy-paper-arrival-seat-q5.bin.gz +0 -0
  80. package/puppy-paper-behaviors-q5.bin.gz +0 -0
  81. package/puppy-paper-rear-gallop-q5.bin.gz +0 -0
  82. package/puppy-paper-seat-q5.bin.gz +0 -0
  83. package/puppy-stride-tweens-q5.bin.gz +0 -0
package/puppy-paper.mjs CHANGED
@@ -354,9 +354,9 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
354
354
  };
355
355
  }
356
356
 
357
- export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,seat,arrivalSeat,behaviors,actions,interactions,comfort,contacts=true,pawTransport=false,motionTables=null}={}){
357
+ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,seat,arrivalSeat,behaviors,actions,interactions,comfort,contacts=true,pawTransport=false,motionTables=null,kernel=null}={}){
358
358
  if(![width,height].every(Number.isInteger)||width<2||height<2||width*height>1_000_000)throw Error('Invalid paper grid');
359
- const tween=createCachedTween(cache,info),renderMeta={...meta,pairs:info.pair_map,run_sequence:info.run_sequence};
359
+ const tween=createCachedTween(cache,info,kernel),renderMeta={...meta,pairs:info.pair_map,run_sequence:info.run_sequence};
360
360
  const out=new Uint8ClampedArray(width*height*4),area=createPaperAreaSampler();
361
361
  // Only one authored source is drawn at a time. Small samplers borrow a view of
362
362
  // the action-sized scratch. A sleep blend reads each base pixel before writing
@@ -364,14 +364,14 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
364
364
  const actionScratch=new Uint8ClampedArray(Math.max(320*320,(actions?.info.width??320)*(actions?.info.height??320))*4);
365
365
  const spriteScratch=actionScratch.subarray(0,320*320*4);
366
366
  for(let i=3;i<actionScratch.length;i+=4)actionScratch[i]=255;
367
- const rearSample=rear?createRearSampler(rear.bytes,rear.info,spriteScratch):null,lock=createPaperFootLock({maxImageStretch:pawTransport?1.45:Infinity});
368
- const approachSample=approach?createRearSampler(approach.bytes,approach.info,spriteScratch):null;
369
- const seatSample=seat?createPaperSeatSampler(seat.bytes,seat.info,spriteScratch):null;
370
- const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info,spriteScratch):null;
371
- const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info,spriteScratch):null;
372
- const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info,spriteScratch):null;
367
+ const rearSample=rear?createRearSampler(rear.bytes,rear.info,spriteScratch,kernel):null,lock=createPaperFootLock({maxImageStretch:pawTransport?1.45:Infinity});
368
+ const approachSample=approach?createRearSampler(approach.bytes,approach.info,spriteScratch,kernel):null;
369
+ const seatSample=seat?createPaperSeatSampler(seat.bytes,seat.info,spriteScratch,kernel):null;
370
+ const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info,spriteScratch,kernel):null;
371
+ const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info,spriteScratch,kernel):null;
372
+ const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info,spriteScratch,kernel):null;
373
373
  const comfortSample=comfort?createComfortSpriteSampler(comfort.bytes,comfort.info,spriteScratch):null;
374
- const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info,actionScratch.subarray(0,actions.info.width*actions.info.height*4),motionTables?.actionClips):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
374
+ const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info,actionScratch.subarray(0,actions.info.width*actions.info.height*4),motionTables?.actionClips,kernel):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
375
375
  const episodes=(key,labels,indices,samples,options)=>motionTables?.support[key]?createBakedSupportEpisodes(motionTables.support[key]):createPaperSupportEpisodes(labels,indices,samples,options);
376
376
  const walkEpisodes=actions?episodes('walk',actions.info.feet,actions.info.clips.walk,9):null;
377
377
  const sourceEpisodes=rear?{
@@ -415,8 +415,8 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
415
415
  let sx=Math.floor(rawX);if(state.flip)sx=sourceN-1-sx;
416
416
  return image[(Math.floor(rawY)*sourceN+sx)*4]>16;
417
417
  }}):null;render.lastTransport=transport;
418
- const minify=side<320||sideX<320,filter=isAction?actionArea:area;if(minify)filter.prepare(image);
419
- out.fill(0);for(let i=3;i<out.length;i+=4)out[i]=255;
418
+ const minify=side<320||sideX<320,filter=isAction?actionArea:area;if(minify&&!(kernel?.blit&&transport))filter.prepare(image);
419
+ if(kernel?.clear)kernel.clear(out);else{out.fill(0);for(let i=3;i<out.length;i+=4)out[i]=255;}
420
420
  render.lastPlacement={left,top,side,sideX};
421
421
  // Measured drawn mouth of the EXACT sample above (same priority, quantization,
422
422
  // placement and mirror). Side/rear/action/interaction stay null: unsupported.
@@ -430,7 +430,7 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
430
430
  }
431
431
  const drawTop=top-originY/320*side,bottom=top+Math.max(sourceN-originY,...pins.map(p=>p.target.y+20))/320*side;
432
432
  const drawLeft=left+Math.min(-originX,...pins.map(p=>p.target.x-20))/320*sideX,drawRight=left+Math.max(sourceN-originX,...pins.map(p=>p.target.x+20))/320*sideX;
433
- for(let dy=Math.max(0,minify?Math.floor(drawTop):Math.ceil(drawTop-.5));dy<Math.min(height,minify?Math.ceil(bottom):Math.ceil(bottom-.5));dy++)for(let dx=Math.max(0,minify?Math.floor(drawLeft):Math.ceil(drawLeft-.5));dx<Math.min(width,minify?Math.ceil(drawRight):Math.ceil(drawRight-.5));dx++){
433
+ if(kernel?.blit&&transport)kernel.blit(out,image,{width,height,sourceN,left,top,side,sideX,originX,originY,drawTop,bottom,drawLeft,drawRight,flip:state.flip,layers:transport.layers});else for(let dy=Math.max(0,minify?Math.floor(drawTop):Math.ceil(drawTop-.5));dy<Math.min(height,minify?Math.ceil(bottom):Math.ceil(bottom-.5));dy++)for(let dx=Math.max(0,minify?Math.floor(drawLeft):Math.ceil(drawLeft-.5));dx<Math.min(width,minify?Math.ceil(drawRight):Math.ceil(drawRight-.5));dx++){
434
434
  const px=(dx+.5-left)/sideX*320,py=(dy+.5-top)/side*320;
435
435
  const warped=transport?transport(px,py):footSourcePoint(px,py,pins);if(!warped)continue;
436
436
  const rawX=warped.x+originX,rawY=warped.y+originY;
Binary file
@@ -13,7 +13,7 @@ Usage:
13
13
  retrieviewer doctor [--agent ID] [--json]
14
14
  retrieviewer agents [--json]
15
15
  retrieviewer preview [--original]
16
- retrieviewer desktop [--ink cream|gold|dark] [--return-style auto|brake|trot|run-trot] [--watch stackbite|off] [--brain openrouter|off]
16
+ retrieviewer desktop [--pet dog|cat] [--engine auto|native|node] [--ink cream|gold|dark] [--return-style auto|brake|trot|run-trot] [--watch stackbite|off] [--brain openrouter|off]
17
17
  retrieviewer brain-check [--json]
18
18
  retrieviewer attention [--json]
19
19
  retrieviewer desktop-status [--json]
@@ -26,8 +26,10 @@ Non-interactive changes require --agent and --yes. Unsupported native renderers
26
26
  are refused: MCP connectivity alone is never treated as in-screen rendering.
27
27
  No command changes credentials, shell profiles or agent executables.
28
28
  Desktop is a separate native overlay, NOT a Codex plugin. Choose a window once;
29
- use Codex normally. Exit from its tray icon or desktop-stop. No auto-start is added.
30
- Windows is verified; macOS and Linux X11 adapters are experimental. Native Wayland
29
+ use Codex normally. Exit from its controls or desktop-stop. No auto-start is added.
30
+ Windows x64 basic mode uses one native process; Node exits after launch.
31
+ --pet cat selects the ginger cat. Watch/brain use the Node compatibility engine.
32
+ Windows and AlmaLinux8 X11 are verified; macOS is experimental. Native Wayland
31
33
  app tracking is not supported. Linux targets AlmaLinux8/GLIBC2.28, system Python3.6+.
32
34
  Return style defaults to auto (mix all three). Restart the companion to change it.
33
35
  Watch is OFF by default. Inside a live StackBite Seat, --watch stackbite reads only
@@ -42,7 +44,7 @@ export function parseArguments(args){
42
44
  const [command='help',...rest]=args,options={};
43
45
  for(let i=0;i<rest.length;i++){
44
46
  const token=rest[i];
45
- if(token==='--agent'||token==='--ink'||token==='--return-style'||token==='--watch'||token==='--brain'){
47
+ if(token==='--agent'||token==='--ink'||token==='--return-style'||token==='--watch'||token==='--brain'||token==='--pet'||token==='--engine'){
46
48
  const key=token.slice(2);
47
49
  if(options[key]||!rest[i+1]||rest[i+1].startsWith('-'))throw Error(token+' requires one value');
48
50
  options[key]=rest[++i];
@@ -50,13 +52,15 @@ export function parseArguments(args){
50
52
  else throw Error('Unknown argument: '+token);
51
53
  }
52
54
  if(options.agent&&!AGENTS.some(a=>a.id===options.agent))throw Error('Unknown agent: '+options.agent);
55
+ if(options.pet&&!['dog','cat'].includes(options.pet))throw Error('--pet must be dog or cat');
56
+ if(options.engine&&!['auto','native','node'].includes(options.engine))throw Error('--engine must be auto, native or node');
53
57
  if(options.ink&&!['cream','gold','dark'].includes(options.ink))throw Error('--ink must be cream, gold or dark');
54
58
  if(options['return-style']&&!['auto','brake','trot','run-trot'].includes(options['return-style']))throw Error('--return-style must be auto, brake, trot or run-trot');
55
59
  if(options.watch&&!['off','stackbite'].includes(options.watch))throw Error('--watch must be stackbite or off');
56
60
  if(options.brain&&!['off','openrouter'].includes(options.brain))throw Error('--brain must be openrouter or off');
57
61
  if(options.brain==='openrouter'&&options.watch!=='stackbite')throw Error('--brain openrouter requires --watch stackbite');
58
62
  if(!['help','--help','-h','enable','disable','disconnect','status','doctor','agents','preview','attention','brain-check','desktop','desktop-status','desktop-stop','desktop-clean','desktop-remove'].includes(command))throw Error('Unknown command: '+command);
59
- const allowed={desktop:['ink','return-style','watch','brain'],attention:['json'],'brain-check':['json'],enable:['agent','yes','json','dry-run'],disconnect:['agent','yes','json','dry-run'],disable:['agent','json'],status:['agent','json'],doctor:['agent','json'],agents:['json'],preview:['original'],'desktop-status':['json'],'desktop-clean':['yes'],'desktop-remove':['yes']};
63
+ const allowed={desktop:['ink','return-style','watch','brain','pet','engine'],attention:['json'],'brain-check':['json'],enable:['agent','yes','json','dry-run'],disconnect:['agent','yes','json','dry-run'],disable:['agent','json'],status:['agent','json'],doctor:['agent','json'],agents:['json'],preview:['original'],'desktop-status':['json'],'desktop-clean':['yes'],'desktop-remove':['yes']};
60
64
  for(const key of Object.keys(options))if(!(allowed[command]??[]).includes(key))throw Error(`--${key} is not supported by ${command}`);
61
65
  return {command,options};
62
66
  }
@@ -82,7 +86,7 @@ export async function runCli(args,{input=process.stdin,output=process.stdout,err
82
86
  if(['desktop-clean','desktop-remove'].includes(command)&&!options.yes)throw Error('Use '+command+' --yes to remove only the companion cache.');
83
87
  const api=desktop??await import('./integrations/windows-overlay/desktop.mjs');
84
88
  const action={'desktop':'startDesktop','desktop-status':'desktopStatus','desktop-stop':'stopDesktop','desktop-clean':'cleanDesktop','desktop-remove':'removeDesktop'}[command];
85
- emit(await api[action]({...options.ink?{ink:options.ink}:{},...options['return-style']?{returnStyle:options['return-style']}:{},...options.watch?{watch:options.watch}:{},...options.brain?{brain:options.brain}:{} }));return 0;
89
+ emit(await api[action]({...options.pet?{pet:options.pet}:{},...options.engine?{engine:options.engine}:{},...options.ink?{ink:options.ink}:{},...options['return-style']?{returnStyle:options['return-style']}:{},...options.watch?{watch:options.watch}:{},...options.brain?{brain:options.brain}:{} }));return 0;
86
90
  }
87
91
  if(command==='preview'){await import('./examples/live.mjs');return 0;}
88
92
  if(['status','doctor','agents'].includes(command)){
@@ -1,7 +1,7 @@
1
1
  /** A headless pet: the host owns input, time and terminal rendering. */
2
2
  export type FetchReturnStyle = 'auto' | 'brake' | 'trot' | 'run-trot';
3
3
  export interface PetOptions {
4
- /** Asset names are relative; return original file bytes (keep .gz files compressed). */
4
+ /** Relative names; return unchanged .rvz/.gz file bytes. Custom loaders use memory-backed art. */
5
5
  loadAsset?: (name: string) => Promise<ArrayBuffer | ArrayBufferView>;
6
6
  seed?: number;
7
7
  energy?: number;
package/retrieviewer.mjs CHANGED
@@ -5,6 +5,7 @@ import {createSnappyTrickTimings} from './puppy-paper-trick-timing.mjs';
5
5
  import {createCompanionLife,FETCH_RETURN_STYLES} from './puppy-companion-life.mjs';
6
6
  import {paintCompanionObjects} from './puppy-companion-objects.mjs';
7
7
  import {readPackedFrames} from './puppy-frame-store.mjs';
8
+ import {expandIndexedArchive} from './puppy-frame-indexed.mjs';
8
9
  import {BAKED_MOTION} from './puppy-motion-tables.generated.mjs';
9
10
 
10
11
  // Embeddable autonomous puppy. Importing or creating has no DOM, timer, stdin/stdout
@@ -19,12 +20,12 @@ export const ART_PROFILES=Object.freeze(['original','q5']);
19
20
  export const PET_ASSETS=Object.freeze({
20
21
  // Kept as a legacy identifier only; createPet/getPetAssetFiles no longer load it.
21
22
  animal:'locomotion-puppy.animal',
22
- stride:'puppy-stride-tweens-q5.bin.gz',strideInfo:'puppy-stride-meta.json',stableInfo:'puppy-stable-meta.json',
23
+ stride:'puppy-stride-tweens-q5.rvz',strideInfo:'puppy-stride-meta.json',stableInfo:'puppy-stable-meta.json',
23
24
  rear:'puppy-paper-rear-gallop',approach:'puppy-paper-approach',seat:'puppy-paper-seat',arrivalSeat:'puppy-paper-arrival-seat',
24
25
  behaviors:'puppy-paper-behaviors',actions:'puppy-paper-actions-v9',interactions:'puppy-companion-interactions-v3',comfort:'puppy-companion-comfort',
25
26
  });
26
27
  const BUNDLES=['rear','approach','seat','arrivalSeat','behaviors','actions','interactions','comfort'];
27
- const frameFile=(base,profile)=>base+(profile==='q5'?'-q5.bin.gz':'-compact.bin.gz');
28
+ const frameFile=(base,profile)=>base+(profile==='q5'?'-q5.rvz':'-compact.bin.gz');
28
29
  /** Exact files a host/bundler must copy, including metadata. Never loads assets. */
29
30
  export function getPetAssetFiles(artProfile=DEFAULT_ART_PROFILE){
30
31
  if(!ART_PROFILES.includes(artProfile))throw RangeError('artProfile must be original or q5');
@@ -50,9 +51,9 @@ const bytesOf=(name,value)=>{
50
51
  if(ArrayBuffer.isView(value))return new Uint8Array(value.buffer,value.byteOffset,value.byteLength);
51
52
  throw TypeError(`loadAsset(${name}) must return an ArrayBuffer or typed bytes`);
52
53
  };
53
- async function gunzip(bytes){
54
+ async function gunzip(bytes,size){
54
55
  // Native Node gzip avoids Blob/Response/stream copies of every large cache.
55
- if(typeof process!=='undefined'&&process.versions?.node){const {gunzipSync}=await import('node:zlib');return gunzipSync(bytes);}
56
+ if(typeof process!=='undefined'&&process.versions?.node){const {gunzipSync}=await import('node:zlib');return gunzipSync(bytes,size===undefined?undefined:{maxOutputLength:Math.max(1,size)});}
56
57
  if(typeof DecompressionStream!=='undefined')return new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer());
57
58
  throw Error('This browser needs DecompressionStream for pet assets');
58
59
  }
@@ -75,8 +76,8 @@ function heldFrameKey(state,report,{columns,rows,cellAspect=.5,camera,poseFps=24
75
76
  report.ball,report.treat]);
76
77
  }
77
78
  async function loadPetAssets(loadAsset,artProfile){
78
- // Node keeps filtered crops compressed; only the existing two-frame LRU is
79
- // expanded. Browser bundles retain the portable bit-packed implementation.
79
+ // Default local Node hosts keep art on disk and reconstruct two frames per
80
+ // family. Custom/browser loaders retain the compatible memory-backed store.
80
81
  let codec;
81
82
  if(typeof process!=='undefined'&&process.versions?.node){
82
83
  const {deflateRawSync,inflateRawSync}=await import('node:zlib');
@@ -86,9 +87,18 @@ async function loadPetAssets(loadAsset,artProfile){
86
87
  }
87
88
  const load=async name=>bytesOf(name,await loadAsset(name));
88
89
  const json=async name=>JSON.parse(new TextDecoder().decode(await load(name)));
89
- const frames=async (name,options)=>readPackedFrames(await gunzip(await load(name)),{...options,codec});
90
- // One family at a time: never inflate all eight legacy-sized buffers together.
90
+ let openFrameFile;
91
+ if(artProfile==='q5'&&loadAsset===defaultLoadAsset&&new URL('.',import.meta.url).protocol==='file:'&&typeof process!=='undefined'&&process.versions?.node)({openFrameFile}=await import('./puppy-frame-file.mjs'));
92
+ const frames=async (name,options)=>{
93
+ if(name.endsWith('.rvz')){
94
+ if(openFrameFile)return openFrameFile(new URL(name,import.meta.url),options);
95
+ return readPackedFrames(await expandIndexedArchive(await load(name),gunzip),{...options,codec});
96
+ }
97
+ return readPackedFrames(await gunzip(await load(name)),{...options,codec});
98
+ };
99
+ // Read small indices locally; expand portable archives one family at a time.
91
100
  const data={assets:{}};
101
+ try{
92
102
  data.info=await json(PET_ASSETS.strideInfo);data.meta=await json(PET_ASSETS.stableInfo);
93
103
  data.cache=await frames(frameFile('puppy-stride-tweens',artProfile));
94
104
  for(const key of BUNDLES){
@@ -100,18 +110,20 @@ async function loadPetAssets(loadAsset,artProfile){
100
110
  data.assets[key]={info,bytes:await frames(frameFile(base,artProfile),options)};
101
111
  }
102
112
  return data;
113
+ }catch(error){disposeAssets(data);throw error;}
103
114
  }
115
+ function disposeAssets(data){data?.cache?.dispose?.();for(const family of Object.values(data?.assets??{}))family.bytes?.dispose?.();}
104
116
  async function acquireAssets(loadAsset,artProfile){
105
117
  let entry;
106
118
  if(loadAsset===defaultLoadAsset){entry=sharedAssets.get(artProfile);if(!entry){entry={users:0,promise:loadPetAssets(loadAsset,artProfile)};sharedAssets.set(artProfile,entry);}}
107
119
  else entry={users:0,promise:loadPetAssets(loadAsset,artProfile)};
108
120
  entry.users++;
109
121
  let released=false;
110
- const release=()=>{if(released)return;released=true;const previous=entry;entry=null;if(--previous.users===0&&sharedAssets.get(artProfile)===previous)sharedAssets.delete(artProfile);};
111
- try{return{data:await entry.promise,release};}catch(error){release();throw error;}
122
+ const release=()=>{if(released)return;released=true;const previous=entry;entry=null;if(--previous.users===0){if(sharedAssets.get(artProfile)===previous)sharedAssets.delete(artProfile);disposeAssets(previous.data);}};
123
+ try{const data=await entry.promise;entry.data=data;return{data,release};}catch(error){release();throw error;}
112
124
  }
113
125
 
114
- export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE,returnStyle='auto'}={}){
126
+ function petOptions({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE,returnStyle='auto'}={}){
115
127
  if(typeof loadAsset!=='function')throw TypeError('loadAsset must be a function');
116
128
  if(seed===undefined)seed=globalThis.crypto?.getRandomValues?globalThis.crypto.getRandomValues(new Uint32Array(1))[0]:731;
117
129
  if(!Number.isInteger(seed)||seed<0||seed>0xffffffff)throw RangeError('seed must be a uint32');
@@ -119,7 +131,16 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
119
131
  if(typeof pawTransport!=='boolean')throw TypeError('pawTransport must be boolean');
120
132
  if(!ART_PROFILES.includes(artProfile))throw RangeError('artProfile must be original or q5');
121
133
  if(!FETCH_RETURN_STYLES.includes(returnStyle))throw RangeError('returnStyle must be auto, brake, trot or run-trot');
122
- let lease=await acquireAssets(loadAsset,artProfile),data=lease.data;
134
+ return {loadAsset,seed,energy,pawTransport,artProfile,returnStyle};
135
+ }
136
+ export async function createPet(options={}){
137
+ const normalized=petOptions(options),lease=await acquireAssets(normalized.loadAsset,normalized.artProfile);
138
+ return createPetFromAssets(normalized,lease);
139
+ }
140
+ // Shared simulation/render core for local native hosts. The provider owns I/O.
141
+ export function createPetFromAssets(options,lease,kernel=null){
142
+ const {loadAsset,seed,energy,pawTransport,artProfile,returnStyle}=petOptions(options);
143
+ let data=lease.data;
123
144
  const release=lease.release;lease=null;
124
145
  let {cache,info,meta}=data;const assets={...data.assets};data=null;
125
146
  // Baked tables describe the bundled Q5 source only. External/optional artwork
@@ -154,14 +175,14 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
154
175
  if(typeof cellAspect!=='number'||!Number.isFinite(cellAspect)||cellAspect<=0)throw RangeError('cellAspect must be a positive number');
155
176
  if(![12,24,30].includes(poseFps))throw RangeError('poseFps must be 12, 24 or 30');
156
177
  const width=columns*2,height=rows*4,key=`${width}x${height}`;
157
- if(paintKey!==key){paint=createPaperRenderer(cache,info,meta,width,height,{...assets,pawTransport,motionTables});paintKey=key;}
178
+ if(paintKey!==key){paint=createPaperRenderer(cache,info,meta,width,height,{...assets,pawTransport,motionTables,kernel});paintKey=key;}
158
179
  const viewportWidth=camera?.viewportWidth??columns*cellAspect,viewportHeight=camera?.viewportHeight??rows;
159
180
  const projection=createPaperProjection({viewportWidth,viewportHeight,rect:camera?.rect??{left:0,top:0,width:viewportWidth,height:viewportHeight},width,height});
160
181
  const state=actor.sample(time,{poseFps}),report=life.report;
161
182
  if(report.gesture)state.interactionPose=report.gesture;
162
183
  const placement=projection.placement(state.x,state.z,state.bodyLift??0),pixels=paint(state,{...placement,contactEnabled:!!contactEnabled});
163
184
  paintCompanionObjects(pixels,width,height,projection,report,state,paint.lastMouth);
164
- const {text}=rgbaToBraille(pixels,width,height,{method:'stable',brightness:1.18,contrast:1.1});
185
+ const {text}=rgbaToBraille(pixels,width,height,{method:'stable',brightness:1.18,contrast:1.1,kernel});
165
186
  lastHeldFrameKey=heldFrameKey(state,report,{columns,rows,cellAspect,camera,poseFps,contactEnabled});
166
187
  return {text,columns,rows,pixels,state,report,placement,projection,artProfile,mouth:paint.lastMouth,contacts:paint.report()};
167
188
  },
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file