From 37a9a747f7677fa05e3ddf5669c0379aa65c1e39 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Mon, 20 Jun 2022 00:18:29 -0300 Subject: [PATCH] feat: exclude rects on textkit layout container (#1895) --- .changeset/long-years-cry.md | 5 + .../textkit/src/layout/generateLineRects.js | 54 ++++++++ .../textkit/src/layout/layoutParagraph.js | 20 ++- packages/textkit/src/rect/crop.js | 8 +- packages/textkit/src/rect/intersects.js | 17 +++ packages/textkit/src/rect/partition.js | 12 ++ .../textkit/tests/rect/intersects.test.js | 115 ++++++++++++++++++ packages/textkit/tests/rect/partition.test.js | 23 ++++ 8 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 .changeset/long-years-cry.md create mode 100644 packages/textkit/src/layout/generateLineRects.js create mode 100644 packages/textkit/src/rect/intersects.js create mode 100644 packages/textkit/src/rect/partition.js create mode 100644 packages/textkit/tests/rect/intersects.test.js create mode 100644 packages/textkit/tests/rect/partition.test.js diff --git a/.changeset/long-years-cry.md b/.changeset/long-years-cry.md new file mode 100644 index 000000000..d48666bcb --- /dev/null +++ b/.changeset/long-years-cry.md @@ -0,0 +1,5 @@ +--- +'@react-pdf/textkit': minor +--- + +feat: exclude rects on textkit layout container diff --git a/packages/textkit/src/layout/generateLineRects.js b/packages/textkit/src/layout/generateLineRects.js new file mode 100644 index 000000000..4e389b298 --- /dev/null +++ b/packages/textkit/src/layout/generateLineRects.js @@ -0,0 +1,54 @@ +import intersects from '../rect/intersects'; +import partition from '../rect/partition'; + +const getLineFragment = (lineRect, excludeRect) => { + if (!intersects(excludeRect, lineRect)) return [lineRect]; + + const eStart = excludeRect.x; + const eEnd = excludeRect.x + excludeRect.width; + const lStart = lineRect.x; + const lEnd = lineRect.x + lineRect.width; + + const a = Object.assign({}, lineRect, { width: eStart - lStart }); + const b = Object.assign({}, lineRect, { x: eEnd, width: lEnd - eEnd }); + + return [a, b].filter(r => r.width > 0); +}; + +const getLineFragments = (rect, excludeRects) => { + let fragments = [rect]; + + for (let i = 0; i < excludeRects.length; i += 1) { + const excludeRect = excludeRects[i]; + + fragments = fragments.reduce((acc, fragment) => { + const pieces = getLineFragment(fragment, excludeRect); + return acc.concat(pieces); + }, []); + } + + return fragments; +}; + +const generateLineRects = (container, height) => { + const { excludeRects, ...rect } = container; + + if (!excludeRects) return [rect]; + + const lineRects = []; + const maxY = Math.max(...excludeRects.map(r => r.y + r.height)); + + let currentRect = rect; + + while (currentRect.y < maxY) { + const [lineRect, rest] = partition(currentRect, height); + const lineRectFragments = getLineFragments(lineRect, excludeRects); + + currentRect = rest; + lineRects.push(...lineRectFragments); + } + + return [...lineRects, currentRect]; +}; + +export default generateLineRects; diff --git a/packages/textkit/src/layout/layoutParagraph.js b/packages/textkit/src/layout/layoutParagraph.js index 4005bbad4..73f0e6a41 100644 --- a/packages/textkit/src/layout/layoutParagraph.js +++ b/packages/textkit/src/layout/layoutParagraph.js @@ -1,5 +1,6 @@ import omit from '../run/omit'; import stringHeight from '../attributedString/height'; +import generateLineRects from './generateLineRects'; const ATTACHMENT_CODE = '\ufffc'; // 65532 @@ -26,7 +27,8 @@ const purgeAttachments = attributedString => { * @param {Array} attributed strings * @return {Object} layout blocks */ -const layoutLines = (rect, lines, indent) => { +const layoutLines = (rects, lines, indent) => { + let rect = rects.shift(); let currentY = rect.y; return lines.map((line, i) => { @@ -34,6 +36,11 @@ const layoutLines = (rect, lines, indent) => { const style = line.runs?.[0]?.attributes || {}; const height = Math.max(stringHeight(line), style.lineHeight); + if (currentY + height > rect.y + rect.height) { + rect = rects.shift(); + currentY = rect.y; + } + const newLine = Object.assign({}, line); delete newLine.syllables; @@ -60,12 +67,17 @@ const layoutLines = (rect, lines, indent) => { * @param {Object} attributed string * @return {Object} layout block */ -const layoutParagraph = (engines, options) => (rect, paragraph) => { +const layoutParagraph = (engines, options) => (container, paragraph) => { + const height = stringHeight(paragraph); const indent = paragraph.runs?.[0]?.attributes?.indent || 0; - const availableWidths = [rect.width - indent, rect.width]; + const rects = generateLineRects(container, height); + + const availableWidths = rects.map(r => r.width); + availableWidths[0] -= indent; + const lines = engines.linebreaker(options)(paragraph, availableWidths); - return layoutLines(rect, lines, indent); + return layoutLines(rects, lines, indent); }; export default layoutParagraph; diff --git a/packages/textkit/src/rect/crop.js b/packages/textkit/src/rect/crop.js index 89387be64..9e37f8743 100644 --- a/packages/textkit/src/rect/crop.js +++ b/packages/textkit/src/rect/crop.js @@ -1,3 +1,5 @@ +import partition from './partition'; + /** * Crop upper section of rect * @@ -5,10 +7,8 @@ * @return {Object} cropped rect */ const crop = (height, rect) => { - const y = rect.y + height; - const h = rect.height - height; - - return Object.assign({}, rect, { y, height: h }); + const [, result] = partition(rect, height); + return result; }; export default crop; diff --git a/packages/textkit/src/rect/intersects.js b/packages/textkit/src/rect/intersects.js new file mode 100644 index 000000000..a796ce51c --- /dev/null +++ b/packages/textkit/src/rect/intersects.js @@ -0,0 +1,17 @@ +/** + * Checks if two rects intersect each other + * + * @param {Rect} a + * @param {Rect} b + * @returns {Boolean} rects intersects + */ +const intersects = (a, b) => { + const x = Math.max(a.x, b.x); + const num1 = Math.min(a.x + a.width, b.x + b.width); + const y = Math.max(a.y, b.y); + const num2 = Math.min(a.y + a.height, b.y + b.height); + + return num1 >= x && num2 >= y; +}; + +export default intersects; diff --git a/packages/textkit/src/rect/partition.js b/packages/textkit/src/rect/partition.js new file mode 100644 index 000000000..4d5926bf6 --- /dev/null +++ b/packages/textkit/src/rect/partition.js @@ -0,0 +1,12 @@ +const partition = (rect, height) => { + const a = Object.assign({}, rect, { height }); + + const b = Object.assign({}, rect, { + y: rect.y + height, + height: rect.height - height, + }); + + return [a, b]; +}; + +export default partition; diff --git a/packages/textkit/tests/rect/intersects.test.js b/packages/textkit/tests/rect/intersects.test.js new file mode 100644 index 000000000..9210ca004 --- /dev/null +++ b/packages/textkit/tests/rect/intersects.test.js @@ -0,0 +1,115 @@ +import intersects from '../../src/rect/intersects'; + +describe('rect intersects operator', () => { + test('should not intesect on top-left corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 0, y: 0, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on top-left corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 0, y: 0, width: 60, height: 60 }; + + expect(intersects(a, b)).toEqual(true); + }); + + test('should not intesect on top edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 50, y: 0, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on top edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 50, y: 0, width: 30, height: 60 }; + + expect(intersects(a, b)).toEqual(true); + }); + + test('should not intesect on top-right corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 150, y: 0, width: 50, height: 50 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on top-right corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 70, y: 0, width: 50, height: 50 }; + + expect(intersects(a, b)).toEqual(true); + }); + + test('should not intesect on top edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 150, y: 40, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on top edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 130, y: 40, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(true); + }); + + test('should not intesect on bottom-right corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 150, y: 170, width: 50, height: 50 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on bottom-right corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 70, y: 120, width: 50, height: 50 }; + + expect(intersects(a, b)).toEqual(true); + }); + + test('should not intesect on bottom edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 70, y: 180, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on bottom edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 70, y: 100, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(true); + }); + + test('should not intesect on bottom-left corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 0, y: 170, width: 50, height: 50 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on bottom-left corner', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 0, y: 120, width: 50, height: 50 }; + + expect(intersects(a, b)).toEqual(true); + }); + + test('should not intesect on left edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 0, y: 60, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(false); + }); + + test('should intesect on left edge', () => { + const a = { x: 50, y: 50, width: 90, height: 110 }; + const b = { x: 30, y: 60, width: 30, height: 30 }; + + expect(intersects(a, b)).toEqual(true); + }); +}); diff --git a/packages/textkit/tests/rect/partition.test.js b/packages/textkit/tests/rect/partition.test.js new file mode 100644 index 000000000..f4963bcfd --- /dev/null +++ b/packages/textkit/tests/rect/partition.test.js @@ -0,0 +1,23 @@ +import partition from '../../src/rect/partition'; + +describe('rect partition operator', () => { + test('should return empty rect if height 0', () => { + const target = { x: 10, y: 10, width: 90, height: 110 }; + const result = partition(target, 0); + + expect(result).toEqual([ + { x: 10, y: 10, width: 90, height: 0 }, + { x: 10, y: 10, width: 90, height: 110 }, + ]); + }); + + test('should return correct partition', () => { + const target = { x: 10, y: 10, width: 90, height: 110 }; + const result = partition(target, 20); + + expect(result).toEqual([ + { x: 10, y: 10, width: 90, height: 20 }, + { x: 10, y: 30, width: 90, height: 90 }, + ]); + }); +});