From 69f6f6593f198ccd12702678abf42bd0edb5abec Mon Sep 17 00:00:00 2001 From: IoannisP_ITENG Date: Sat, 6 Jul 2024 13:49:08 +0200 Subject: [PATCH 1/8] FontLayout: Speedup, show progress per polygon Speedup the finding of distributed points in a polygon by introducing a higher convergence threshold and an absolute limit on the amount of points per polygon Also add options for showing progress --- src/faebryk/exporters/pcb/layout/font.py | 15 ++++++++++++--- src/faebryk/libs/geometry/basic.py | 23 ++++++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/faebryk/exporters/pcb/layout/font.py b/src/faebryk/exporters/pcb/layout/font.py index dc13228f..11c0e6dc 100644 --- a/src/faebryk/exporters/pcb/layout/font.py +++ b/src/faebryk/exporters/pcb/layout/font.py @@ -11,7 +11,7 @@ ) from faebryk.libs.font import Font from faebryk.libs.geometry.basic import get_distributed_points_in_polygon -from rich.progress import track +from rich.progress import Progress logger = logging.getLogger(__name__) @@ -47,8 +47,17 @@ def __init__( logger.info(f"Finding points in polygons with density: {density}") nodes = [] - for p in track(polys, description="Finding points in polygons"): - nodes.extend(get_distributed_points_in_polygon(polygon=p, density=density)) + with Progress(*Progress.get_default_columns()) as progress: + tasks = [ + progress.add_task(f"Converging points for polygon {i}", total=100) + for i, _ in enumerate(polys) + ] + for i, p in enumerate(polys): + nodes.extend( + get_distributed_points_in_polygon( + polygon=p, density=density, progress=progress, task_id=tasks[i] + ) + ) logger.info(f"Creating {len(nodes)} nodes in polygons") diff --git a/src/faebryk/libs/geometry/basic.py b/src/faebryk/libs/geometry/basic.py index a737f5ea..f5330f8f 100644 --- a/src/faebryk/libs/geometry/basic.py +++ b/src/faebryk/libs/geometry/basic.py @@ -7,6 +7,7 @@ from typing import Iterable, TypeVar import numpy as np +from rich.progress import Progress, TaskID from shapely import MultiPolygon, Point, Polygon, transform logger = logging.getLogger(__name__) @@ -80,16 +81,24 @@ def flatten_polygons(polygons: list[Polygon]) -> list[Polygon]: def get_distributed_points_in_polygon( polygon: Polygon, density: float, + max_points: int = 50, + convergence_threshold: float = 0.05, + progress: Progress | None = None, + task_id: TaskID | None = None, ) -> list[Point]: """ Get a list of points that are distributed in a polygon - :param polygon: The polygon to distribute the points in :param density: The density of the points + :param density: The desired density of the points + :param max_points: The maximum number of points to distribute, used to limit the + execution time. Takes precedence over density + :param convergence_threshold: The threshold for the convergence of the points. The + algorithm will stop when the maximum distance between points is less than this value :return: A list of points that are distributed in the polygon """ - num_points = int(polygon.area * density) + num_points = min(int(polygon.area * density), max_points) if polygon.area > 0 and num_points == 0: num_points = 1 @@ -171,7 +180,15 @@ def get_distributed_points_in_polygon( points[i] = new_point - if max(point_distance_travel) < density / 100: + if progress and task_id: + progress.update( + task_id, + completed=max( + (100 * convergence_threshold) / max(point_distance_travel), 100 + ), + ) + + if max(point_distance_travel) < convergence_threshold: break return points From 0554ee190bff3ad071f7453d6f10ea7f640ac43a Mon Sep 17 00:00:00 2001 From: Jasper Smit Date: Sat, 6 Jul 2024 15:11:12 +0200 Subject: [PATCH 2/8] Fontlayout: Fix division by zero in progress update --- src/faebryk/libs/geometry/basic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/faebryk/libs/geometry/basic.py b/src/faebryk/libs/geometry/basic.py index f5330f8f..d526eb0a 100644 --- a/src/faebryk/libs/geometry/basic.py +++ b/src/faebryk/libs/geometry/basic.py @@ -184,7 +184,9 @@ def get_distributed_points_in_polygon( progress.update( task_id, completed=max( - (100 * convergence_threshold) / max(point_distance_travel), 100 + (100 * convergence_threshold) + / max(point_distance_travel + [convergence_threshold]), + 100, ), ) From ab6b4abb644fe05c6040f2a66ff293af3888cc8c Mon Sep 17 00:00:00 2001 From: Jasper Smit Date: Mon, 8 Jul 2024 17:24:41 +0200 Subject: [PATCH 3/8] Geometry: Lower default convergence_threshold in get_distributed_points_in_polygon Lowers the default theshold to get more accurate positioning --- src/faebryk/libs/geometry/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/faebryk/libs/geometry/basic.py b/src/faebryk/libs/geometry/basic.py index d526eb0a..aed3d4cd 100644 --- a/src/faebryk/libs/geometry/basic.py +++ b/src/faebryk/libs/geometry/basic.py @@ -82,7 +82,7 @@ def get_distributed_points_in_polygon( polygon: Polygon, density: float, max_points: int = 50, - convergence_threshold: float = 0.05, + convergence_threshold: float = 0.02, progress: Progress | None = None, task_id: TaskID | None = None, ) -> list[Point]: From 05f24f9a64074ad84144c084da096a291e353097 Mon Sep 17 00:00:00 2001 From: Jasper Smit Date: Mon, 8 Jul 2024 17:25:28 +0200 Subject: [PATCH 4/8] Geometry: Fix bugs in get_distributed_points_in_polygon - Fix moving point inside polygon when COM is outside Moves the new point inside the polygon again when the calculated center-of-mass of the point is outside the polygon. - Fix exception when no points have travelled - Add check for empty polygon to return empty array - Fix some tasks not updating --- src/faebryk/libs/geometry/basic.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/faebryk/libs/geometry/basic.py b/src/faebryk/libs/geometry/basic.py index aed3d4cd..91dba1c2 100644 --- a/src/faebryk/libs/geometry/basic.py +++ b/src/faebryk/libs/geometry/basic.py @@ -9,6 +9,7 @@ import numpy as np from rich.progress import Progress, TaskID from shapely import MultiPolygon, Point, Polygon, transform +from shapely.ops import nearest_points logger = logging.getLogger(__name__) @@ -98,6 +99,9 @@ def get_distributed_points_in_polygon( :return: A list of points that are distributed in the polygon """ + if polygon.area == 0: + return [] + num_points = min(int(polygon.area * density), max_points) if polygon.area > 0 and num_points == 0: num_points = 1 @@ -150,8 +154,6 @@ def get_distributed_points_in_polygon( point_bubble = point_bubble.difference(exclusion_polygon) - assert point_bubble.contains(point) - if isinstance(point_bubble, MultiPolygon): for p in list(point_bubble.geoms): if p.contains(point): @@ -160,12 +162,8 @@ def get_distributed_points_in_polygon( new_point = point_bubble.centroid - # TODO: it is possible it will be outside, then just move it to the - # closest point on the polygon. if not point_bubble.contains(new_point): - continue - - assert polygon.contains(new_point) + new_point, _ = nearest_points(polygon, new_point) point_distance_travel.append( Geometry.distance_euclid( @@ -180,7 +178,7 @@ def get_distributed_points_in_polygon( points[i] = new_point - if progress and task_id: + if progress is not None and task_id is not None: progress.update( task_id, completed=max( @@ -190,7 +188,7 @@ def get_distributed_points_in_polygon( ), ) - if max(point_distance_travel) < convergence_threshold: + if max(point_distance_travel + [0]) < convergence_threshold: break return points From efa77c36e176202cafa7f700a7a2a4fda30465dd Mon Sep 17 00:00:00 2001 From: Jasper Smit Date: Mon, 8 Jul 2024 17:30:47 +0200 Subject: [PATCH 5/8] Font: Add support for multiline strings Add support for left-aligned multiline strings --- src/faebryk/libs/font.py | 59 +++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/faebryk/libs/font.py b/src/faebryk/libs/font.py index 2074ff4a..68a17bfc 100644 --- a/src/faebryk/libs/font.py +++ b/src/faebryk/libs/font.py @@ -53,7 +53,6 @@ def string_to_polygons( face = freetype.Face(str(self.path)) polygons = [] - offset = Point(0, 0) if scale_to_fit: font_size = 1 @@ -61,40 +60,44 @@ def string_to_polygons( text_size = Point(0, 0) scale = font_size / face.units_per_EM - for char in string: - face.load_char(char) + for i, line in enumerate(reversed(string.split("\\n"))): + offset = Point(0, i * face.units_per_EM) - if bbox and not scale_to_fit: - if offset.x + face.glyph.advance.x > bbox[0] / scale: - if not wrap: - break - offset = Point(0, offset.y + face.glyph.advance.y) - if offset.y > bbox[1] / scale: - break + for char in line: + face.load_char(char) - points = face.glyph.outline.points - contours = face.glyph.outline.contours + if bbox and not scale_to_fit: + if offset.x + face.glyph.advance.x > bbox[0] / scale: + if not wrap: + break + offset = Point(0, offset.y + face.glyph.advance.y) + if offset.y > bbox[1] / scale: + break - start = 0 + points = face.glyph.outline.points + contours = face.glyph.outline.contours - for contour in contours: - contour_points = [Point(p) for p in points[start : contour + 1]] - contour_points.append(contour_points[0]) - start = contour + 1 - contour_points = [ - Point(p.x + offset.x, p.y + offset.y) for p in contour_points - ] - polygons.append(Polygon(contour_points)) + start = 0 - offset = Point(offset.x + face.glyph.advance.x, offset.y) + for contour in contours: + contour_points = [Point(p) for p in points[start : contour + 1]] + contour_points.append(contour_points[0]) + start = contour + 1 + contour_points = [ + Point(p.x + offset.x, p.y + offset.y) for p in contour_points + ] + polygons.append(Polygon(contour_points)) - if not wrap or not bbox: - continue + offset = Point(offset.x + face.glyph.advance.x, offset.y) - if offset.x > bbox[0]: - offset = Point(0, offset.y + face.glyph.advance.y) - if offset.y > bbox[1]: - break + if not wrap or not bbox: + continue + + if offset.x > bbox[0]: + offset = Point(0, offset.y + face.glyph.advance.y) + if offset.y > bbox[1]: + logger.warning("Text does not fit in bounding box") + break bounds = [p.bounds for p in polygons] min_x, min_y, max_x, max_y = ( From 363c6fe96b21a691818f90679c3cea1117d9576c Mon Sep 17 00:00:00 2001 From: Jasper Smit Date: Mon, 8 Jul 2024 20:44:29 +0200 Subject: [PATCH 6/8] Geometry: Add bezier calculation Add get_point_on_bezier_curve() --- src/faebryk/libs/geometry/basic.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/faebryk/libs/geometry/basic.py b/src/faebryk/libs/geometry/basic.py index 91dba1c2..ca94c440 100644 --- a/src/faebryk/libs/geometry/basic.py +++ b/src/faebryk/libs/geometry/basic.py @@ -14,6 +14,22 @@ logger = logging.getLogger(__name__) +def get_point_on_bezier_curve(p, t: float) -> np.ndarray: + """ + Get a point on a quadratic bezier curve + + :param p: The points, including the start and end points + :param t: The parameter t [0, 1] + :return: The point on the curve + """ + if len(p) == 1: + return p[0] + else: + return (1 - t) * get_point_on_bezier_curve( + p[:-1], t + ) + t * get_point_on_bezier_curve(p[1:], t) + + def polygon_insert_cutout(polygon: Polygon, cutout: Polygon) -> Polygon: """ Insert a cutout into a polygon From dec408caaa9aabacfb21829d0b95a36da7e5ba5a Mon Sep 17 00:00:00 2001 From: Jasper Smit Date: Mon, 8 Jul 2024 20:44:45 +0200 Subject: [PATCH 7/8] Font: Add bezier curve approximation Adds a resolution parameter to string_to_polygons() which specifies in how many points a bezier curve in a font is approximated. --- src/faebryk/libs/font.py | 41 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/faebryk/libs/font.py b/src/faebryk/libs/font.py index 68a17bfc..ad953003 100644 --- a/src/faebryk/libs/font.py +++ b/src/faebryk/libs/font.py @@ -5,8 +5,10 @@ from pathlib import Path import freetype +import numpy as np from faebryk.libs.geometry.basic import ( flatten_polygons, + get_point_on_bezier_curve, transform_polygon, ) from shapely import Point, Polygon @@ -27,6 +29,7 @@ def string_to_polygons( bbox: tuple[float, float] | None = None, wrap: bool = False, scale_to_fit: bool = False, + resolution: int = 10, ) -> list[Polygon]: """ Render the polygons of a string from a ttf font file @@ -37,6 +40,7 @@ def string_to_polygons( :param bbox: The bounding box to fit the text in, in points :param wrap: Wrap the text to fit the bounding box :param scale_to_fit: Scale the text to fit the bounding box + :param resolution: The resolution of the bezier curves :return: A list of polygons that represent the string """ @@ -75,19 +79,50 @@ def string_to_polygons( break points = face.glyph.outline.points + tags = face.glyph.outline.tags contours = face.glyph.outline.contours start = 0 + ts = np.linspace(0, 1, resolution) + for contour in contours: - contour_points = [Point(p) for p in points[start : contour + 1]] - contour_points.append(contour_points[0]) - start = contour + 1 + contour_points = [] + point_info = list( + zip(points[start : contour + 1], tags[start : contour + 1]) + ) + point_info.append(point_info[0]) + i = 0 + while i < len(point_info): + # find segment of points that are off curve + segment = [point_info[i][0]] + i += 1 + for j in range(i, len(point_info)): + point, tag = point_info[j] + segment.append(point) + if tag & 1: + i = j + break + + contour_points.extend( + [ + Point( + get_point_on_bezier_curve( + [np.array(s) for s in segment], ts + ) + ) + for ts in ts + ] + ) + + # apply the offset contour_points = [ Point(p.x + offset.x, p.y + offset.y) for p in contour_points ] polygons.append(Polygon(contour_points)) + start = contour + 1 + offset = Point(offset.x + face.glyph.advance.x, offset.y) if not wrap or not bbox: From a1364266f1ba12a00d2c630b467e4d60b7235e4e Mon Sep 17 00:00:00 2001 From: Jasper Smit Date: Mon, 8 Jul 2024 20:55:06 +0200 Subject: [PATCH 8/8] Geometry: Improve get_distributed_points_in_polygon Improve placement of characters that have disjoint polygons by iterating until threshold, then forcing the points to be inside the polygon and iterating again instead of always forcing the points to be inside the polygon. --- src/faebryk/libs/geometry/basic.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/faebryk/libs/geometry/basic.py b/src/faebryk/libs/geometry/basic.py index ca94c440..aa60a9e2 100644 --- a/src/faebryk/libs/geometry/basic.py +++ b/src/faebryk/libs/geometry/basic.py @@ -128,6 +128,10 @@ def get_distributed_points_in_polygon( size_x = max_x - min_x size_y = max_y - min_y + # iterate until threshold, then force points to be inside the polygon and iterate + # again + force_in_polygon = False + while True: point_distance_travel = [] for i, point in enumerate(points): @@ -178,7 +182,7 @@ def get_distributed_points_in_polygon( new_point = point_bubble.centroid - if not point_bubble.contains(new_point): + if force_in_polygon and not point_bubble.contains(new_point): new_point, _ = nearest_points(polygon, new_point) point_distance_travel.append( @@ -205,7 +209,9 @@ def get_distributed_points_in_polygon( ) if max(point_distance_travel + [0]) < convergence_threshold: - break + if force_in_polygon: + break + force_in_polygon = True return points