| 1176 | * maxX/maxY should already be clamped to both screen bounds by the caller. |
| 1177 | */ |
| 1178 | export function blitRegion( |
| 1179 | dst: Screen, |
| 1180 | src: Screen, |
| 1181 | regionX: number, |
| 1182 | regionY: number, |
| 1183 | maxX: number, |
| 1184 | maxY: number, |
| 1185 | ): void { |
| 1186 | regionX = Math.max(0, regionX) |
| 1187 | regionY = Math.max(0, regionY) |
| 1188 | if (regionX >= maxX || regionY >= maxY) return |
| 1189 | |
| 1190 | const rowLen = maxX - regionX |
| 1191 | const srcStride = src.width << 1 |
| 1192 | const dstStride = dst.width << 1 |
| 1193 | const rowBytes = rowLen << 1 // 2 Int32s per cell |
| 1194 | const srcCells = src.cells |
| 1195 | const dstCells = dst.cells |
| 1196 | const srcNoSel = src.noSelect |
| 1197 | const dstNoSel = dst.noSelect |
| 1198 | const srcContentEnd = src.contentEnd |
| 1199 | const dstContentEnd = dst.contentEnd |
| 1200 | |
| 1201 | // softWrap is per-row — copy the row range regardless of stride/width. |
| 1202 | // Partial-width blits still carry the row's wrap provenance since the |
| 1203 | // blitted content (a cached ink-text node) is what set the bit. |
| 1204 | dst.softWrap.set(src.softWrap.subarray(regionY, maxY), regionY) |
| 1205 | |
| 1206 | // Fast path: contiguous memory when copying full-width rows at same stride |
| 1207 | if (regionX === 0 && maxX === src.width && src.width === dst.width) { |
| 1208 | const srcStart = regionY * srcStride |
| 1209 | const totalBytes = (maxY - regionY) * srcStride |
| 1210 | dstCells.set( |
| 1211 | srcCells.subarray(srcStart, srcStart + totalBytes), |
| 1212 | srcStart, // srcStart === dstStart when strides match and regionX === 0 |
| 1213 | ) |
| 1214 | // noSelect is 1 byte/cell vs cells' 8 — same region, different scale |
| 1215 | const nsStart = regionY * src.width |
| 1216 | const nsLen = (maxY - regionY) * src.width |
| 1217 | dstNoSel.set(srcNoSel.subarray(nsStart, nsStart + nsLen), nsStart) |
| 1218 | dstContentEnd.set(srcContentEnd.subarray(regionY, maxY), regionY) |
| 1219 | } else { |
| 1220 | // Per-row copy for partial-width or mismatched-stride regions |
| 1221 | let srcRowCI = regionY * srcStride + (regionX << 1) |
| 1222 | let dstRowCI = regionY * dstStride + (regionX << 1) |
| 1223 | let srcRowNS = regionY * src.width + regionX |
| 1224 | let dstRowNS = regionY * dst.width + regionX |
| 1225 | for (let y = regionY; y < maxY; y++) { |
| 1226 | dstCells.set(srcCells.subarray(srcRowCI, srcRowCI + rowBytes), dstRowCI) |
| 1227 | dstNoSel.set(srcNoSel.subarray(srcRowNS, srcRowNS + rowLen), dstRowNS) |
| 1228 | const copiedEnd = Math.min(srcContentEnd[y] ?? 0, maxX) |
| 1229 | if (copiedEnd > regionX) { |
| 1230 | dstContentEnd[y] = Math.max(dstContentEnd[y] ?? 0, copiedEnd) |
| 1231 | } |
| 1232 | srcRowCI += srcStride |
| 1233 | dstRowCI += dstStride |
| 1234 | srcRowNS += src.width |
| 1235 | dstRowNS += dst.width |