1const isTouchDevice = 2 window.matchMedia( 3 '(any-hover: none) and (orientation: portrait)').matches; 4const cached = {}; 5let mapContainer; 6const mapResizeButtons = [ 7 {name: 'add-column-right', pos: 'right', place: 0}, 8 {name: 'remove-column-right', pos: 'right', place: 1}, 9 {name: 'add-row-bottom', pos: 'bottom', place: 0}, 10 {name: 'remove-row-bottom', pos: 'bottom', place: 1}, 11 {name: 'add-column-left', pos: 'left', place: 0}, 12 {name: 'remove-column-left', pos: 'left', place: 1}, 13 {name: 'add-row-top', pos: 'top', place: 0}, 14 {name: 'remove-row-top', pos: 'top', place: 1}, 15]; 16let refreshMapResizeButtonLocationsTimeout = null; 17 18function getCached(obj, fieldName) { 19 const cachedObj = cached[obj] || {}; 20 let result = cachedObj[fieldName]; 21 if (!result) { 22 result = obj[fieldName]; 23 cached[obj] = cachedObj; 24 cachedObj[fieldName] = result; 25 } 26 return result; 27} 28 29function setAndCache(obj, fieldName, value) { 30 obj[fieldName] = value; 31 if (!cached[obj]) cached[obj] = {}; 32 cached[obj][fieldName] = value; 33} 34 35function incrementAndCache(obj, fieldName, valueDiff) { 36 const oldValue = obj[fieldName]; 37 const newValue = oldValue + valueDiff; 38 setAndCache(obj, fieldName, newValue); 39} 40
41function invalidateCached(obj, fieldName) { 42 if (!cached[obj]) return; 43 cached[obj][fieldName] = null; 44} 45 46function handleResizeEvent() { 47 invalidateCached(mapContainer, 'offsetWidth'); 48 invalidateCached(mapContainer, 'offsetHeight'); 49} 50 51function handleKeyDownEvent(keyDownEvent) { 52 if (state.dialog) { 53 switch (keyDownEvent.key) { 54 case 'Escape': state.dialog.cancel(); break; 55 case 'Enter': state.dialog.accept(); break; 56 default: 57 // Ignore. 58 return; 59 } 60 } 61 if (state.isReadOnly()) return; 62 if (keyDownEvent.ctrlKey) { 63 switch (keyDownEvent.key) { 64 case 'z': 65 state.opCenter.undo(); 66 state.opCenter.recordOperationComplete(); 67 keyDownEvent.preventDefault(); 68 break; 69 case 'y': 70 state.opCenter.redo(); 71 state.opCenter.recordOperationComplete(); 72 keyDownEvent.preventDefault(); 73 break; 74 case 'x': 75 if (state.gesture instanceof SelectGesture) { 76 state.gesture.cut(); 77 keyDownEvent.preventDefault(); 78 } 79 break; 80 case 'c': 81 if (state.gesture instanceof SelectGesture) { 82 state.gesture.copy(); 83 keyDownEvent.preventDefault(); 84 } 85 break; 86 case 'v': { 87 state.gesture = new PasteGesture(); 88 keyDownEvent.preventDefault(); 89 break; 90 } 91 } 92 } else { 93 switch (keyDownEvent.key) { 94 case 'Escape': { 95 state.menu.setToInitialSelection(); 96 keyDownEvent.preventDefault(); 97 break; 98 } 99 case 'Delete': 100 if (state.gesture instanceof SelectGesture) { 101 state.gesture.deleteSelection(); 102 keyDownEvent.preventDefault(); 103 } 104 break; 105 default: 106 // Delegate this to the menu. 107 state.menu.keydown(keyDownEvent.key); 108 break; 109 } 110 } 111} 112 113function updateMapTransform(shouldRefreshMapResizeButtonLocations) { 114 const nav = state.navigation; 115 const theMap = document.getElementById('theMap'); 116 theMap.style.transform = `scale(${nav.scale})`; 117 // For proper container sizing: 118 invalidateCached(mapContainer, 'offsetWidth'); 119 invalidateCached(mapContainer, 'offsetHeight'); 120 const mapWidth = getCached(mapContainer, 'offsetWidth'); 121 const mapHeight = getCached(mapContainer, 'offsetHeight'); 122 const mapFrame = document.getElementById('mapFrame'); 123 mapFrame.style.width = 124 (state.theMap.mapWidth * nav.scale + mapWidth) + 'px'; 125 mapFrame.style.height = 126 (state.theMap.mapHeight * nav.scale + mapHeight) + 'px'; 127 theMap.style.left = (mapWidth / 2) + 'px'; 128 theMap.style.top = (mapHeight / 2) + 'px'; 129 if (shouldRefreshMapResizeButtonLocations) { 130 refreshMapResizeButtonLocations(); 131 } 132} 133 134let useWheelForZooming = true; 135function handleWheelEvent(wheelEvent) { 136 wheelEvent.preventDefault(); 137 if (wheelEvent.deltaX > 0 || wheelEvent.ctrlKey) { 138 // The first time a horizontal scroll is encountered, or the first time 139 // ctrl is held during a scroll, we assume trackpad / touchpad and stop 140 // using wheel for zooming. 141 useWheelForZooming = false; 142 } 143 if (useWheelForZooming || wheelEvent.ctrlKey) { 144 zoom(wheelEvent, !useWheelForZooming); 145 } else { 146 pan(-wheelEvent.deltaX, -wheelEvent.deltaY); 147 } 148} 149 150function zoom(wheelEvent, incremental = false) { 151 const nav = state.navigation; 152 153 // Calculate distance from mapContainer edges. 154 let distanceFromLeft = wheelEvent.x + getCached(mapContainer, 'scrollLeft'); 155 let distanceFromTop = wheelEvent.y + getCached(mapContainer, 'scrollTop'); 156 distanceFromLeft -= getCached(mapContainer, 'offsetWidth') / 2; 157 distanceFromTop -= getCached(mapContainer, 'offsetHeight') / 2; 158 159 let scaleDiff = 1.0; 160 if (wheelEvent.deltaY > 0 && nav.scale > 0.3) { 161 scaleDiff = -0.2; 162 } else if (wheelEvent.deltaY < 0 && nav.scale < 5.9) { 163 scaleDiff = 0.2; 164 } else { 165 return; 166 } 167 if (incremental) { 168 scaleDiff *= 0.3 * nav.scale; 169 } 170 const growth = scaleDiff / nav.scale; 171 nav.scale += scaleDiff; 172 173 pan(-growth * distanceFromLeft, -growth * distanceFromTop); 174 updateMapTransform(true); 175} 176 177function handleMouseDownEvent(mouseEvent) { 178 if (mouseEvent.buttons == 4) { 179 // Don't propagate middle mouse click, to avoid Chrome's "mouse nav" mode. 180 mouseEvent.stopPropagation(); 181 mouseEvent.preventDefault(); 182 } 183} 184 185function handleMouseMoveEvent(mouseEvent) { 186 if (mouseEvent.movementX == 0 && mouseEvent.movementY == 0) return; 187 if (mouseEvent.buttons == 4) { 188 // Middle button is pan. 189 pan(mouseEvent.movementX, mouseEvent.movementY); 190 } 191} 192 193let scrollCallRequested = false; 194let prevCellKey = null; 195function handleScrollEvent(event) { 196 if (scrollCallRequested) return; 197 scrollCallRequested = true; 198 event.stopPropagation(); 199 window.requestAnimationFrame(() => { 200 invalidateCached(mapContainer, 'scrollLeft'); 201 invalidateCached(mapContainer, 'scrollTop'); 202 refreshMapResizeButtonLocations(); 203 scrollCallRequested = false; 204 205 if (!isTouchDevice) return; 206 const theMap = document.getElementById('theMap'); 207 invalidateCached(mapContainer, 'offsetWidth'); 208 invalidateCached(mapContainer, 'offsetHeight'); 209 const currentCellKey = getCellKey( 210 getCached(mapContainer, 'scrollLeft') + 211 getCached(mapContainer, 'offsetWidth') / 2 - 212 getCached(theMap, 'offsetLeft'), 213 getCached(mapContainer, 'scrollTop') + 214 getCached(mapContainer, 'offsetHeight') / 2 - 215 getCached(theMap, 'offsetTop')); 216 if (!prevCellKey || prevCellKey != currentCellKey) { 217 if (prevCellKey) { 218 const prevCell = state.theMap.cells.get(prevCellKey); 219 if (prevCell && state.gesture) { 220 state.gesture.stopHover(); 221 } 222 } 223 const currentCell = state.theMap.cells.get(currentCellKey); 224 if (currentCell && state.gesture) { 225 if (isActionClicked) { 226 state.gesture.continueGesture(currentCell); 227 } else { 228 state.gesture.startHover(currentCell); 229 } 230 } 231 prevCellKey = currentCellKey; 232 } 233 }); 234} 235 236function pan(x, y) { 237 incrementAndCache(mapContainer, 'scrollLeft', -x); 238 incrementAndCache(mapContainer, 'scrollTop', -y); 239} 240 241function calcDistance(x1, y1, x2, y2) { 242 return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 243} 244 245function calcCenter(x1, y1, x2, y2) { 246 return {x: (x1 + x2) / 2, y: (y1 + y2) / 2}; 247} 248 249let currentPinch = null; 250let prevSingleTouchPos = null; 251function handleTouchStartEvent(touchEvent) { 252 if (touchEvent.touches.length == 2) { 253 if (touchEvent.touches[0].target.classList.contains('action-pane') || 254 touchEvent.touches[1].target.classList.contains('action-pane')) { 255 return; 256 } 257 currentPinch = { 258 initialDistance: 259 calcDistance( 260 touchEvent.touches[0].pageX, 261 touchEvent.touches[0].pageY, 262 touchEvent.touches[1].pageX, 263 touchEvent.touches[1].pageY), 264 center: 265 calcCenter( 266 touchEvent.touches[0].pageX, 267 touchEvent.touches[0].pageY, 268 touchEvent.touches[1].pageX, 269 touchEvent.touches[1].pageY), 270 initialScale: state.navigation.scale, 271 }; 272 } else { 273 const mapContainerTouches = getMapContainerTouches(touchEvent.touches); 274 if (touchEvent.touches.length > 1 && mapContainerTouches.length == 1) { 275 prevSingleTouchPos = { 276 x: mapContainerTouches[0].pageX, 277 y: mapContainerTouches[0].pageY, 278 }; 279 } 280 } 281} 282 283let pinchCallRequested = false; 284let panRequested = false; 285function handleTouchMoveEvent(touchEvent) { 286 if (currentPinch && touchEvent.touches.length == 2 && !pinchCallRequested) { 287 pinchCallRequested = true; 288 window.requestAnimationFrame(() => { 289 pinchCallRequested = false; 290 const center = 291 calcCenter( 292 touchEvent.touches[0].pageX, 293 touchEvent.touches[0].pageY, 294 touchEvent.touches[1].pageX, 295 touchEvent.touches[1].pageY); 296 const distance = 297 calcDistance( 298 touchEvent.touches[0].pageX, 299 touchEvent.touches[0].pageY, 300 touchEvent.touches[1].pageX, 301 touchEvent.touches[1].pageY); 302 303 // First, scroll to 0,0 304 const oldScroll = { 305 x: getCached(mapContainer, 'scrollLeft'), 306 y: getCached(mapContainer, 'scrollTop'), 307 }; 308 setAndCache(mapContainer, 'scrollLeft', 0); 309 setAndCache(mapContainer, 'scrollTop', 0); 310 console.log(oldScroll); 311 312 // Apply zoom. 313 const newScale = 314 currentPinch.initialScale * (distance / currentPinch.initialDistance); 315 //const scaleDiff = newScale - state.navigation.scale; 316 //const growth = scaleDiff / state.navigation.scale; 317 //const scaleFactor = newScale / state.navigation.scale; 318 state.navigation.scale = newScale; 319 updateMapTransform(false); 320 321 // Finally, scroll back. 322 const centerDiff = { 323 x: center.x - currentPinch.center.x, 324 y: center.y - currentPinch.center.y, 325 }; 326 setAndCache( 327 mapContainer, 'scrollLeft', 328 oldScroll.x + centerDiff.x * newScale); 329 setAndCache( 330 mapContainer, 'scrollTop',
331 oldScroll.y + centerDiff.y * newScale); 332 333 currentPinch.center = center; 334 updateMapTransform(true); 335 }); 336 } else { 337 if (!panRequested) { 338 // If exactly one touch is over the map container, use it to pan. 339 const mapContainerTouches = getMapContainerTouches(touchEvent.touches); 340 if (touchEvent.touches.length > 1 && mapContainerTouches.length == 1) { 341 panRequested = true; 342 window.requestAnimationFrame(() => { 343 const pos = { 344 x: mapContainerTouches[0].pageX, 345 y: mapContainerTouches[0].pageY, 346 }; 347 if (prevSingleTouchPos) { 348 pan(pos.x - prevSingleTouchPos.x, pos.y - prevSingleTouchPos.y); 349 } 350 prevSingleTouchPos = pos; 351 panRequested = false; 352 }); 353 } 354 } 355 } 356} 357 358function getMapContainerTouches(touchList) { 359 return Array.from(touchList).filter( 360 touch => touch.target.id == 'mapContainer' || 361 touch.target.classList.contains('grid-cell')); 362} 363 364function handleTouchEndEvent() { 365 currentPinch = null; 366 prevSingleTouchPos = null; 367} 368 369function getCellKey(x, y) { 370 const nav = state.navigation; 371 const cellSize = state.theMap.cellWidth * nav.scale; 372 const borderSize = state.theMap.dividerWidth * nav.scale; 373 const cellAndBorderSize = cellSize + borderSize; 374 375 const toRow = 376 Math.floor(y / cellAndBorderSize) + state.getProperty(pk.firstRow); 377 const fromRow = (y % cellAndBorderSize) / borderSize > 1 ? toRow : toRow - 1; 378 379 const toCol = 380 Math.floor(x / cellAndBorderSize) + state.getProperty(pk.firstColumn); 381 const fromCol = (x % cellAndBorderSize) / borderSize > 1 ? toCol : toCol - 1; 382 383 const key = fromRow != toRow || fromCol != toCol ? 384 CellMap.dividerCellKey(fromRow, fromCol, toRow, toCol) : 385 CellMap.primaryCellKey(fromRow, fromCol); 386 return key; 387} 388 389function resizeGridBy( 390 firstColumnDiff, lastColumnDiff, firstRowDiff, lastRowDiff) { 391 if (state.isReadOnly()) return; 392 // First, complete pending ops. 393 state.opCenter.recordOperationComplete(); 394 deleteContentThatWillBeOutsideTheMap( 395 firstColumnDiff, lastColumnDiff, firstRowDiff, lastRowDiff); 396 // Update the state's grid data. 397 [ 398 {diff: firstColumnDiff, prop: pk.firstColumn}, 399 {diff: lastColumnDiff, prop: pk.lastColumn}, 400 {diff: firstRowDiff, prop: pk.firstRow}, 401 {diff: lastRowDiff, prop: pk.lastRow}, 402 ].forEach(({diff, prop}) => { 403 if (diff != 0) { 404 state.setProperty(prop, state.getProperty(prop) + diff, true); 405 } 406 }); 407 // Update transform so that elements on the viewport won't move around. 408 if (firstColumnDiff != 0 || firstRowDiff != 0) { 409 // The below will trigger a scroll event, though. To prevent that event from 410 // starting a resize button refresh flow, we set the button refresh timer id 411 // here to be some invalid number, if it's not defined. 412 if (refreshMapResizeButtonLocationsTimeout == null) { 413 refreshMapResizeButtonLocationsTimeout = -1; 414 } 415 const nav = state.navigation; 416 incrementAndCache(mapContainer, 'scrollLeft', 417 -firstColumnDiff * nav.scale * 418 (state.theMap.cellWidth + state.theMap.dividerWidth)); 419 incrementAndCache(mapContainer, 'scrollTop', 420 -firstRowDiff * nav.scale * 421 (state.theMap.cellHeight + state.theMap.dividerHeight)); 422 } 423 updateMapTransform(false); 424 state.opCenter.recordOperationComplete(true); 425} 426 427function cellIsOutsideBoundaries( 428 cell, firstColumn, lastColumn, firstRow, lastRow) { 429 return cell.column < firstColumn - 0.5 || 430 cell.column >= lastColumn || 431 cell.row < firstRow - 0.5 || 432 cell.row >= lastRow; 433} 434 435function deleteContentThatWillBeOutsideTheMap( 436 firstColumnDiff, lastColumnDiff, firstRowDiff, lastRowDiff) { 437 const newFirstColumn = state.getProperty(pk.firstColumn) + firstColumnDiff;
438 const newLastColumn = state.getProperty(pk.lastColumn) + lastColumnDiff; 439 const newFirstRow = state.getProperty(pk.firstRow) + firstRowDiff; 440 const newLastRow = state.getProperty(pk.lastRow) + lastRowDiff; 441 const cellsToDelete = new Set(); 442 const cellLayersToDelete = new Map(); 443 ct.children.forEach(layer => cellLayersToDelete.set(layer, new Set())); 444 state.theMap.cells.forEach(cell => { 445 // If the cell is outside the map, delete it. 446 if (cellIsOutsideBoundaries( 447 cell, newFirstColumn, newLastColumn, newFirstRow, newLastRow)) { 448 cellsToDelete.add(cell); 449 } 450 // If the cell has an endcell which is outside the map for a given layer, 451 // delete the content for that layer. 452 ct.children.forEach(layer => { 453 const content = cell.getLayerContent(layer); 454 if (!content) return; 455 const endCellKey = content[ck.endCell]; 456 if (!endCellKey) return; 457 const endCell = state.theMap.cells.get(endCellKey); 458 if (cellIsOutsideBoundaries( 459 endCell, newFirstColumn, newLastColumn, newFirstRow, newLastRow)) { 460 cellLayersToDelete.get(layer).add(cell); 461 } 462 }); 463 }); 464 state.theMap.cells.forEach(cell => { 465 ct.children.forEach(layer => { 466 // Delete if it was deleted or its endcell was deleted. 467 let deleteLayerContent = 468 cellsToDelete.has(cell) || cellLayersToDelete.get(layer).has(cell); 469 // Delete if its startcell was deleted. 470 if (!deleteLayerContent) { 471 const content = cell.getLayerContent(layer); 472 if (content && content[ck.startCell]) { 473 const startCell = state.theMap.cells.get(content[ck.startCell]); 474 if (cellsToDelete.has(startCell) || 475 cellLayersToDelete.get(layer).has(startCell)) { 476 deleteLayerContent = true; 477 } 478 } 479 } 480 if (deleteLayerContent) { 481 cell.setLayerContent(layer, null, true); 482 } 483 }); 484 }); 485} 486 487function resetView() { 488 const nav = state.navigation; 489 nav.scale = 1.0; 490 updateMapTransform(false); 491 const theMap = document.getElementById('theMap'); 492 setAndCache(mapContainer, 'scrollLeft', theMap.clientWidth / 2); 493 setAndCache(mapContainer, 'scrollTop', theMap.clientHeight / 2); 494 updateMapTransform(true); 495} 496 497function resetGrid() { 498 if (state.isReadOnly()) return; 499 state.opCenter.recordOperationComplete(); 500 state.theMap.resetToDefault(); 501 [pk.firstColumn, pk.firstRow, pk.lastColumn, pk.lastRow].forEach(property => { 502 state.setProperty(property, null, true); 503 }); 504 resetView(); 505 // Force a map rewrite with the operation. 506 state.opCenter.recordOperationComplete(true); 507} 508 509function refreshMapResizeButtonLocations() { 510 if (refreshMapResizeButtonLocationsTimeout) { 511 clearTimeout(refreshMapResizeButtonLocationsTimeout); 512 } else { 513 mapResizeButtons.forEach(button => { 514 if (!button.element) { 515 button.element = 516 document 517 .getElementsByClassName('map-resize-button-' + button.name)[0]; 518 } 519 document.getElementById('app').classList.add('transforming-map'); 520 }); 521 } 522 refreshMapResizeButtonLocationsTimeout = setTimeout(() => { 523 const theMap = document.getElementById('theMap'); 524 document.getElementById('app').classList.remove('transforming-map'); 525 const nav = state.navigation; 526 const left = theMap.offsetLeft; 527 const right = left + (theMap.offsetWidth * nav.scale); 528 const top = theMap.offsetTop; 529 const bottom = top + (theMap.offsetHeight * nav.scale); 530 const rect = {left, right, top, bottom}; 531 mapResizeButtons.forEach(button => { 532 if (!button.element) return; 533 let x = clamp(rect.left + 70, getCached(mapContainer, 'scrollLeft') + 534 getCached(mapContainer, 'offsetWidth') / 2, rect.right - 70); 535 let y = clamp(rect.top + 70, getCached(mapContainer, 'scrollTop') + 536 getCached(mapContainer, 'offsetHeight') / 2, rect.bottom - 70); 537 let offsetX = button.place == 0 ? -70 : 40; 538 let offsetY = offsetX; 539 switch (button.pos) { 540 case 'right': x = rect.right; offsetX = 50; break; 541 case 'bottom': y = rect.bottom; offsetY = 50; break; 542 case 'left': x = rect.left; offsetX = -70; break; 543 case 'top': y = rect.top; offsetY = -70; break; 544 } 545 button.element.style.left = (x + offsetX) + 'px'; 546 button.element.style.top = (y + offsetY) + 'px'; 547 refreshMapResizeButtonLocationsTimeout = null; 548 });
549 theMap.classList.add('resized-map'); 550 setTimeout(() => { theMap.classList.remove('resized-map'); }, 100); 551 }, 1000); 552} 553 554let isActionClicked = false; 555function switchToMobileMode() { 556 const scale = 1.6; 557 const app = document.getElementById('app'); 558 app.style.transform = `scale(${scale})`; 559 app.style.width = (100 / scale) + '%'; 560 app.style.height = (100 / scale) + '%'; 561 createAndAppendDivWithClass(mapContainer, 'mobile-cursor'); 562 invalidateCached(mapContainer, 'offsetWidth'); 563 invalidateCached(mapContainer, 'offsetHeight'); 564 updateMapTransform(true); 565 const actionPane = 566 createAndAppendDivWithClass( 567 document.body, 'action-pane'); 568 actionPane.textContent = 'Tap here to Apply.'; 569 actionPane.addEventListener('touchstart', e => { 570 e.stopPropagation(); 571 if (state.gesture) { 572 isActionClicked = true; 573 state.gesture.startGesture(); 574 } 575 }); 576 actionPane.addEventListener('touchend', e => { 577 e.stopPropagation(); 578 if (state.gesture) { 579 state.gesture.stopGesture(); 580 isActionClicked = false; 581 if (prevCellKey) { 582 const prevCell = state.theMap.cells.get(prevCellKey); 583 state.gesture.startHover(prevCell); 584 } 585 } 586 }); 587 actionPane.addEventListener('touchmove', e => { e.stopPropagation(); }); 588 actionPane.onmousedown = e => e.stopPropagation(); 589 actionPane.onmousemove = e => e.stopPropagation(); 590 actionPane.onmouseup = e => e.stopPropagation(); 591} 592 593function numberRooms(kind, variation) { 594 let num = 1; 595 const roomsToNumber = []; 596 getRooms().forEach(room => { 597 const existingContent = room.centerCell.getLayerContent(ct.text); 598 if (existingContent) { 599 if (!existingContent[ck.startCell] && !existingContent[ck.endCell]) { 600 // There's already a single-cell text content at the center. Maybe 601 // that's a previous number? 602 const existingNum = Number.parseInt(existingContent[ck.text]); 603 if (existingNum != NaN) { 604 num = Math.max(num, existingNum + 1); 605 } 606 } 607 return; 608 } 609 roomsToNumber.push(room); 610 }); 611 roomsToNumber.forEach(room => { 612 room.centerCell.setLayerContent(ct.text, { 613 [ck.kind]: kind.id, 614 [ck.variation]: variation.id, 615 [ck.text]: num++, 616 }, true); 617 }); 618 state.opCenter.recordOperationComplete(); 619} 620 621function getRooms() { 622 const rooms = []; 623 state.theMap.cells.forEach(cell => { 624 if (cell.role != 'primary') return; 625 if (cell.hasLayerContent(ct.walls)) return; 626 if (rooms.some(room => room.cells.has(cell))) return; 627 const newRoom = createRoom(cell); 628 if (newRoom) rooms.push(newRoom); 629 }); 630 // Filter out rooms that contain other rooms. 631 return rooms.filter(room => !rooms.some(otherRoom => 632 room != otherRoom && room.minCol <= otherRoom.minCol && 633 room.minRow <= otherRoom.minRow && 634 room.maxCol >= otherRoom.maxCol && 635 room.maxRow >= otherRoom.maxRow)); 636} 637 638function createRoom(seed) { 639 const cells = new Set([seed]); 640 let front = new Set([seed]); 641 while (front.size > 0) { 642 const newFront = new Set(); 643 front.forEach(cell => { 644 ['top', 'bottom', 'left', 'right'].forEach(dir => { 645 const neighbors = cell.getNeighbors(dir); 646 if (!neighbors) return; 647 const neighborPrimary = neighbors.cells[0]; 648 if (!neighborPrimary || 649 neighborPrimary.hasLayerContent(ct.walls) || 650 newFront.has(neighborPrimary) || 651 cells.has(neighborPrimary)) { 652 return; 653 } 654 const neighborDivider = neighbors.dividerCell; 655 if (neighborPrimary.hasLayerContent(ct.walls)) return; 656 // Check corners 657 const cornerDirs = (dir == 'left' || dir == 'right') ? 658 ['top', 'bottom'] : ['left', 'right']; 659 if (cornerDirs 660 .map(cornerDir => neighborDivider.getNeighbor(cornerDir, true)) 661 .filter(corner => !!corner) 662 .every(corner => corner.hasLayerContent(ct.walls))) { 663 return; 664 } 665 cells.add(neighborPrimary); 666 newFront.add(neighborPrimary); 667 }); 668 });
669 front = newFront; 670 } 671 if (cells.size <= 1) return null; 672 let minRow = Number.POSITIVE_INFINITY; 673 let minCol = Number.POSITIVE_INFINITY; 674 let maxRow = Number.NEGATIVE_INFINITY; 675 let maxCol = Number.NEGATIVE_INFINITY; 676 cells.forEach(cell => { 677 minRow = Math.min(cell.row, minRow); 678 minCol = Math.min(cell.column, minCol); 679 maxRow = Math.max(cell.row, maxRow); 680 maxCol = Math.max(cell.column, minCol); 681 }); 682 // Find the cell closest to the center. 683 const centerRow = Math.floor((minRow + maxRow) / 2); 684 const centerCol = Math.floor((minCol + maxCol) / 2); 685 let centerCell = null; 686 let minDistance = Number.POSITIVE_INFINITY; 687 for (const cell of cells) { 688 const distance = Math.pow(cell.row - centerRow, 2) + 689 Math.pow(cell.column - centerCol, 2); 690 if (distance < minDistance) { 691 minDistance = distance; 692 centerCell = cell; 693 if (distance == 0) break; 694 } 695 } 696 return {cells, minRow, minCol, maxRow, maxCol, centerCell}; 697}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.