Merge pull request 'Removing mxpic_forge from dependency' (#2) from main into pengkun_main #3
+454
-12
@@ -9,6 +9,11 @@ from .eda_loader import CellSpec, InstanceSpec, LinkSpec, load_cell_spec
|
||||
from .technology import apply_technology_manifest, load_technology_manifest
|
||||
|
||||
|
||||
ROUTE_MIN_SPACING = 10.0
|
||||
ROUTE_ENDPOINT_SPACING_IGNORE = ROUTE_MIN_SPACING * 2.0
|
||||
ROUTE_SPACING_GEOMETRY_ADJUSTMENT_ENABLED = False
|
||||
|
||||
|
||||
def build_project_gds(
|
||||
project_dir: str,
|
||||
output_path: str,
|
||||
@@ -105,8 +110,7 @@ def _build_cell(spec: CellSpec, built_cells: dict, pdk_root: str, prefer_full_gd
|
||||
_register_element_pins(pin_map, element_name, element, nd)
|
||||
|
||||
for bundle in spec.bundles.values():
|
||||
for link in bundle.links:
|
||||
_route_link(link, pin_map, Route, warnings)
|
||||
_route_bundle_links(bundle.links, pin_map, Route, warnings)
|
||||
return top
|
||||
|
||||
|
||||
@@ -268,7 +272,398 @@ def _metal_route_pcb_enabled(xsection: str) -> bool:
|
||||
return normalized in {"metal_1", "metal1", "metal_2", "metal2"}
|
||||
|
||||
|
||||
def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list) -> None:
|
||||
def _route_bundle_links(links: list, pin_map: dict, Route, warnings: list) -> None:
|
||||
plans = []
|
||||
by_order = {}
|
||||
for order, link in enumerate(links):
|
||||
p1 = pin_map.get((link.src_inst, link.src_pin))
|
||||
p2 = pin_map.get((link.dst_inst, link.dst_pin))
|
||||
if p1 is None or p2 is None:
|
||||
by_order[order] = {"link": link, "adjusted_points": None}
|
||||
continue
|
||||
points = _route_spacing_reference_points(link, p1, p2)
|
||||
if len(points) < 2:
|
||||
by_order[order] = {"link": link, "adjusted_points": None, "adjusted_offset": None}
|
||||
continue
|
||||
plan = {
|
||||
"order": order,
|
||||
"link": link,
|
||||
"pin1": p1,
|
||||
"pin2": p2,
|
||||
"points": points,
|
||||
"automatic": len(link.points or []) < 2,
|
||||
"adjusted_points": None,
|
||||
"adjusted_offset": None,
|
||||
"check_points": None,
|
||||
"route_options": {},
|
||||
}
|
||||
plans.append(plan)
|
||||
by_order[order] = plan
|
||||
|
||||
_assign_sbend_lstart_spacing(plans)
|
||||
|
||||
accepted = []
|
||||
for plan in plans:
|
||||
check_points = _route_spacing_check_points(plan["points"])
|
||||
if _route_points_have_spacing(check_points, accepted):
|
||||
plan["check_points"] = check_points
|
||||
accepted.append(plan)
|
||||
continue
|
||||
adjustment = _first_spacing_adjusted_route(plan, accepted)
|
||||
if adjustment is not None:
|
||||
warnings.append(
|
||||
f"Detected route spacing below {ROUTE_MIN_SPACING:g}um for "
|
||||
f"{plan['link'].src_inst}:{plan['link'].src_pin} -> "
|
||||
f"{plan['link'].dst_inst}:{plan['link'].dst_pin}"
|
||||
)
|
||||
if ROUTE_SPACING_GEOMETRY_ADJUSTMENT_ENABLED:
|
||||
plan["adjusted_points"] = adjustment["points"]
|
||||
plan["check_points"] = _route_spacing_check_points(adjustment["points"])
|
||||
else:
|
||||
plan["check_points"] = check_points
|
||||
else:
|
||||
plan["check_points"] = check_points
|
||||
accepted.append(plan)
|
||||
|
||||
for order in range(len(links)):
|
||||
plan = by_order.get(order)
|
||||
if not plan:
|
||||
continue
|
||||
if plan.get("adjusted_points"):
|
||||
route = Route(
|
||||
radius=plan["link"].radius or 10,
|
||||
width=plan["link"].width,
|
||||
xs=plan["link"].xsection,
|
||||
PCB=_metal_route_pcb_enabled(plan["link"].xsection),
|
||||
)
|
||||
if plan.get("automatic"):
|
||||
if _route_guided_straight_link(plan["link"], route, warnings, plan["adjusted_points"]):
|
||||
warnings.append(
|
||||
f"Applied {ROUTE_MIN_SPACING:g}um spacing lane to "
|
||||
f"{plan['link'].src_inst}:{plan['link'].src_pin} -> "
|
||||
f"{plan['link'].dst_inst}:{plan['link'].dst_pin}"
|
||||
)
|
||||
continue
|
||||
if _route_guided_link(plan["link"], route, warnings, plan["adjusted_points"]):
|
||||
continue
|
||||
if plan.get("route_options", {}).get("Lstart"):
|
||||
warnings.append(
|
||||
f"Applied sbend Lstart {plan['route_options']['Lstart']:g}um to "
|
||||
f"{plan['link'].src_inst}:{plan['link'].src_pin} -> "
|
||||
f"{plan['link'].dst_inst}:{plan['link'].dst_pin}"
|
||||
)
|
||||
_route_link(plan["link"], pin_map, Route, warnings, plan.get("route_options"))
|
||||
|
||||
|
||||
def _assign_sbend_lstart_spacing(plans: list) -> None:
|
||||
groups = {}
|
||||
for plan in plans:
|
||||
if not plan.get("automatic"):
|
||||
continue
|
||||
if _route_method_name_for_pins(plan["pin1"], plan["pin2"]) != "sbend_p2p":
|
||||
continue
|
||||
key = _sbend_spacing_group_key(plan)
|
||||
if key is None:
|
||||
continue
|
||||
groups.setdefault(key, []).append(plan)
|
||||
|
||||
for group in groups.values():
|
||||
if len(group) < 2:
|
||||
continue
|
||||
axis = _dominant_spacing_axis(group[0]["points"][0], group[0]["points"][-1])
|
||||
coord_key = "y" if axis == "horizontal" else "x"
|
||||
step = _sbend_lstart_step(group)
|
||||
for plan, rank in _sbend_lstart_ranks(group, coord_key):
|
||||
plan["route_options"]["Lstart"] = rank * step
|
||||
|
||||
|
||||
def _sbend_lstart_step(group: list) -> float:
|
||||
width = max((_safe_float(plan["link"].width, 0.5) or 0.5) for plan in group)
|
||||
return ROUTE_MIN_SPACING + width
|
||||
|
||||
|
||||
def _sbend_lstart_ranks(group: list, coord_key: str) -> list:
|
||||
deltas = [_route_coord_delta(plan, coord_key) for plan in group]
|
||||
nonzero_deltas = [delta for delta in deltas if abs(delta) > 1e-9]
|
||||
if nonzero_deltas and all(delta > 0 for delta in nonzero_deltas):
|
||||
ordered = sorted(group, key=lambda plan: _route_mid_coord(plan, coord_key))
|
||||
return [(plan, len(ordered) - index) for index, plan in enumerate(ordered)]
|
||||
if nonzero_deltas and all(delta < 0 for delta in nonzero_deltas):
|
||||
ordered = sorted(group, key=lambda plan: _route_mid_coord(plan, coord_key), reverse=True)
|
||||
return [(plan, len(ordered) - index) for index, plan in enumerate(ordered)]
|
||||
|
||||
center = sum(_route_mid_coord(plan, coord_key) for plan in group) / len(group)
|
||||
distances = sorted({round(abs(_route_mid_coord(plan, coord_key) - center), 6) for plan in group})
|
||||
return [
|
||||
(plan, distances.index(round(abs(_route_mid_coord(plan, coord_key) - center), 6)) + 1)
|
||||
for plan in group
|
||||
]
|
||||
|
||||
|
||||
def _route_coord_delta(plan: dict, coord_key: str) -> float:
|
||||
start = plan["points"][0]
|
||||
end = plan["points"][-1]
|
||||
return float(end[coord_key]) - float(start[coord_key])
|
||||
|
||||
|
||||
def _sbend_spacing_group_key(plan: dict):
|
||||
link = plan["link"]
|
||||
axis = _dominant_spacing_axis(plan["points"][0], plan["points"][-1])
|
||||
return (
|
||||
axis,
|
||||
str(link.xsection or ""),
|
||||
_safe_float(link.width, 0.0),
|
||||
_safe_float(link.radius, 0.0),
|
||||
)
|
||||
|
||||
|
||||
def _route_group_needs_spacing(group: list) -> bool:
|
||||
check_points = [_route_spacing_check_points(plan["points"]) for plan in group]
|
||||
threshold = _sbend_lstart_step(group)
|
||||
for index in range(len(check_points)):
|
||||
for other_index in range(index + 1, len(check_points)):
|
||||
if _polyline_spacing(check_points[index], check_points[other_index]) < threshold:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _route_mid_coord(plan: dict, coord_key: str) -> float:
|
||||
start = plan["points"][0]
|
||||
end = plan["points"][-1]
|
||||
return (float(start[coord_key]) + float(end[coord_key])) / 2.0
|
||||
|
||||
|
||||
def _route_spacing_reference_points(link: LinkSpec, pin1, pin2) -> list:
|
||||
if len(link.points or []) >= 2:
|
||||
return _route_points_with_pin_endpoints(link.points, pin1, pin2)
|
||||
p1 = _pin_point(pin1)
|
||||
p2 = _pin_point(pin2)
|
||||
if p1 is None or p2 is None:
|
||||
return []
|
||||
return _automatic_route_reference_points(pin1, pin2, p1, p2)
|
||||
|
||||
|
||||
def _automatic_route_reference_points(pin1, pin2, point1: dict, point2: dict) -> list:
|
||||
angle1 = _pin_angle(pin1)
|
||||
angle2 = _pin_angle(pin2)
|
||||
if angle1 is None or angle2 is None:
|
||||
return [point1, point2]
|
||||
delta = (angle2 - angle1) % 360
|
||||
if abs(delta - 90) <= 1e-6 or abs(delta - 270) <= 1e-6:
|
||||
elbow = _orthogonal_elbow(point1, angle1, point2, angle2)
|
||||
if elbow is not None and _distance(point1, elbow) > 1e-9 and _distance(elbow, point2) > 1e-9:
|
||||
return [point1, elbow, point2]
|
||||
return [point1, point2]
|
||||
|
||||
|
||||
def _orthogonal_elbow(point1: dict, angle1: float, point2: dict, angle2: float):
|
||||
dx1, dy1 = _direction_vector(angle1)
|
||||
dx2, dy2 = _direction_vector(angle2)
|
||||
determinant = dx2 * dy1 - dx1 * dy2
|
||||
if abs(determinant) <= 1e-9:
|
||||
return None
|
||||
delta_x = point2["x"] - point1["x"]
|
||||
delta_y = point2["y"] - point1["y"]
|
||||
t1 = (dx2 * delta_y - dy2 * delta_x) / determinant
|
||||
t2 = (dx1 * delta_y - dy1 * delta_x) / determinant
|
||||
if t1 < -1e-9 or t2 < -1e-9:
|
||||
return None
|
||||
return {"x": point1["x"] + dx1 * t1, "y": point1["y"] + dy1 * t1}
|
||||
|
||||
|
||||
def _route_spacing_check_points(points_or_plan) -> list:
|
||||
source_points = points_or_plan.get("points", []) if isinstance(points_or_plan, dict) else points_or_plan
|
||||
points = [
|
||||
{"x": float(point["x"]), "y": float(point["y"])}
|
||||
for point in source_points
|
||||
if isinstance(point, dict) and "x" in point and "y" in point
|
||||
]
|
||||
if len(points) < 2:
|
||||
return points
|
||||
return _trim_polyline_ends(points, ROUTE_ENDPOINT_SPACING_IGNORE)
|
||||
|
||||
|
||||
def _trim_polyline_ends(points: list, distance: float) -> list:
|
||||
trimmed = _trim_polyline_start(points, distance)
|
||||
trimmed = list(reversed(_trim_polyline_start(list(reversed(trimmed)), distance)))
|
||||
return trimmed if len(trimmed) >= 2 else points
|
||||
|
||||
|
||||
def _trim_polyline_start(points: list, distance: float) -> list:
|
||||
if len(points) < 2 or distance <= 0:
|
||||
return points
|
||||
remaining = float(distance)
|
||||
for index in range(len(points) - 1):
|
||||
segment_length = _distance(points[index], points[index + 1])
|
||||
if segment_length <= 1e-9:
|
||||
continue
|
||||
if remaining < segment_length:
|
||||
start = _point_toward(points[index], points[index + 1], remaining)
|
||||
return [start] + points[index + 1:]
|
||||
remaining -= segment_length
|
||||
return points[-2:]
|
||||
|
||||
|
||||
def _route_points_have_spacing(points: list, accepted_plans: list) -> bool:
|
||||
return all(_polyline_spacing(points, plan.get("check_points") or []) >= ROUTE_MIN_SPACING for plan in accepted_plans)
|
||||
|
||||
|
||||
def _first_spacing_adjusted_route(plan: dict, accepted_plans: list):
|
||||
for offset in _spacing_offsets(plan, accepted_plans):
|
||||
points = _spacing_adjusted_route_points(plan["link"], plan["pin1"], plan["pin2"], offset)
|
||||
if _route_points_have_spacing(_route_spacing_check_points(points), accepted_plans):
|
||||
return {"offset": offset, "points": points}
|
||||
return None
|
||||
|
||||
|
||||
def _spacing_offsets(plan: dict = None, accepted_plans: list = None, limit: int = 16) -> list:
|
||||
preferred_sign = _preferred_spacing_sign(plan, accepted_plans)
|
||||
offsets = []
|
||||
for step in range(1, limit + 1):
|
||||
distance = step * (ROUTE_MIN_SPACING / 2.0)
|
||||
offsets.extend([preferred_sign * distance, -preferred_sign * distance])
|
||||
return offsets
|
||||
|
||||
|
||||
def _preferred_spacing_sign(plan: dict = None, accepted_plans: list = None) -> float:
|
||||
if not plan or not accepted_plans:
|
||||
return 1.0
|
||||
axis = _dominant_spacing_axis(plan["points"][0], plan["points"][-1])
|
||||
coord_key = "y" if axis == "horizontal" else "x"
|
||||
current_coord = _average_polyline_coord(_route_spacing_check_points(plan["points"]), coord_key)
|
||||
nearest_coord = None
|
||||
nearest_spacing = float("inf")
|
||||
for accepted in accepted_plans:
|
||||
spacing = _polyline_spacing(_route_spacing_check_points(plan["points"]), accepted.get("check_points") or [])
|
||||
if spacing >= nearest_spacing:
|
||||
continue
|
||||
nearest_spacing = spacing
|
||||
nearest_coord = _average_polyline_coord(accepted.get("check_points") or [], coord_key)
|
||||
if nearest_coord is None:
|
||||
return 1.0
|
||||
if abs(current_coord - nearest_coord) <= 1e-9:
|
||||
return 1.0
|
||||
return 1.0 if current_coord > nearest_coord else -1.0
|
||||
|
||||
|
||||
def _average_polyline_coord(points: list, coord_key: str) -> float:
|
||||
values = [float(point[coord_key]) for point in points or [] if isinstance(point, dict) and coord_key in point]
|
||||
if not values:
|
||||
return 0.0
|
||||
return sum(values) / len(values)
|
||||
|
||||
|
||||
def _spacing_adjusted_route_points(link: LinkSpec, pin1, pin2, offset: float) -> list:
|
||||
base_points = _route_spacing_reference_points(link, pin1, pin2)
|
||||
if len(base_points) < 2:
|
||||
return base_points
|
||||
p1 = base_points[0]
|
||||
p2 = base_points[-1]
|
||||
axis = _dominant_spacing_axis(p1, p2)
|
||||
escape = max((_safe_float(link.radius, 10.0) or 10.0), ROUTE_MIN_SPACING)
|
||||
angle1 = _pin_angle(pin1) or _segment_angle(p1, p2) or 0.0
|
||||
angle2 = _pin_angle(pin2) or ((angle1 + 180) % 360)
|
||||
source_dx, source_dy = _direction_vector(angle1)
|
||||
target_dx, target_dy = _direction_vector(angle2)
|
||||
if axis == "horizontal":
|
||||
source_escape = {"x": p1["x"] + source_dx * escape, "y": p1["y"]}
|
||||
source_lane = {"x": source_escape["x"], "y": source_escape["y"] + offset}
|
||||
target_escape = {"x": p2["x"] - target_dx * escape, "y": p2["y"]}
|
||||
target_lane = {"x": target_escape["x"], "y": target_escape["y"] + offset}
|
||||
else:
|
||||
source_escape = {"x": p1["x"], "y": p1["y"] + source_dy * escape}
|
||||
source_lane = {"x": source_escape["x"] + offset, "y": source_escape["y"]}
|
||||
target_escape = {"x": p2["x"], "y": p2["y"] - target_dy * escape}
|
||||
target_lane = {"x": target_escape["x"] + offset, "y": target_escape["y"]}
|
||||
return [p1, source_escape, source_lane, target_lane, target_escape, p2]
|
||||
|
||||
|
||||
def _dominant_spacing_axis(point1: dict, point2: dict) -> str:
|
||||
return "horizontal" if abs(point2["x"] - point1["x"]) >= abs(point2["y"] - point1["y"]) else "vertical"
|
||||
|
||||
|
||||
def _direction_vector(angle: float) -> tuple:
|
||||
radians = math.radians(angle)
|
||||
dx = math.cos(radians)
|
||||
dy = math.sin(radians)
|
||||
if abs(dx) < 1e-9:
|
||||
dx = 0.0
|
||||
if abs(dy) < 1e-9:
|
||||
dy = 0.0
|
||||
return dx, dy
|
||||
|
||||
|
||||
def _polyline_spacing(points1: list, points2: list) -> float:
|
||||
segments1 = _polyline_segments(points1)
|
||||
segments2 = _polyline_segments(points2)
|
||||
if not segments1 or not segments2:
|
||||
return float("inf")
|
||||
return min(_segment_spacing(seg1, seg2) for seg1 in segments1 for seg2 in segments2)
|
||||
|
||||
|
||||
def _polyline_segments(points: list) -> list:
|
||||
clean_points = [
|
||||
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
|
||||
for point in points or []
|
||||
if isinstance(point, dict)
|
||||
]
|
||||
clean_points = [point for point in clean_points if point["x"] is not None and point["y"] is not None]
|
||||
return list(zip(clean_points, clean_points[1:]))
|
||||
|
||||
|
||||
def _segment_spacing(segment1: tuple, segment2: tuple) -> float:
|
||||
a, b = segment1
|
||||
c, d = segment2
|
||||
if _segments_intersect(a, b, c, d):
|
||||
return 0.0
|
||||
return min(
|
||||
_point_segment_distance(a, c, d),
|
||||
_point_segment_distance(b, c, d),
|
||||
_point_segment_distance(c, a, b),
|
||||
_point_segment_distance(d, a, b),
|
||||
)
|
||||
|
||||
|
||||
def _segments_intersect(a: dict, b: dict, c: dict, d: dict) -> bool:
|
||||
def orientation(p, q, r):
|
||||
value = (q["y"] - p["y"]) * (r["x"] - q["x"]) - (q["x"] - p["x"]) * (r["y"] - q["y"])
|
||||
if abs(value) <= 1e-9:
|
||||
return 0
|
||||
return 1 if value > 0 else 2
|
||||
|
||||
def on_segment(p, q, r):
|
||||
return (
|
||||
min(p["x"], r["x"]) - 1e-9 <= q["x"] <= max(p["x"], r["x"]) + 1e-9
|
||||
and min(p["y"], r["y"]) - 1e-9 <= q["y"] <= max(p["y"], r["y"]) + 1e-9
|
||||
)
|
||||
|
||||
o1 = orientation(a, b, c)
|
||||
o2 = orientation(a, b, d)
|
||||
o3 = orientation(c, d, a)
|
||||
o4 = orientation(c, d, b)
|
||||
if o1 != o2 and o3 != o4:
|
||||
return True
|
||||
return (
|
||||
(o1 == 0 and on_segment(a, c, b))
|
||||
or (o2 == 0 and on_segment(a, d, b))
|
||||
or (o3 == 0 and on_segment(c, a, d))
|
||||
or (o4 == 0 and on_segment(c, b, d))
|
||||
)
|
||||
|
||||
|
||||
def _point_segment_distance(point: dict, start: dict, end: dict) -> float:
|
||||
dx = end["x"] - start["x"]
|
||||
dy = end["y"] - start["y"]
|
||||
length_squared = dx * dx + dy * dy
|
||||
if length_squared <= 1e-18:
|
||||
return _distance(point, start)
|
||||
t = ((point["x"] - start["x"]) * dx + (point["y"] - start["y"]) * dy) / length_squared
|
||||
t = max(0.0, min(1.0, t))
|
||||
projection = {"x": start["x"] + t * dx, "y": start["y"] + t * dy}
|
||||
return _distance(point, projection)
|
||||
|
||||
|
||||
def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list, route_options: dict = None) -> None:
|
||||
route = Route(radius=link.radius or 10, width=link.width, xs=link.xsection, PCB=_metal_route_pcb_enabled(link.xsection))
|
||||
p1 = pin_map.get((link.src_inst, link.src_pin))
|
||||
p2 = pin_map.get((link.dst_inst, link.dst_pin))
|
||||
@@ -291,21 +686,67 @@ def _route_link(link: LinkSpec, pin_map: dict, Route, warnings: list) -> None:
|
||||
if route_method is None:
|
||||
warnings.append(f"Route method {method_name} unavailable; falling back to sbend_p2p")
|
||||
route_method = route.sbend_p2p
|
||||
route_method(
|
||||
pin1=p1,
|
||||
pin2=p2,
|
||||
width=link.width,
|
||||
radius=link.radius or 10,
|
||||
xs=link.xsection,
|
||||
arrow=False,
|
||||
).put()
|
||||
kwargs = {
|
||||
"pin1": p1,
|
||||
"pin2": p2,
|
||||
"width": link.width,
|
||||
"radius": link.radius or 10,
|
||||
"xs": link.xsection,
|
||||
"arrow": False,
|
||||
}
|
||||
if route_options:
|
||||
kwargs.update(route_options)
|
||||
try:
|
||||
route_method(**kwargs).put()
|
||||
except TypeError:
|
||||
if not route_options:
|
||||
raise
|
||||
warnings.append(
|
||||
f"Route method {method_name} rejected spacing options; retrying without options for "
|
||||
f"{link.src_inst}:{link.src_pin} -> {link.dst_inst}:{link.dst_pin}"
|
||||
)
|
||||
for key in route_options:
|
||||
kwargs.pop(key, None)
|
||||
route_method(**kwargs).put()
|
||||
|
||||
|
||||
def _route_guided_straight_link(link: LinkSpec, route, warnings: list, points_override=None) -> bool:
|
||||
route_method = getattr(route, "strt_p2p", None)
|
||||
if route_method is None:
|
||||
warnings.append("Spacing route requires Route.strt_p2p; falling back to automatic p2p route")
|
||||
return False
|
||||
points = [
|
||||
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
|
||||
for point in points_override or []
|
||||
if isinstance(point, dict)
|
||||
]
|
||||
points = [point for point in points if point["x"] is not None and point["y"] is not None]
|
||||
if len(points) < 2:
|
||||
return False
|
||||
for index in range(len(points) - 1):
|
||||
start = points[index]
|
||||
end = points[index + 1]
|
||||
angle = _segment_angle(start, end)
|
||||
if angle is None or _distance(start, end) <= 1e-9:
|
||||
continue
|
||||
route_method(
|
||||
pin1=(start["x"], start["y"], angle),
|
||||
pin2=(end["x"], end["y"], angle),
|
||||
width=link.width,
|
||||
xs=link.xsection,
|
||||
arrow=False,
|
||||
).put()
|
||||
return True
|
||||
|
||||
|
||||
def _route_guided_link(link: LinkSpec, route, warnings: list, points_override=None) -> bool:
|
||||
route_method = getattr(route, "strt_p2p", None)
|
||||
if route_method is None:
|
||||
warnings.append("Manual route points require Route.strt_p2p; falling back to automatic p2p route")
|
||||
return False
|
||||
bend_method = getattr(route, "bend_p2p", None)
|
||||
bend_method = getattr(route, "strt_bend_strt_p2p", None)
|
||||
if bend_method is None:
|
||||
bend_method = getattr(route, "bend_p2p", None)
|
||||
source_points = points_override if points_override is not None else (link.points or [])
|
||||
points = [
|
||||
{"x": _safe_float(point.get("x"), None), "y": _safe_float(point.get("y"), None)}
|
||||
@@ -615,6 +1056,7 @@ class _NazcaInterconnectRoute:
|
||||
width=self._route_width(width),
|
||||
radius=self._route_radius(radius),
|
||||
xs=self._route_xs(xs),
|
||||
Lstart=kwargs.get("Lstart", 0),
|
||||
arrow=arrow,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user