Fix track preview start marker alignment

#3
by TiGa-RCE - opened
Files changed (2) hide show
  1. RELEASE_MANIFEST.json +1 -1
  2. track-map.js +32 -16
RELEASE_MANIFEST.json CHANGED
@@ -83,7 +83,7 @@
83
  "star_check.py": "5c17080957b81ffccf0de8314635b471562e55c92c97a1207d9034a99279b2c3",
84
  "steam_leaderboard.py": "d76397c39cae58f636ddf9a9be1a46ae3d77fefd6b7269feac73d34d97165481",
85
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
86
- "track-map.js": "ab5374aa49c7ccec90a787ee8c56cd71beb56b903bf2bbc322783e6f43c009f0",
87
  "training_config.py": "6eb7282bbee3b3a6273bddc29c949d9904067414c26e501d815012f033173a1c",
88
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
89
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
 
83
  "star_check.py": "5c17080957b81ffccf0de8314635b471562e55c92c97a1207d9034a99279b2c3",
84
  "steam_leaderboard.py": "d76397c39cae58f636ddf9a9be1a46ae3d77fefd6b7269feac73d34d97165481",
85
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
86
+ "track-map.js": "547b737d123d9479fc166ddc564568c0ff4b2099200c78e36bb46ec461bc754c",
87
  "training_config.py": "6eb7282bbee3b3a6273bddc29c949d9904067414c26e501d815012f033173a1c",
88
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
89
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
track-map.js CHANGED
@@ -14,6 +14,25 @@
14
  throw Error('Track coordinates exceed preview bounds');
15
  return [x,y];
16
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  async function decode(file){
18
  if(!file||file.size<1||file.size>1048576)throw Error('Choose a .track file under 1 MB');
19
  if(typeof DecompressionStream!=='function')throw Error('This browser cannot preview gzip tracks');
@@ -39,14 +58,20 @@
39
  const id=item?.Block?.TileSetId;
40
  const block=atlas?.blocks?.[`${id?.AtlasId}:${id?.AlternativeId}`];
41
  if(!block||![0,1,2].includes(block.surface))throw Error('Track uses a piece not in the simulator atlas');
42
- return {x,y,surface:block.surface,connections:block.connections};
 
43
  });
44
  if(new Set(tiles.map(t=>`${t.x},${t.y}`)).size!==tiles.length)throw Error('Duplicate track tile');
45
  const start=vector(saved.StartTileCoords);
 
 
 
 
 
46
  const minX=Math.min(...tiles.map(t=>t.x)),maxX=Math.max(...tiles.map(t=>t.x));
47
  const minY=Math.min(...tiles.map(t=>t.y)),maxY=Math.max(...tiles.map(t=>t.y));
48
  if(maxX-minX>30||maxY-minY>30)throw Error('Track preview is too large');
49
- return {tiles,start,bounds:{minX,maxX,minY,maxY}};
50
  }
51
  function render(canvas,track){
52
  const context=canvas.getContext('2d');
@@ -63,25 +88,16 @@
63
  context.beginPath();context.roundRect(x+2,y+2,size-4,size-4,Math.max(4,size*.13));context.fill();
64
  context.strokeStyle='#d7e8f2';context.lineWidth=Math.max(1,size*.018);
65
  context.stroke();
66
- const edge=connection=>{
67
- const offset=(connection.index-1)*size*.16;
68
- switch(connection.side){
69
- case 0:return [x+size/2+offset,y+2];
70
- case 1:return [x+size/2+offset,y+size-2];
71
- case 2:return [x+2,y+size/2+offset];
72
- case 3:return [x+size-2,y+size/2+offset];
73
- default:return [x+size/2,y+size/2];
74
- }
75
- };
76
  if(Array.isArray(tile.connections)&&tile.connections.length>=2){
77
- const a=edge(tile.connections[0]),z=edge(tile.connections[1]);
78
  context.beginPath();context.moveTo(...a);
79
  context.quadraticCurveTo(x+size/2,y+size/2,...z);
80
  context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055);
81
  context.lineCap='round';context.stroke();
82
  }
83
  }
84
- const sx=left+(track.start[0]-b.minX+.5)*size,sy=top+(track.start[1]-b.minY+.5)*size;
 
85
  context.beginPath();context.arc(sx,sy,Math.max(6,size*.105),0,Math.PI*2);
86
  context.fillStyle='#66e5aa';context.fill();context.strokeStyle='#0c1926';context.lineWidth=3;context.stroke();
87
  }
@@ -106,10 +122,10 @@
106
  if(current!==version)return;
107
  render(canvas,track);
108
  const counts=[0,0,0];track.tiles.forEach(tile=>counts[tile.surface]++);
109
- status.textContent=source+' · '+track.tiles.length+' pieces · green dot marks the start';
110
  legend.textContent=NAMES.filter((_,i)=>counts[i]).map((name,index)=>{
111
  const i=NAMES.indexOf(name);return name+' '+counts[i];
112
- }).join(' · ')+' · Piece and terrain preview; collision shapes are not shown.';
113
  }catch(error){
114
  if(current!==version)return;
115
  renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.';
 
14
  throw Error('Track coordinates exceed preview bounds');
15
  return [x,y];
16
  }
17
+ function connection(value,message='Track has an invalid piece connection'){
18
+ if(!value||typeof value!=='object'||Array.isArray(value)||
19
+ !Number.isInteger(value.side)||value.side<0||value.side>3||
20
+ !Number.isInteger(value.index)||value.index<0||value.index>2||
21
+ !Number.isInteger(value.type)||value.type<0)throw Error(message);
22
+ return {side:value.side,index:value.index,type:value.type};
23
+ }
24
+ function connectionPoint(value,x,y,size){
25
+ const {side,index}=connection(value);
26
+ // Native Connection.position uses offsets 128,384,640 in a 768-unit tile.
27
+ // Shared boundaries keep adjacent path endpoints and the saved start aligned.
28
+ const offset=[128,384,640][index]/768*size;
29
+ switch(side){
30
+ case 0:return [x+offset,y];
31
+ case 1:return [x+offset,y+size];
32
+ case 2:return [x,y+offset];
33
+ case 3:return [x+size,y+offset];
34
+ }
35
+ }
36
  async function decode(file){
37
  if(!file||file.size<1||file.size>1048576)throw Error('Choose a .track file under 1 MB');
38
  if(typeof DecompressionStream!=='function')throw Error('This browser cannot preview gzip tracks');
 
58
  const id=item?.Block?.TileSetId;
59
  const block=atlas?.blocks?.[`${id?.AtlasId}:${id?.AlternativeId}`];
60
  if(!block||![0,1,2].includes(block.surface))throw Error('Track uses a piece not in the simulator atlas');
61
+ if(!Array.isArray(block.connections)||block.connections.length!==2)throw Error('Track piece connections are unavailable');
62
+ return {x,y,surface:block.surface,connections:block.connections.map(value=>connection(value))};
63
  });
64
  if(new Set(tiles.map(t=>`${t.x},${t.y}`)).size!==tiles.length)throw Error('Duplicate track tile');
65
  const start=vector(saved.StartTileCoords);
66
+ const startTile=tiles.find(tile=>tile.x===start[0]&&tile.y===start[1]);
67
+ if(!startTile)throw Error('Start tile is not present in this track');
68
+ const startConnection=connection({side:saved.StartTileConnection?.Side,index:saved.StartTileConnection?.Index,type:saved.StartTileConnection?.Type},'Track has an invalid or missing start connection');
69
+ if(!startTile.connections.some(item=>item.side===startConnection.side&&item.index===startConnection.index&&item.type===startConnection.type))
70
+ throw Error('Saved start connection does not match the start tile');
71
  const minX=Math.min(...tiles.map(t=>t.x)),maxX=Math.max(...tiles.map(t=>t.x));
72
  const minY=Math.min(...tiles.map(t=>t.y)),maxY=Math.max(...tiles.map(t=>t.y));
73
  if(maxX-minX>30||maxY-minY>30)throw Error('Track preview is too large');
74
+ return {tiles,start,startConnection,bounds:{minX,maxX,minY,maxY}};
75
  }
76
  function render(canvas,track){
77
  const context=canvas.getContext('2d');
 
88
  context.beginPath();context.roundRect(x+2,y+2,size-4,size-4,Math.max(4,size*.13));context.fill();
89
  context.strokeStyle='#d7e8f2';context.lineWidth=Math.max(1,size*.018);
90
  context.stroke();
 
 
 
 
 
 
 
 
 
 
91
  if(Array.isArray(tile.connections)&&tile.connections.length>=2){
92
+ const a=connectionPoint(tile.connections[0],x,y,size),z=connectionPoint(tile.connections[1],x,y,size);
93
  context.beginPath();context.moveTo(...a);
94
  context.quadraticCurveTo(x+size/2,y+size/2,...z);
95
  context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055);
96
  context.lineCap='round';context.stroke();
97
  }
98
  }
99
+ const startX=left+(track.start[0]-b.minX)*size,startY=top+(track.start[1]-b.minY)*size;
100
+ const [sx,sy]=connectionPoint(track.startConnection,startX,startY,size);
101
  context.beginPath();context.arc(sx,sy,Math.max(6,size*.105),0,Math.PI*2);
102
  context.fillStyle='#66e5aa';context.fill();context.strokeStyle='#0c1926';context.lineWidth=3;context.stroke();
103
  }
 
122
  if(current!==version)return;
123
  render(canvas,track);
124
  const counts=[0,0,0];track.tiles.forEach(tile=>counts[tile.surface]++);
125
+ status.textContent=source+' · '+track.tiles.length+' pieces · green dot marks the saved starting point';
126
  legend.textContent=NAMES.filter((_,i)=>counts[i]).map((name,index)=>{
127
  const i=NAMES.indexOf(name);return name+' '+counts[i];
128
+ }).join(' · ')+' · Orange line shows the approximate route; collision shapes and car positions are not shown.';
129
  }catch(error){
130
  if(current!==version)return;
131
  renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.';