@@ -338,8 +338,13 @@ private void BuildScene(IEnumerable<ModelEntry> selection)
338338 return ;
339339 }
340340
341- // 2) Uniform grid sized to the widest / deepest castle so nothing overlaps .
341+ // 2) Uniform grid sized so neither the castles NOR their (often wider) 3D name labels overlap .
342342 float cellWidth = placements . Max ( p => p . Bounds . size . x ) + _gap ;
343+ float maxLabelScale = placements . Max ( p => Mathf . Max ( 0.15f , p . Bounds . size . x * 0.035f ) * _labelScale ) ;
344+ int longestLabel = placements . Max ( p => ( p . Model . DisplayName + " 00/100" ) . Length ) ;
345+ // Rough TextMesh advance ~2.3 world units per character at scale 1.0: widen the cell just enough
346+ // to fit the longest label so adjacent names never collide, without pushing castles far apart.
347+ cellWidth = Mathf . Max ( cellWidth , longestLabel * maxLabelScale * 2.3f + _gap ) ;
343348 float cellDepth = placements . Max ( p => p . Bounds . size . z ) + _gap ;
344349 float maxHeight = placements . Max ( p => p . Bounds . size . y ) ;
345350 int columns = Mathf . Clamp ( Mathf . Min ( _maxPerRow , placements . Count ) , 1 , placements . Count ) ;
@@ -381,6 +386,29 @@ private void BuildScene(IEnumerable<ModelEntry> selection)
381386
382387 // 3) Frame the whole comparison for the user.
383388 FrameCamera ( overall ) ;
389+
390+ // Billboard every label to actually FACE the framing camera (readable, not mirrored). A fixed
391+ // 180° Y-flip reverses the glyphs; LookRotation toward the camera keeps a correct right-vector.
392+ Camera frameCam = Camera . main ;
393+ if ( frameCam != null )
394+ {
395+ foreach ( Transform child in root . transform )
396+ {
397+ if ( ! child . name . StartsWith ( "Label_" ) )
398+ {
399+ continue ;
400+ }
401+
402+ // TextMesh glyphs read correctly when the mesh's +Z points AWAY from the viewer
403+ // (camera looks along the text's +Z). Pointing +Z at the camera mirrors the glyphs.
404+ Vector3 awayFromCam = child . position - frameCam . transform . position ;
405+ if ( awayFromCam . sqrMagnitude > 0.0001f )
406+ {
407+ child . rotation = Quaternion . LookRotation ( awayFromCam , Vector3 . up ) ;
408+ }
409+ }
410+ }
411+
384412 Selection . activeGameObject = root ;
385413 if ( SceneView . lastActiveSceneView != null )
386414 {
@@ -519,7 +547,24 @@ private sealed class ModelEntry
519547 public string [ ] CastleLabels = Array . Empty < string > ( ) ;
520548
521549 // Folder names use underscores as separators; present them as spaces for readability.
522- public string DisplayName => ModelName . Replace ( '_' , ' ' ) ;
550+ public string DisplayName
551+ {
552+ get
553+ {
554+ string s = ModelName . Replace ( '_' , ' ' ) ;
555+ // Drop the routing-provider prefix (e.g. "opencode ", "codex ") so the label shows the
556+ // MODEL, not the gateway -- shorter labels also stop long names overlapping their neighbours.
557+ foreach ( string prefix in new [ ] { "opencode " , "codex " , "zai " , "lmstudio " } )
558+ {
559+ if ( s . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) )
560+ {
561+ return s . Substring ( prefix . Length ) ;
562+ }
563+ }
564+
565+ return s ;
566+ }
567+ }
523568
524569 public void RebuildLabels ( )
525570 {
0 commit comments