@@ -111,6 +111,7 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
111111 private var lastLaneLODScale : Float ?
112112 private var vehicleInstanceBuffer : MTLBuffer ?
113113 private var vehicleInstanceCount = 0
114+ private var vehicleInstanceCapacity = 0
114115 private var lastVehicleLODScale : Float ?
115116 private var lastDragLocation : CGPoint ?
116117 private var mouseDownLocation : CGPoint ?
@@ -524,7 +525,7 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
524525 encoder. setRenderPipelineState ( pipeline)
525526 encoder. setVertexBuffer ( vehicleInstanceBuffer, offset: 0 , index: 0 )
526527 encoder. setVertexBytes ( & uniforms, length: MemoryLayout< LaneViewportUniforms> . stride, index: 1 )
527- encoder. drawPrimitives ( type: . triangle, vertexStart: 0 , vertexCount: 3 , instanceCount: vehicleInstanceCount)
528+ encoder. drawPrimitives ( type: . triangle, vertexStart: 0 , vertexCount: 9 , instanceCount: vehicleInstanceCount)
528529 }
529530
530531 encoder. endEncoding ( )
@@ -1141,8 +1142,15 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
11411142 private func updateVehicleBuffer( samples: [ VehicleRenderSample ] , scale: Float ? = nil ) {
11421143 let renderScale = scale ?? currentRenderScale ( )
11431144 lastVehicleLODScale = renderScale
1144- guard let device, samples . isEmpty == false else {
1145+ guard let device else {
11451146 vehicleInstanceBuffer = nil
1147+ vehicleInstanceCount = 0
1148+ vehicleInstanceCapacity = 0
1149+ metalView. setNeedsDisplay ( bounds)
1150+ return
1151+ }
1152+
1153+ guard samples. isEmpty == false , let size = RenderLOD . vehicleScreenSize ( scale: renderScale) else {
11461154 vehicleInstanceCount = 0
11471155 metalView. setNeedsDisplay ( bounds)
11481156 return
@@ -1151,9 +1159,6 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
11511159 var instances : [ VehicleInstance ] = [ ]
11521160 instances. reserveCapacity ( samples. count)
11531161 for vehicle in samples {
1154- guard let size = RenderLOD . vehicleScreenSize ( scale: renderScale) else {
1155- continue
1156- }
11571162 let isSelected = vehicle. id == selectedVehicleID || selectedVehicleIDs. contains ( vehicle. id)
11581163 let color = isSelected ? selectedVehicleColor ( ) : vehicleColor ( for: vehicle)
11591164 instances. append ( VehicleInstance (
@@ -1164,12 +1169,25 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
11641169 }
11651170 vehicleInstanceCount = instances. count
11661171 guard instances. isEmpty == false else {
1167- vehicleInstanceBuffer = nil
11681172 metalView. setNeedsDisplay ( bounds)
11691173 return
11701174 }
1171- vehicleInstanceBuffer = instances. withUnsafeBytes { bytes in
1172- device. makeBuffer ( bytes: bytes. baseAddress!, length: bytes. count, options: . storageModeShared)
1175+
1176+ if vehicleInstanceBuffer == nil || vehicleInstanceCapacity < instances. count {
1177+ let nextCapacity = max ( instances. count, max ( vehicleInstanceCapacity * 2 , 256 ) )
1178+ vehicleInstanceBuffer = device. makeBuffer (
1179+ length: nextCapacity * MemoryLayout< VehicleInstance> . stride,
1180+ options: . storageModeShared
1181+ )
1182+ vehicleInstanceCapacity = nextCapacity
1183+ }
1184+
1185+ if let vehicleInstanceBuffer {
1186+ instances. withUnsafeBytes { bytes in
1187+ if let baseAddress = bytes. baseAddress {
1188+ vehicleInstanceBuffer. contents ( ) . copyMemory ( from: baseAddress, byteCount: bytes. count)
1189+ }
1190+ }
11731191 }
11741192 metalView. setNeedsDisplay ( bounds)
11751193 }
@@ -1419,6 +1437,56 @@ enum RenderLOD {
14191437 }
14201438}
14211439
1440+ enum VehicleHeading {
1441+ static func screenRadians( sumoDegrees: Float , viewportRotationRadians: Float ) -> Float {
1442+ ( ( sumoDegrees - 90 ) * . pi / 180 ) - viewportRotationRadians
1443+ }
1444+
1445+ static func sumoDegrees( from source: SIMD2 < Float > , to target: SIMD2 < Float > ) -> Float ? {
1446+ let delta = target - source
1447+ guard simd_length_squared ( delta) > 0.0001 else { return nil }
1448+ return normalizedDegrees ( atan2 ( delta. x, delta. y) * 180 / . pi)
1449+ }
1450+
1451+ static func interpolatedSUMODegrees(
1452+ from sourceAngle: Float ,
1453+ to targetAngle: Float ,
1454+ sourcePosition: SIMD2 < Float > ,
1455+ targetPosition: SIMD2 < Float > ,
1456+ progress: Float
1457+ ) -> Float {
1458+ let clamped = max ( 0 , min ( progress, 1 ) )
1459+ let baseAngle = mixDegrees ( sourceAngle, targetAngle, progress: clamped)
1460+ guard let movementAngle = sumoDegrees ( from: sourcePosition, to: targetPosition) else {
1461+ return baseAngle
1462+ }
1463+
1464+ let midTurnWeight = sin ( clamped * . pi)
1465+ return mixDegrees ( baseAngle, movementAngle, progress: min ( midTurnWeight * 0.65 , 0.65 ) )
1466+ }
1467+
1468+ static func mixDegrees( _ source: Float , _ target: Float , progress: Float ) -> Float {
1469+ let clamped = max ( 0 , min ( progress, 1 ) )
1470+ return normalizedDegrees ( source + shortestDeltaDegrees( from: source, to: target) * clamped)
1471+ }
1472+
1473+ static func shortestDeltaDegrees( from source: Float , to target: Float ) -> Float {
1474+ var delta = normalizedDegrees ( target) - normalizedDegrees( source)
1475+ while delta > 180 { delta -= 360 }
1476+ while delta < - 180 { delta += 360 }
1477+ return delta
1478+ }
1479+
1480+ private static func normalizedDegrees( _ degrees: Float ) -> Float {
1481+ guard degrees. isFinite else { return 0 }
1482+ var normalized = degrees. truncatingRemainder ( dividingBy: 360 )
1483+ if normalized < 0 {
1484+ normalized += 360
1485+ }
1486+ return normalized
1487+ }
1488+ }
1489+
14221490private func screenDistance( from a: CGPoint , to b: CGPoint ) -> CGFloat {
14231491 let dx = a. x - b. x
14241492 let dy = a. y - b. y
@@ -1486,13 +1554,16 @@ private struct VehicleRenderSample {
14861554
14871555 func interpolated( to target: VehicleRenderSample , progress: Float ) -> VehicleRenderSample {
14881556 let clamped = max ( 0 , min ( progress, 1 ) )
1489- var angleDelta = target. angle - angle
1490- while angleDelta > 180 { angleDelta -= 360 }
1491- while angleDelta < - 180 { angleDelta += 360 }
14921557 return VehicleRenderSample (
14931558 id: target. id,
14941559 position: position + ( target. position - position) * clamped,
1495- angle: angle + angleDelta * clamped,
1560+ angle: VehicleHeading . interpolatedSUMODegrees (
1561+ from: angle,
1562+ to: target. angle,
1563+ sourcePosition: position,
1564+ targetPosition: target. position,
1565+ progress: clamped
1566+ ) ,
14961567 speed: speed + ( target. speed - speed) * clamped,
14971568 typeID: target. typeID,
14981569 acceleration: interpolateOptional ( acceleration, target. acceleration, progress: clamped) ,
@@ -1694,15 +1765,15 @@ vertex BackgroundVertexOut backgroundVertex(
16941765 const device BackgroundRenderVertex *vertices [[buffer(0)]],
16951766 constant LaneViewportUniforms &uniforms [[buffer(1)]]
16961767) {
1697- const BackgroundRenderVertex vertex = vertices[vertexID];
1698- const float2 world = float2(vertex .position);
1768+ const BackgroundRenderVertex backgroundVertexData = vertices[vertexID];
1769+ const float2 world = float2(backgroundVertexData .position);
16991770 const float2 size = uniforms.viewport.xy;
17001771 const float2 screen = worldToScreen(world, uniforms);
17011772
17021773 BackgroundVertexOut out;
17031774 out.position = screenToClip(screen, size);
1704- out.texCoord = float2(vertex .texCoord);
1705- out.tint = vertex .tint;
1775+ out.texCoord = float2(backgroundVertexData .texCoord);
1776+ out.tint = backgroundVertexData .tint;
17061777 return out;
17071778}
17081779
@@ -1798,23 +1869,46 @@ vertex VehicleVertexOut vehicleVertex(
17981869 const VehicleInstance vehicle = vehicles[instanceID];
17991870 const float2 size = uniforms.viewport.xy;
18001871 const float2 world = vehicle.pose.xy;
1801- const float angle = (90.0 - vehicle.pose.z) * 0.01745329252 + uniforms.camera.w;
1872+ const float angle = (vehicle.pose.z - 90.0 ) * 0.01745329252 - uniforms.camera.w;
18021873 const float2 forward = float2(cos(angle), sin(angle));
18031874 const float2 right = float2(-forward.y, forward.x);
18041875 const float length = clamp(vehicle.metrics.x, 2.0, 34.0);
18051876 const float width = clamp(vehicle.metrics.y, 1.0, 14.0);
18061877 const float2 screenCenter = worldToScreen(world, uniforms);
1878+ const float2 nose = screenCenter + forward * (length * 0.58);
1879+ const float2 frontRight = screenCenter + forward * (length * 0.14) + right * (width * 0.52);
1880+ const float2 rearRight = screenCenter - forward * (length * 0.44) + right * (width * 0.42);
1881+ const float2 rearLeft = screenCenter - forward * (length * 0.44) - right * (width * 0.42);
1882+ const float2 frontLeft = screenCenter + forward * (length * 0.14) - right * (width * 0.52);
18071883
18081884 float2 screen;
18091885 switch (vertexID) {
18101886 case 0:
1811- screen = screenCenter + forward * (length * 0.58) ;
1887+ screen = nose ;
18121888 break;
18131889 case 1:
1814- screen = screenCenter - forward * (length * 0.42) - right * (width * 0.5);
1890+ screen = frontRight;
1891+ break;
1892+ case 2:
1893+ screen = frontLeft;
1894+ break;
1895+ case 3:
1896+ screen = frontRight;
1897+ break;
1898+ case 4:
1899+ screen = rearRight;
1900+ break;
1901+ case 5:
1902+ screen = rearLeft;
1903+ break;
1904+ case 6:
1905+ screen = frontRight;
1906+ break;
1907+ case 7:
1908+ screen = rearLeft;
18151909 break;
18161910 default:
1817- screen = screenCenter - forward * (length * 0.42) + right * (width * 0.5) ;
1911+ screen = frontLeft ;
18181912 break;
18191913 }
18201914
0 commit comments