99 * colstart: number,
1010 * rowstart: number,
1111 * rowspan: number,
12- * order: number,
13- * snapColstart?: number,
14- * snapRowstart?: number,
12+ * order: number
1513 * }} GhostDataWrite
1614 *
1715 *
@@ -120,10 +118,18 @@ export function createPositionIndex(items, rows) {
120118 }
121119 for ( const item of sorted ) {
122120 const {
123- colstart, colspan, rowstart
121+ colstart, colspan, rowstart, rowspan
124122 } = item ;
125- for ( let i = 0 ; i < colspan ; i ++ ) {
126- positionsIndex . get ( rowstart ) ?. set ( colstart + i , item . _id ) ;
123+ const height = Math . max ( 1 , rowspan || 1 ) ;
124+ for ( let r = 0 ; r < height ; r ++ ) {
125+ const row = rowstart + r ;
126+ const xIndex = positionsIndex . get ( row ) ;
127+ if ( ! xIndex ) {
128+ continue ;
129+ }
130+ for ( let i = 0 ; i < colspan ; i ++ ) {
131+ xIndex . set ( colstart + i , item . _id ) ;
132+ }
127133 }
128134 }
129135
@@ -402,6 +408,10 @@ export function getMoveChanges({
402408 item,
403409 precomp
404410} ) {
411+ // Guard against mismatched item/data identifiers when the target id exists in lookup
412+ if ( data . id && state ?. lookup ?. has ?. ( data . id ) && item ?. _id && data . id !== item . _id ) {
413+ return [ ] ;
414+ }
405415 if ( ! data . colstart || ! data . rowstart ||
406416 ( data . colstart === item . colstart && data . rowstart === item . rowstart )
407417 ) {
@@ -451,26 +461,17 @@ export function getMoveChanges({
451461 }
452462
453463 // Strategy 2: horizontal nudge of neighbours only
454- const overlapExists = true ; // we only reach here when placeIfFree is false
464+ // We only reach here when placeIfFree is false
455465 // Decide preferred nudge directions based on movement intent and edge overlaps
456466 const oldStartCol = item . colstart ;
457467 const oldEndCol = oldStartCol + ( item . colspan || 1 ) - 1 ;
458468 // Check if the target start column is empty across all spanned rows AND
459469 // lies outside the item's original horizontal footprint. This avoids
460470 // misclassifying small same-direction shifts (where newStartCol is still
461471 // within the old footprint) as the "empty start" special case.
462- const startOutsideOld = ( newStartCol < oldStartCol ) || ( newStartCol > oldEndCol ) ;
463- const targetStartEmpty = startOutsideOld && ( ( ) => {
464- for ( let r = newStartRow ; r <= newEndRow ; r ++ ) {
465- const occRow = precomp ?. occByRow ?. get ( r ) ;
466- const rowIndex = state . positions . get ( r ) ;
467- const id = ( occRow ? occRow [ newStartCol ] : rowIndex ?. get ( newStartCol ) ) ;
468- if ( id && id !== item . _id ) {
469- return false ;
470- }
471- }
472- return true ;
473- } ) ( ) ;
472+ const _startOutsideOld = ( newStartCol < oldStartCol ) || ( newStartCol > oldEndCol ) ;
473+ // Note: no special opposite-direction bias solely because the target start
474+ // cell is empty. We rely on boundary overlap/equal-edge heuristics below.
474475 // Note: vertical-only moves are handled specially below
475476 // Note: vertical-only moves are handled specially below
476477
@@ -500,11 +501,6 @@ export function getMoveChanges({
500501 if ( ! primaryDir ) {
501502 return [ 'east' , 'west' ] ;
502503 }
503- // Special rule: targeting an empty start cell but overall footprint overlaps
504- // -> attempt only the opposite direction.
505- if ( overlapExists && targetStartEmpty ) {
506- return primaryDir === 'east' ? [ 'west' ] : [ 'east' ] ;
507- }
508504 if ( ! target ) {
509505 // No concrete target: only try primary to avoid unintended cascades.
510506 return primaryDir === 'east' ? [ 'east' ] : [ 'west' ] ;
@@ -841,6 +837,8 @@ function attemptHorizontalNudge({
841837 // No shift for this item; it defines the next available start.
842838 neededStart = Math . max ( neededStart , end0 + 1 ) ;
843839 }
840+ // No need to fail if neededStart exceeds grid after processing;
841+ // we already enforce bounds on each neighbor shift.
844842 }
845843 } else {
846844 // dir === 'west'
@@ -862,9 +860,8 @@ function attemptHorizontalNudge({
862860 // No shift; it defines the next available end.
863861 neededEnd = Math . min ( neededEnd , start0 - 1 ) ;
864862 }
865- if ( neededEnd < 0 ) {
866- return null ;
867- }
863+ // Similarly, don't fail solely due to neededEnd becoming < 0
864+ // after processing; individual shifts are already bounded.
868865 }
869866 }
870867
0 commit comments