@@ -74,72 +74,72 @@ function renderRow(group: RowGroup): string[] {
7474 * Rows are sorted by width ascending (pyramid shape).
7575 * Returns the best assignment as a row-index-per-block array, or null.
7676 */
77+ /** Decode a combo number into per-block row assignments and check all rows are used */
78+ function decodeAssignment (
79+ combo : number , blockCount : number , targetRows : number ,
80+ blockRow : number [ ] , rowUsed : boolean [ ] ,
81+ ) : boolean {
82+ let encoded = combo ;
83+ for ( let b = 0 ; b < blockCount ; b ++ ) {
84+ blockRow [ b ] = encoded % targetRows ;
85+ encoded = Math . floor ( encoded / targetRows ) ;
86+ }
87+ rowUsed . fill ( false , 0 , targetRows ) ;
88+ for ( let b = 0 ; b < blockCount ; b ++ ) rowUsed [ blockRow [ b ] ! ] = true ;
89+ for ( let r = 0 ; r < targetRows ; r ++ ) { if ( ! rowUsed [ r ] ) return false ; }
90+ return true ;
91+ }
92+
93+ /** Compute the total rendered width of each row from block assignments */
94+ function computeRowWidths (
95+ blockRow : number [ ] , blockCount : number , targetRows : number ,
96+ blockWidths : number [ ] , rowWidths : number [ ] ,
97+ ) : void {
98+ for ( let r = 0 ; r < targetRows ; r ++ ) rowWidths [ r ] = 0 ;
99+ for ( let b = 0 ; b < blockCount ; b ++ ) rowWidths [ blockRow [ b ] ! ] += blockWidths [ b ] ! + BOX_CHROME ;
100+ for ( let r = 0 ; r < targetRows ; r ++ ) {
101+ let count = 0 ;
102+ for ( let b = 0 ; b < blockCount ; b ++ ) { if ( blockRow [ b ] === r ) count ++ ; }
103+ if ( count > 1 ) rowWidths [ r ] += ( count - 1 ) * GAP ;
104+ }
105+ }
106+
107+ /** Check rows fit within width limits (pyramid order) and return a score, or -Infinity if invalid */
108+ function scoreAssignment (
109+ targetRows : number , rowWidths : number [ ] , row1MaxWidth : number , maxRowWidth : number ,
110+ ) : number {
111+ // Sort by width ascending — narrowest row displayed first (pyramid)
112+ const displayOrder : number [ ] = [ ] ;
113+ for ( let r = 0 ; r < targetRows ; r ++ ) displayOrder . push ( r ) ;
114+ displayOrder . sort ( ( a , b ) => rowWidths [ a ] ! - rowWidths [ b ] ! ) ;
115+
116+ for ( let pos = 0 ; pos < displayOrder . length ; pos ++ ) {
117+ const limit = pos === 0 ? row1MaxWidth : maxRowWidth ;
118+ if ( rowWidths [ displayOrder [ pos ] ! ] ! > limit ) return - Infinity ;
119+ }
120+
121+ // Row count dominates (1e9), widest row is tiebreaker (1e3)
122+ let widest = 0 ;
123+ for ( let r = 0 ; r < targetRows ; r ++ ) { if ( rowWidths [ r ] ! > widest ) widest = rowWidths [ r ] ! ; }
124+ return - ( targetRows * 1e9 ) - ( widest * 1e3 ) ;
125+ }
126+
77127export function findOptimalAssignment (
78128 blockCount : number , blockWidths : number [ ] , row1MaxWidth : number , maxRowWidth : number ,
79129) : number [ ] | null {
80130 let bestAssignment : number [ ] | null = null ;
81131 let bestScore = - Infinity ;
82132
83- // Reusable buffers to avoid per-iteration allocations
84133 const blockRow = new Array < number > ( blockCount ) ;
85134 const rowUsed = new Array < boolean > ( blockCount ) ;
86135 const rowWidths = new Array < number > ( blockCount ) ;
87136
88137 for ( let targetRows = 1 ; targetRows <= blockCount ; targetRows ++ ) {
89138 const totalCombinations = targetRows ** blockCount ;
90-
91139 for ( let combo = 0 ; combo < totalCombinations ; combo ++ ) {
92- // Decode combo into per-block row assignments (base-targetRows digits)
93- let encoded = combo ;
94- for ( let block = 0 ; block < blockCount ; block ++ ) {
95- blockRow [ block ] = encoded % targetRows ;
96- encoded = Math . floor ( encoded / targetRows ) ;
97- }
98-
99- // Verify all target rows are occupied
100- rowUsed . fill ( false , 0 , targetRows ) ;
101- for ( let block = 0 ; block < blockCount ; block ++ ) rowUsed [ blockRow [ block ] ! ] = true ;
102- let allRowsOccupied = true ;
103- for ( let row = 0 ; row < targetRows ; row ++ ) {
104- if ( ! rowUsed [ row ] ) { allRowsOccupied = false ; break ; }
105- }
106- if ( ! allRowsOccupied ) continue ;
107-
108- // Compute total width per row (block widths + chrome + gaps)
109- for ( let row = 0 ; row < targetRows ; row ++ ) rowWidths [ row ] = 0 ;
110- for ( let block = 0 ; block < blockCount ; block ++ ) {
111- rowWidths [ blockRow [ block ] ! ] += blockWidths [ block ] ! + BOX_CHROME ;
112- }
113- for ( let row = 0 ; row < targetRows ; row ++ ) {
114- let blocksInRow = 0 ;
115- for ( let block = 0 ; block < blockCount ; block ++ ) {
116- if ( blockRow [ block ] === row ) blocksInRow ++ ;
117- }
118- if ( blocksInRow > 1 ) rowWidths [ row ] += ( blocksInRow - 1 ) * GAP ;
119- }
120-
121- // Sort rows by width ascending (pyramid: narrowest on top)
122- const displayOrder : number [ ] = [ ] ;
123- for ( let row = 0 ; row < targetRows ; row ++ ) displayOrder . push ( row ) ;
124- displayOrder . sort ( ( a , b ) => rowWidths [ a ] ! - rowWidths [ b ] ! ) ;
125-
126- // Validate: narrowest row (displayed first) uses tighter width limit
127- let valid = true ;
128- for ( let pos = 0 ; pos < displayOrder . length ; pos ++ ) {
129- const widthLimit = pos === 0 ? row1MaxWidth : maxRowWidth ;
130- if ( rowWidths [ displayOrder [ pos ] ! ] ! > widthLimit ) { valid = false ; break ; }
131- }
132- if ( ! valid ) continue ;
133-
134- // Score: row count dominates (1e9 weight), widest row is tiebreaker (1e3).
135- // Higher score = better layout. Both terms are negative so fewer rows
136- // and smaller widest row both increase the score.
137- let widestRow = 0 ;
138- for ( let row = 0 ; row < targetRows ; row ++ ) {
139- if ( rowWidths [ row ] ! > widestRow ) widestRow = rowWidths [ row ] ! ;
140- }
141- const score = - ( targetRows * 1e9 ) - ( widestRow * 1e3 ) ;
142-
140+ if ( ! decodeAssignment ( combo , blockCount , targetRows , blockRow , rowUsed ) ) continue ;
141+ computeRowWidths ( blockRow , blockCount , targetRows , blockWidths , rowWidths ) ;
142+ const score = scoreAssignment ( targetRows , rowWidths , row1MaxWidth , maxRowWidth ) ;
143143 if ( score > bestScore ) {
144144 bestScore = score ;
145145 bestAssignment = blockRow . slice ( 0 , blockCount ) ;
0 commit comments