File size: 4,045 Bytes
d0a5946
0820aaf
d0a5946
 
 
 
 
0820aaf
 
d0a5946
 
 
 
 
 
 
0820aaf
 
 
d0a5946
0820aaf
d0a5946
0820aaf
d0a5946
 
 
 
0820aaf
d0a5946
 
 
0820aaf
d0a5946
0820aaf
d0a5946
0820aaf
d0a5946
0820aaf
 
d0a5946
 
 
 
 
0820aaf
 
 
 
d0a5946
0820aaf
 
 
d0a5946
 
 
0820aaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0a5946
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
 * Shanghai International Circuit loader + start/grid poses.
 */

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const MAP_URL = 'assets/maps/shanghai/track.glb';

export class Track {
  constructor(scene) {
    this.scene = scene;
    this.root = new THREE.Group();
    scene.add(this.root);
    this._loader = new GLTFLoader();
    this.mapId = null;
    this.ready = false;
    this.start = { x: 0, y: 0.4, z: 0, yaw: 0 };
    this._bbox = null;

    // Temporary ground until map loads
    const ground = new THREE.Mesh(
      new THREE.PlaneGeometry(200, 200),
      new THREE.MeshStandardMaterial({ color: 0x2a2622, roughness: 0.95 }),
    );
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    ground.name = '__placeholder';
    this.root.add(ground);
  }

  async loadShanghai() {
    try {
      const gltf = await this._loader.loadAsync(MAP_URL);
      while (this.root.children.length) {
        this.root.remove(this.root.children[0]);
      }
      const model = gltf.scene;
      model.traverse((o) => {
        if (o.isMesh) {
          o.castShadow = true;
          o.receiveShadow = true;
        }
      });
      this.root.add(model);
      this.mapId = 'shanghai';
      this.ready = true;
      this._deriveStartFromMap(model);
      return true;
    } catch (err) {
      console.warn('map load failed', err);
      this.ready = false;
      return false;
    }
  }

  /**
   * Find start/finish / pit / grid cues in node names; else use asphalt AABB heuristic.
   * Shanghai SIC start/finish sits near the pit straight — we prefer named nodes,
   * then fall back to the longest thin asphalt-ish bounds.
   */
  _deriveStartFromMap(model) {
    const nameHits = [];
    const meshes = [];
    model.updateWorldMatrix(true, true);
    model.traverse((o) => {
      if (!o.isMesh) return;
      meshes.push(o);
      const n = `${o.name || ''} ${o.parent?.name || ''}`.toLowerCase();
      if (
        n.includes('start') ||
        n.includes('finish') ||
        n.includes('grid') ||
        n.includes('pit') ||
        n.includes('s/f') ||
        n.includes('sf_')
      ) {
        nameHits.push(o);
      }
    });

    let anchor = null;
    let yaw = 0;

    if (nameHits.length) {
      const box = new THREE.Box3();
      for (const m of nameHits) box.expandByObject(m);
      const c = box.getCenter(new THREE.Vector3());
      const size = box.getSize(new THREE.Vector3());
      anchor = c;
      // Face along the longer horizontal axis of the start strip.
      yaw = size.x >= size.z ? 0 : Math.PI / 2;
    } else {
      // Fallback: whole-map center shifted toward a long strip (pit straight guess).
      const box = new THREE.Box3().setFromObject(model);
      this._bbox = box;
      const c = box.getCenter(new THREE.Vector3());
      const size = box.getSize(new THREE.Vector3());
      // Shanghai-like: start straight often near one end of the long axis.
      if (size.x >= size.z) {
        anchor = new THREE.Vector3(c.x + size.x * 0.12, box.min.y, c.z);
        yaw = 0;
      } else {
        anchor = new THREE.Vector3(c.x, box.min.y, c.z + size.z * 0.12);
        yaw = Math.PI / 2;
      }
    }

    const groundY = (() => {
      const box = new THREE.Box3().setFromObject(model);
      return box.min.y;
    })();

    this.start = {
      x: anchor.x,
      y: groundY + 0.35,
      z: anchor.z,
      yaw,
    };
  }

  /** Grid slot relative to start line (GTA-style staggered pairs). */
  getGridPose(index = 0) {
    const i = Math.max(0, index | 0);
    const row = Math.floor(i / 2);
    const side = i % 2 === 0 ? -1 : 1;
    const lateral = side * 3.2;
    const back = -row * 7.5;
    const yaw = this.start.yaw;
    const fx = Math.sin(yaw);
    const fz = Math.cos(yaw);
    const rx = Math.cos(yaw);
    const rz = -Math.sin(yaw);
    return {
      x: this.start.x + fx * back + rx * lateral,
      y: this.start.y,
      z: this.start.z + fz * back + rz * lateral,
      yaw,
    };
  }
}