TiGa-RCE commited on
Commit
9f816c2
路
verified 路
1 Parent(s): 81953fd

Fix track preview start marker alignment

Browse files

The track preview placed the green start marker at the center of StartTileCoords and ignored StartTileConnection. On A01, this shifted the marker half a tile left of the saved start.

Use the saved connection for the marker and share the native connection offsets with the orange route endpoints. Validate that the start matches its atlas tile and clarify that the route is approximate.

This PR includes the matching RELEASE_MANIFEST.json entry so the renderer and integrity manifest reach main together.

Validation: six focused renderer regressions pass, covering A01, every side/index, adjacent endpoints, invalid start data, and input preservation. Independent review also accepted all 828 connections in the current atlas.

Files changed (1) hide show
  1. track-map.js +32 -16
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.';