Skip to content
This repository was archived by the owner on Dec 10, 2024. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/faebryk/exporters/pcb/layout/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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")

Expand Down
104 changes: 71 additions & 33 deletions src/faebryk/libs/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
"""

Expand All @@ -53,49 +57,83 @@ def string_to_polygons(

face = freetype.Face(str(self.path))
polygons = []
offset = Point(0, 0)

if scale_to_fit:
font_size = 1

text_size = Point(0, 0)

scale = font_size / face.units_per_EM
for char in string:
face.load_char(char)

if bbox and not scale_to_fit:
if offset.x + face.glyph.advance.x > bbox[0] / scale:
if not wrap:
break
for i, line in enumerate(reversed(string.split("\\n"))):
offset = Point(0, i * face.units_per_EM)

for char in line:
face.load_char(char)

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

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_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:
continue

if offset.x > bbox[0]:
offset = Point(0, offset.y + face.glyph.advance.y)
if offset.y > bbox[1] / scale:
if offset.y > bbox[1]:
logger.warning("Text does not fit in bounding box")
break

points = face.glyph.outline.points
contours = face.glyph.outline.contours

start = 0

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))

offset = Point(offset.x + face.glyph.advance.x, offset.y)

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]:
break

bounds = [p.bounds for p in polygons]
min_x, min_y, max_x, max_y = (
min(b[0] for b in bounds),
Expand Down
63 changes: 51 additions & 12 deletions src/faebryk/libs/geometry/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,29 @@
from typing import Iterable, TypeVar

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__)


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
Expand Down Expand Up @@ -80,16 +98,27 @@ 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.02,
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)
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

Expand All @@ -99,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):
Expand Down Expand Up @@ -141,8 +174,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):
Expand All @@ -151,12 +182,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)
if force_in_polygon and not point_bubble.contains(new_point):
new_point, _ = nearest_points(polygon, new_point)

point_distance_travel.append(
Geometry.distance_euclid(
Expand All @@ -171,8 +198,20 @@ def get_distributed_points_in_polygon(

points[i] = new_point

if max(point_distance_travel) < density / 100:
break
if progress is not None and task_id is not None:
progress.update(
task_id,
completed=max(
(100 * convergence_threshold)
/ max(point_distance_travel + [convergence_threshold]),
100,
),
)

if max(point_distance_travel + [0]) < convergence_threshold:
if force_in_polygon:
break
force_in_polygon = True

return points

Expand Down