Spaces:
Running on Zero
Draw track preview with native cubic curves
Browse filesThe preview still pulled each segment toward its tile center with a quadratic curve. After correcting connection coordinates, that shortcut caused pointed joins and bent offset straight pieces, including A01's bottom-middle tile.
Draw cubic Bezier segments using the simulator source's inward connection normals, type-dependent tangent angles, and handle lengths. This reproduces the native track centerline while retaining the corrected saved starting point. Update the caption and pair the renderer with its RELEASE_MANIFEST.json entry.
Validation: nine focused checks pass. They include all eight A01 cubic controls against independent native-source reference values, all eight tangent joins, the offset straight, angular/mixed-type pieces in both connection orders, and the earlier start-coordinate and invalid-input regressions. References use native revision b90b8af06ffc4ea4e4fd703becd1107a48b8e5e0.
- track-map.js +19 -3
|
@@ -33,6 +33,20 @@
|
|
| 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');
|
|
@@ -89,9 +103,11 @@
|
|
| 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
|
|
|
|
|
|
|
| 93 |
context.beginPath();context.moveTo(...a);
|
| 94 |
-
context.
|
| 95 |
context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055);
|
| 96 |
context.lineCap='round';context.stroke();
|
| 97 |
}
|
|
@@ -125,7 +141,7 @@
|
|
| 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
|
| 129 |
}catch(error){
|
| 130 |
if(current!==version)return;
|
| 131 |
renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.';
|
|
|
|
| 33 |
case 3:return [x+size,y+offset];
|
| 34 |
}
|
| 35 |
}
|
| 36 |
+
function connectionControl(own,other,point,target){
|
| 37 |
+
// Native TrackUtils.GetPathCurve (random_track.rs, b90b8af): each
|
| 38 |
+
// connection's inward normal sets its cubic handle, adjusted by port type.
|
| 39 |
+
// At the exit this is the next tile's negated entry normal, so the same
|
| 40 |
+
// per-tile construction also preserves the curve when endpoints reverse.
|
| 41 |
+
const [nx,ny]=[[0,1],[0,-1],[1,0],[-1,0]][own.side];
|
| 42 |
+
const dx=target[0]-point[0],dy=target[1]-point[1];
|
| 43 |
+
const angle=own.type===1?Math.PI/4:own.type===3?Math.atan2(128,384):own.type===4?Math.atan2(384,128):0;
|
| 44 |
+
const rotation=Math.sign(nx*dy-ny*dx)*angle;
|
| 45 |
+
const c=Math.cos(rotation),s=Math.sin(rotation);
|
| 46 |
+
const smoothness=[0,2,100].includes(own.type)&&[1,3,4].includes(other.type)?0.2:0.5;
|
| 47 |
+
const reach=Math.hypot(dx,dy)*smoothness;
|
| 48 |
+
return [point[0]+(nx*c-ny*s)*reach,point[1]+(nx*s+ny*c)*reach];
|
| 49 |
+
}
|
| 50 |
async function decode(file){
|
| 51 |
if(!file||file.size<1||file.size>1048576)throw Error('Choose a .track file under 1 MB');
|
| 52 |
if(typeof DecompressionStream!=='function')throw Error('This browser cannot preview gzip tracks');
|
|
|
|
| 103 |
context.strokeStyle='#d7e8f2';context.lineWidth=Math.max(1,size*.018);
|
| 104 |
context.stroke();
|
| 105 |
if(Array.isArray(tile.connections)&&tile.connections.length>=2){
|
| 106 |
+
const [entry,exit]=tile.connections;
|
| 107 |
+
const a=connectionPoint(entry,x,y,size),z=connectionPoint(exit,x,y,size);
|
| 108 |
+
const p1=connectionControl(entry,exit,a,z),p2=connectionControl(exit,entry,z,a);
|
| 109 |
context.beginPath();context.moveTo(...a);
|
| 110 |
+
context.bezierCurveTo(...p1,...p2,...z);
|
| 111 |
context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055);
|
| 112 |
context.lineCap='round';context.stroke();
|
| 113 |
}
|
|
|
|
| 141 |
status.textContent=source+' 路 '+track.tiles.length+' pieces 路 green dot marks the saved starting point';
|
| 142 |
legend.textContent=NAMES.filter((_,i)=>counts[i]).map((name,index)=>{
|
| 143 |
const i=NAMES.indexOf(name);return name+' '+counts[i];
|
| 144 |
+
}).join(' 路 ')+' 路 Orange line follows the track centerline; collision shapes and car positions are not shown.';
|
| 145 |
}catch(error){
|
| 146 |
if(current!==version)return;
|
| 147 |
renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.';
|