Spaces:
Running on Zero
Running on Zero
Download track-map.js from TiGa-RCE/ald-tao-gpu: direct link, hf CLI and curl.
- Browser
- Download file 7.95 kB
-
https://huggingface.co/spaces/TiGa-RCE/ald-tao-gpu/resolve/refs%2Fpr%2F3/track-map.js
- Command line
-
hf download hf://spaces/TiGa-RCE/ald-tao-gpu@refs/pr/3/track-map.js
-
curl -L -o track-map.js https://huggingface.co/spaces/TiGa-RCE/ald-tao-gpu/resolve/refs%2Fpr%2F3/track-map.js
7.95 kB
| /* Browser-only preview of the selected saved track. No upload for preview. */ | |
| (function(root,factory){ | |
| if(typeof module==='object'&&module.exports)module.exports=factory(); | |
| else root.TrackMap=factory(); | |
| })(typeof window==='undefined'?this:window,function(){ | |
| const COLOURS=['#607e96','#b89561','#b9d9ed']; | |
| const NAMES=['Asphalt','Dirt','Ice']; | |
| const VECTOR=/^Vector2i\((-?\d+),\s*(-?\d+)\)$/; | |
| function vector(value){ | |
| const match=typeof value==='string'&&VECTOR.exec(value); | |
| if(!match)throw Error('Track has invalid tile coordinates'); | |
| const x=Number(match[1]),y=Number(match[2]); | |
| if(!Number.isSafeInteger(x)||!Number.isSafeInteger(y)||Math.abs(x)>1000||Math.abs(y)>1000) | |
| throw Error('Track coordinates exceed preview bounds'); | |
| return [x,y]; | |
| } | |
| function connection(value,message='Track has an invalid piece connection'){ | |
| if(!value||typeof value!=='object'||Array.isArray(value)|| | |
| !Number.isInteger(value.side)||value.side<0||value.side>3|| | |
| !Number.isInteger(value.index)||value.index<0||value.index>2|| | |
| !Number.isInteger(value.type)||value.type<0)throw Error(message); | |
| return {side:value.side,index:value.index,type:value.type}; | |
| } | |
| function connectionPoint(value,x,y,size){ | |
| const {side,index}=connection(value); | |
| // Native Connection.position uses offsets 128,384,640 in a 768-unit tile. | |
| // Shared boundaries keep adjacent path endpoints and the saved start aligned. | |
| const offset=[128,384,640][index]/768*size; | |
| switch(side){ | |
| case 0:return [x+offset,y]; | |
| case 1:return [x+offset,y+size]; | |
| case 2:return [x,y+offset]; | |
| case 3:return [x+size,y+offset]; | |
| } | |
| } | |
| async function decode(file){ | |
| if(!file||file.size<1||file.size>1048576)throw Error('Choose a .track file under 1 MB'); | |
| if(typeof DecompressionStream!=='function')throw Error('This browser cannot preview gzip tracks'); | |
| const reader=file.stream().pipeThrough(new DecompressionStream('gzip')).getReader(); | |
| const chunks=[];let total=0; | |
| try{ | |
| while(true){ | |
| const {done,value}=await reader.read();if(done)break; | |
| total+=value.byteLength; | |
| if(total>2*1024*1024)throw Error('Track preview exceeds the 2 MB expanded limit'); | |
| chunks.push(value); | |
| } | |
| }finally{reader.releaseLock();} | |
| const bytes=new Uint8Array(total);let at=0; | |
| for(const chunk of chunks){bytes.set(chunk,at);at+=chunk.length;} | |
| return JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes)); | |
| } | |
| function model(saved,atlas){ | |
| const entries=Object.entries(saved?.Tiles??{}); | |
| if(entries.length<1||entries.length>256)throw Error('Track has no previewable tiles'); | |
| const tiles=entries.map(([position,item])=>{ | |
| const [x,y]=vector(position); | |
| const id=item?.Block?.TileSetId; | |
| const block=atlas?.blocks?.[`${id?.AtlasId}:${id?.AlternativeId}`]; | |
| if(!block||![0,1,2].includes(block.surface))throw Error('Track uses a piece not in the simulator atlas'); | |
| if(!Array.isArray(block.connections)||block.connections.length!==2)throw Error('Track piece connections are unavailable'); | |
| return {x,y,surface:block.surface,connections:block.connections.map(value=>connection(value))}; | |
| }); | |
| if(new Set(tiles.map(t=>`${t.x},${t.y}`)).size!==tiles.length)throw Error('Duplicate track tile'); | |
| const start=vector(saved.StartTileCoords); | |
| const startTile=tiles.find(tile=>tile.x===start[0]&&tile.y===start[1]); | |
| if(!startTile)throw Error('Start tile is not present in this track'); | |
| const startConnection=connection({side:saved.StartTileConnection?.Side,index:saved.StartTileConnection?.Index,type:saved.StartTileConnection?.Type},'Track has an invalid or missing start connection'); | |
| if(!startTile.connections.some(item=>item.side===startConnection.side&&item.index===startConnection.index&&item.type===startConnection.type)) | |
| throw Error('Saved start connection does not match the start tile'); | |
| const minX=Math.min(...tiles.map(t=>t.x)),maxX=Math.max(...tiles.map(t=>t.x)); | |
| const minY=Math.min(...tiles.map(t=>t.y)),maxY=Math.max(...tiles.map(t=>t.y)); | |
| if(maxX-minX>30||maxY-minY>30)throw Error('Track preview is too large'); | |
| return {tiles,start,startConnection,bounds:{minX,maxX,minY,maxY}}; | |
| } | |
| function render(canvas,track){ | |
| const context=canvas.getContext('2d'); | |
| if(!context)throw Error('Canvas preview unavailable'); | |
| const w=canvas.width,h=canvas.height; | |
| context.clearRect(0,0,w,h); | |
| context.fillStyle='#0c1926';context.fillRect(0,0,w,h); | |
| const b=track.bounds,columns=b.maxX-b.minX+1,rows=b.maxY-b.minY+1; | |
| const size=Math.min(160,(w-70)/columns,(h-70)/rows); | |
| const left=(w-columns*size)/2,top=(h-rows*size)/2; | |
| for(const tile of track.tiles){ | |
| const x=left+(tile.x-b.minX)*size,y=top+(tile.y-b.minY)*size; | |
| context.fillStyle=COLOURS[tile.surface]; | |
| context.beginPath();context.roundRect(x+2,y+2,size-4,size-4,Math.max(4,size*.13));context.fill(); | |
| context.strokeStyle='#d7e8f2';context.lineWidth=Math.max(1,size*.018); | |
| context.stroke(); | |
| if(Array.isArray(tile.connections)&&tile.connections.length>=2){ | |
| const a=connectionPoint(tile.connections[0],x,y,size),z=connectionPoint(tile.connections[1],x,y,size); | |
| context.beginPath();context.moveTo(...a); | |
| context.quadraticCurveTo(x+size/2,y+size/2,...z); | |
| context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055); | |
| context.lineCap='round';context.stroke(); | |
| } | |
| } | |
| const startX=left+(track.start[0]-b.minX)*size,startY=top+(track.start[1]-b.minY)*size; | |
| const [sx,sy]=connectionPoint(track.startConnection,startX,startY,size); | |
| context.beginPath();context.arc(sx,sy,Math.max(6,size*.105),0,Math.PI*2); | |
| context.fillStyle='#66e5aa';context.fill();context.strokeStyle='#0c1926';context.lineWidth=3;context.stroke(); | |
| } | |
| function mount({document,fetch,picker}){ | |
| const canvas=document.getElementById('trackMap'); | |
| const status=document.getElementById('mapStatus'); | |
| const legend=document.getElementById('mapLegend'); | |
| if(!canvas||!status||!picker)return; | |
| let version=0,atlasPromise=null; | |
| async function refresh(){ | |
| const current=++version; | |
| const info=picker.info(); | |
| const source=info.source==='builtin'?info.display_name:document.getElementById('track')?.files?.[0]?.name; | |
| if(!source){status.textContent='Select a built-in track or choose a custom .track file.';legend.textContent='';renderEmpty();return;} | |
| status.textContent='Loading '+source+'…'; | |
| try{ | |
| atlasPromise??=fetch('/assets/track-atlas.json',{cache:'no-store',credentials:'same-origin'}).then(r=>{ | |
| if(!r.ok)throw Error('Track atlas unavailable');return r.json(); | |
| }); | |
| const [file,atlas]=await Promise.all([picker.file(),atlasPromise]); | |
| const track=model(await decode(file),atlas); | |
| if(current!==version)return; | |
| render(canvas,track); | |
| const counts=[0,0,0];track.tiles.forEach(tile=>counts[tile.surface]++); | |
| status.textContent=source+' · '+track.tiles.length+' pieces · green dot marks the saved starting point'; | |
| legend.textContent=NAMES.filter((_,i)=>counts[i]).map((name,index)=>{ | |
| const i=NAMES.indexOf(name);return name+' '+counts[i]; | |
| }).join(' · ')+' · Orange line shows the approximate route; collision shapes and car positions are not shown.'; | |
| }catch(error){ | |
| if(current!==version)return; | |
| renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.'; | |
| } | |
| } | |
| function renderEmpty(){const ctx=canvas.getContext('2d');ctx.clearRect(0,0,canvas.width,canvas.height);ctx.fillStyle='#0c1926';ctx.fillRect(0,0,canvas.width,canvas.height);} | |
| document.addEventListener('track-library-change',refresh); | |
| refresh(); | |
| return {refresh}; | |
| } | |
| return {mount,model,decode,render}; | |
| }); | |