UNPKG

r2-navigator-js

Version:

Readium 2 'navigator' for NodeJS (TypeScript)

516 lines 20.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VERBOSE = void 0; exports.DOMRectListToArray = DOMRectListToArray; exports.getTextClientRects = getTextClientRects; exports.getClientRectsNoOverlap = getClientRectsNoOverlap; exports.rectIntersect = rectIntersect; exports.rectSubtract = rectSubtract; exports.rectSame = rectSame; exports.rectContainsPoint = rectContainsPoint; exports.rectContains = rectContains; exports.getBoundingRect = getBoundingRect; exports.rectsTouchOrOverlap = rectsTouchOrOverlap; exports.mergeTouchingRects = mergeTouchingRects; exports.replaceOverlapingRects = replaceOverlapingRects; exports.getRectOverlapX = getRectOverlapX; exports.getRectOverlapY = getRectOverlapY; exports.removeContainedRects = removeContainedRects; exports.checkOverlaps = checkOverlaps; exports.VERBOSE = false; const IS_DEV = exports.VERBOSE && (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev"); const LOG_PREFIX = "RECTs -- "; const logRect = (rect) => { const LOG_PREFIX_LOCAL = "logRect ~~ "; if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `TOP:${rect.top} BOTTOM:${rect.bottom} LEFT:${rect.left} RIGHT:${rect.right} WIDTH:${rect.width} HEIGHT:${rect.height}`); } }; function DOMRectListToArray(domRects) { const rects = []; for (const domRect of domRects) { rects.push({ bottom: domRect.bottom, height: domRect.height, left: domRect.left, right: domRect.right, top: domRect.top, width: domRect.width, }); } return rects; } function getTextClientRects(range, elementNamesToSkip) { const doc = range.commonAncestorContainer.ownerDocument; if (!doc) { return []; } const iter = doc.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { var _a; if (node.nodeType === Node.TEXT_NODE && range.intersectsNode(node)) { if (!elementNamesToSkip) { return NodeFilter.FILTER_ACCEPT; } const parentName = (_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.nodeName.toLowerCase(); if (!parentName || !elementNamesToSkip.includes(parentName)) { return NodeFilter.FILTER_ACCEPT; } } return NodeFilter.FILTER_REJECT; }, }); const rects = []; while (iter.nextNode()) { const r = doc.createRange(); if (iter.referenceNode.nodeValue && iter.referenceNode === range.startContainer) { r.setStart(iter.referenceNode, range.startOffset); r.setEnd(iter.referenceNode, iter.referenceNode === range.endContainer ? range.endOffset : iter.referenceNode.nodeValue.length); } else if (iter.referenceNode.nodeValue && iter.referenceNode === range.endContainer) { r.setStart(iter.referenceNode, 0); r.setEnd(iter.referenceNode, range.endOffset); } else { r.selectNode(iter.referenceNode); } if (r.collapsed) { continue; } const nodeRects = DOMRectListToArray(r.getClientRects()); rects.push(...nodeRects); } return rects; } function getClientRectsNoOverlap(originalRects, doNotMergeAlignedRects, vertical, expand) { const LOG_PREFIX_LOCAL = "getClientRectsNoOverlap ~~ "; if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "original number of rects = " + originalRects.length); } if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "expand = " + expand); } const ex = expand ? expand : 0; if (ex) { for (const rect of originalRects) { rect.left -= ex; rect.top -= ex; rect.right += ex; rect.bottom += ex; rect.width += (2 * ex); rect.height += (2 * ex); } } const rectsLandscapeAspectRatio = originalRects.filter((r) => { return r.width >= r.height; }); const rectsPortraitAspectRatio = originalRects.filter((r) => { return r.width < r.height; }); const sortFunc = (r1, r2) => { const areaR1 = r1.width * r1.height; const areaR2 = r2.width * r2.height; return areaR1 < areaR2 ? -1 : areaR1 === areaR2 ? 0 : 1; }; rectsLandscapeAspectRatio.sort(sortFunc); rectsPortraitAspectRatio.sort(sortFunc); originalRects = vertical ? rectsPortraitAspectRatio.concat(rectsLandscapeAspectRatio) : rectsLandscapeAspectRatio.concat(rectsPortraitAspectRatio); const tolerance = 3; if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "tolerance = " + tolerance); } const mergedRects = mergeTouchingRects(originalRects, tolerance, doNotMergeAlignedRects, vertical); if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "after [mergeTouchingRects], number of rects = " + mergedRects.length); } const noContainedRects = removeContainedRects(mergedRects, tolerance, doNotMergeAlignedRects, vertical); if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "after [removeContainedRects], number of rects = " + noContainedRects.length); } const newRects = replaceOverlapingRects(noContainedRects, doNotMergeAlignedRects, vertical); if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "after [replaceOverlapingRects], number of rects = " + newRects.length); } const minArea = 2 * 2; for (let j = newRects.length - 1; j >= 0; j--) { const rect = newRects[j]; let bigEnough = (rect.width * rect.height) > minArea; if (bigEnough && ex && (rect.width <= ex || rect.height <= ex)) { bigEnough = false; } if (!bigEnough) { if (newRects.length > 1) { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "removed small"); } newRects.splice(j, 1); } else { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "removed all smalls, but must keep last small one otherwise array empty!"); } break; } } } if (IS_DEV) { checkOverlaps(newRects); } if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `total reduction ${originalRects.length} --> ${newRects.length}`); for (const r of newRects) { logRect(r); } } return newRects; } function almostEqual(a, b, tolerance) { return Math.abs(a - b) <= tolerance; } function rectIntersect(rect1, rect2) { const maxLeft = Math.max(rect1.left, rect2.left); const minRight = Math.min(rect1.right, rect2.right); const maxTop = Math.max(rect1.top, rect2.top); const minBottom = Math.min(rect1.bottom, rect2.bottom); const rect = { bottom: minBottom, height: Math.max(0, minBottom - maxTop), left: maxLeft, right: minRight, top: maxTop, width: Math.max(0, minRight - maxLeft), }; return rect; } function rectSubtract(rect1, rect2) { const rectIntersected = rectIntersect(rect2, rect1); if (rectIntersected.height === 0 || rectIntersected.width === 0) { return [rect1]; } const rects = []; { const rectA = { bottom: rect1.bottom, height: 0, left: rect1.left, right: rectIntersected.left, top: rect1.top, width: 0, }; rectA.width = rectA.right - rectA.left; rectA.height = rectA.bottom - rectA.top; if (rectA.height !== 0 && rectA.width !== 0) { rects.push(rectA); } } { const rectB = { bottom: rectIntersected.top, height: 0, left: rectIntersected.left, right: rectIntersected.right, top: rect1.top, width: 0, }; rectB.width = rectB.right - rectB.left; rectB.height = rectB.bottom - rectB.top; if (rectB.height !== 0 && rectB.width !== 0) { rects.push(rectB); } } { const rectC = { bottom: rect1.bottom, height: 0, left: rectIntersected.left, right: rectIntersected.right, top: rectIntersected.bottom, width: 0, }; rectC.width = rectC.right - rectC.left; rectC.height = rectC.bottom - rectC.top; if (rectC.height !== 0 && rectC.width !== 0) { rects.push(rectC); } } { const rectD = { bottom: rect1.bottom, height: 0, left: rectIntersected.right, right: rect1.right, top: rect1.top, width: 0, }; rectD.width = rectD.right - rectD.left; rectD.height = rectD.bottom - rectD.top; if (rectD.height !== 0 && rectD.width !== 0) { rects.push(rectD); } } return rects; } function rectSame(rect1, rect2, tolerance) { return almostEqual(rect1.left, rect2.left, tolerance) && almostEqual(rect1.right, rect2.right, tolerance) && almostEqual(rect1.top, rect2.top, tolerance) && almostEqual(rect1.bottom, rect2.bottom, tolerance); } function rectContainsPoint(rect, x, y, tolerance) { return (rect.left < x || almostEqual(rect.left, x, tolerance)) && (rect.right > x || almostEqual(rect.right, x, tolerance)) && (rect.top < y || almostEqual(rect.top, y, tolerance)) && (rect.bottom > y || almostEqual(rect.bottom, y, tolerance)); } function rectContains(rect1, rect2, tolerance) { return (rectContainsPoint(rect1, rect2.left, rect2.top, tolerance) && rectContainsPoint(rect1, rect2.right, rect2.top, tolerance) && rectContainsPoint(rect1, rect2.left, rect2.bottom, tolerance) && rectContainsPoint(rect1, rect2.right, rect2.bottom, tolerance)); } function getBoundingRect(rect1, rect2) { const left = Math.min(rect1.left, rect2.left); const right = Math.max(rect1.right, rect2.right); const top = Math.min(rect1.top, rect2.top); const bottom = Math.max(rect1.bottom, rect2.bottom); return { bottom, height: bottom - top, left, right, top, width: right - left, }; } function rectsTouchOrOverlap(rect1, rect2, tolerance) { return ((rect1.left < rect2.right || (tolerance >= 0 && almostEqual(rect1.left, rect2.right, tolerance))) && (rect2.left < rect1.right || (tolerance >= 0 && almostEqual(rect2.left, rect1.right, tolerance))) && (rect1.top < rect2.bottom || (tolerance >= 0 && almostEqual(rect1.top, rect2.bottom, tolerance))) && (rect2.top < rect1.bottom || (tolerance >= 0 && almostEqual(rect2.top, rect1.bottom, tolerance)))); } function mergeTouchingRects(rects, tolerance, doNotMergeAlignedRects, vertical) { const LOG_PREFIX_LOCAL = "mergeTouchingRects ~~ "; for (let i = 0; i < rects.length; i++) { for (let j = i + 1; j < rects.length; j++) { const rect1 = rects[i]; const rect2 = rects[j]; if (rect1 === rect2) { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "rect1 === rect2 ??!"); } continue; } const rectsLineUpVertically = almostEqual(rect1.top, rect2.top, tolerance) && almostEqual(rect1.bottom, rect2.bottom, tolerance); const mergeAllowedForVerticallyLinedUpRects = !doNotMergeAlignedRects || !vertical; const rectsLineUpHorizontally = almostEqual(rect1.left, rect2.left, tolerance) && almostEqual(rect1.right, rect2.right, tolerance); const mergeAllowedForHorizontallyLinedUpRects = !doNotMergeAlignedRects || vertical; const doMerge = ((rectsLineUpVertically && !rectsLineUpHorizontally) || (!rectsLineUpVertically && rectsLineUpHorizontally)) && ((rectsLineUpHorizontally && mergeAllowedForHorizontallyLinedUpRects) || (rectsLineUpVertically && mergeAllowedForVerticallyLinedUpRects)) && rectsTouchOrOverlap(rect1, rect2, tolerance); if (doMerge) { const newRects = rects.filter((rect) => { return rect !== rect1 && rect !== rect2; }); const boundingRect = getBoundingRect(rect1, rect2); newRects.push(boundingRect); if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `merged ${rects.length} ==> ${newRects.length}, VERTICAL ALIGN: ${rectsLineUpVertically} HORIZONTAL ALIGN: ${rectsLineUpHorizontally} (DO NOT MERGE: ${doNotMergeAlignedRects}, VERTICAL: ${vertical}) `); logRect(rect1); console.log("+"); logRect(rect2); console.log("="); logRect(boundingRect); } return mergeTouchingRects(newRects, tolerance, doNotMergeAlignedRects, vertical); } } } return rects; } function replaceOverlapingRects(rects, doNotMergeAlignedRects, vertical) { const LOG_PREFIX_LOCAL = "replaceOverlapingRects ~~ "; if (doNotMergeAlignedRects) { return rects; } for (let i = 0; i < rects.length; i++) { for (let j = i + 1; j < rects.length; j++) { const rect1 = rects[i]; const rect2 = rects[j]; if (rect1 === rect2) { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "rect1 === rect2 ??!"); } continue; } if (!rectsTouchOrOverlap(rect1, rect2, -1)) { continue; } let toAdd = []; let toRemove; let toPreserve; let n = 0; const subtractRects1 = rectSubtract(rect1, rect2); if (subtractRects1.length === 1) { n = 1; toAdd = subtractRects1; toRemove = rect1; toPreserve = rect2; } else { const subtractRects2 = rectSubtract(rect2, rect1); if (subtractRects1.length < subtractRects2.length) { n = 2; toAdd = subtractRects1; toRemove = rect1; toPreserve = rect2; } else { n = 3; toAdd = subtractRects2; toRemove = rect2; toPreserve = rect1; } } if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `overlap ${n} ADD: ${toAdd.length}`); for (const r of toAdd) { logRect(r); } console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `overlap ${n} REMOVE:`); logRect(toRemove); console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `overlap ${n} KEEP:`); logRect(toPreserve); } if (IS_DEV) { const toCheck = []; toCheck.push(toPreserve); toCheck.push(...toAdd); checkOverlaps(toCheck); } const newRects = rects.filter((rect) => { return rect !== toRemove; }); newRects.push(...toAdd); if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `overlap removed: ${rects.length} ==> ${newRects.length}`); } return replaceOverlapingRects(newRects, doNotMergeAlignedRects, vertical); } } return rects; } function getRectOverlapX(rect1, rect2) { return Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left)); } function getRectOverlapY(rect1, rect2) { return Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top)); } function removeContainedRects(rects, tolerance, doNotMergeAlignedRects, vertical) { const LOG_PREFIX_LOCAL = "removeContainedRects ~~ "; const rectsToKeep = new Set(rects); for (const rect of rects) { const bigEnough = rect.width > 1 && rect.height > 1; if (!bigEnough) { if (IS_DEV) { console.log(LOG_PREFIX + "removed tiny:"); logRect(rect); } rectsToKeep.delete(rect); continue; } for (const possiblyContainingRect of rects) { if (rect === possiblyContainingRect) { continue; } if (!rectsToKeep.has(possiblyContainingRect) || !rectsToKeep.has(rect)) { continue; } if (!rectContains(possiblyContainingRect, rect, tolerance)) { continue; } if (doNotMergeAlignedRects) { const rectsLineUpVertically = almostEqual(possiblyContainingRect.top, rect.top, tolerance) && almostEqual(possiblyContainingRect.bottom, rect.bottom, tolerance); const rectsLineUpHorizontally = almostEqual(possiblyContainingRect.left, rect.left, tolerance) && almostEqual(possiblyContainingRect.right, rect.right, tolerance); if (rectsLineUpVertically && rectsLineUpHorizontally) { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "[identical] removed container (keep contained):"); logRect(possiblyContainingRect); logRect(rect); } rectsToKeep.delete(possiblyContainingRect); continue; } else if (rectsLineUpVertically || rectsLineUpHorizontally) { if (rectsLineUpVertically && !vertical || rectsLineUpHorizontally && vertical) { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "[aligned] removed contained (keep container):"); logRect(rect); logRect(possiblyContainingRect); } rectsToKeep.delete(rect); continue; } continue; } } else { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "[merge yes] removed contained (keep container):"); logRect(rect); logRect(possiblyContainingRect); } rectsToKeep.delete(rect); } continue; } } return Array.from(rectsToKeep); } function checkOverlaps(rects) { const LOG_PREFIX_LOCAL = "checkOverlaps ~~ "; const stillOverlapingRects = []; for (const rect1 of rects) { for (const rect2 of rects) { if (rect1 === rect2) { continue; } const has1 = stillOverlapingRects.includes(rect1); const has2 = stillOverlapingRects.includes(rect2); if (!has1 || !has2) { if (rectsTouchOrOverlap(rect1, rect2, -1)) { if (!has1) { stillOverlapingRects.push(rect1); } if (!has2) { stillOverlapingRects.push(rect2); } console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "RECT 1:"); logRect(rect1); console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + "RECT 2:"); logRect(rect2); const xOverlap = getRectOverlapX(rect1, rect2); console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `X overlap: ${xOverlap}`); const yOverlap = getRectOverlapY(rect1, rect2); console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `Y overlap: ${yOverlap}`); } } } } if (stillOverlapingRects.length) { if (IS_DEV) { console.log(LOG_PREFIX + LOG_PREFIX_LOCAL + `still overlaping = ${stillOverlapingRects.length}`); } } } //# sourceMappingURL=rect-utils.js.map