| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
|
|
| from .models import Design |
|
|
|
|
| def clamp(value: float, low: float, high: float) -> float: |
| return max(low, min(high, value)) |
|
|
|
|
| @dataclass |
| class Mesh: |
| nodes: list[dict] |
| tets: list[list[int]] |
| length: float |
| width: float |
| height: float |
|
|
|
|
| def point_in_feature_volume(point: dict, design: Design) -> bool: |
| x, y, z = point["x"], point["y"], point["z"] |
| length = design.base_length_mm |
| half_width = design.base_width_mm / 2 |
| thickness = design.base_thickness_mm |
| solid = 0 <= x <= length and -half_width <= y <= half_width and 0 <= z <= thickness |
|
|
| if solid and z <= thickness: |
| for hole in design.fixed_holes: |
| if ((x - hole.x) ** 2 + (y - hole.y) ** 2) ** 0.5 < hole.radius: |
| solid = False |
| for feature in design.features: |
| if feature.type == "lightening_hole" and ((x - feature.x) ** 2 + (y - feature.y) ** 2) ** 0.5 < feature.radius: |
| solid = False |
|
|
| for feature in design.features: |
| if feature.type == "boss": |
| radius = clamp(feature.radius, 1, 20) |
| height = clamp(feature.height, 1, 40) |
| if ((x - feature.x) ** 2 + (y - feature.y) ** 2) ** 0.5 <= radius and thickness <= z <= thickness + height: |
| solid = True |
|
|
| if feature.type == "rib": |
| ax, ay, bx, by = feature.x, feature.y, feature.x2, feature.y2 |
| vx, vy = bx - ax, by - ay |
| length_sq = max(vx * vx + vy * vy, 1) |
| t = clamp(((x - ax) * vx + (y - ay) * vy) / length_sq, 0, 1) |
| px, py = ax + t * vx, ay + t * vy |
| rib_height = clamp(feature.height, 1, 60) |
| if ((x - px) ** 2 + (y - py) ** 2) ** 0.5 <= max(feature.width, 1) / 2 and thickness <= z <= thickness + rib_height: |
| solid = True |
|
|
| return solid |
|
|
|
|
| def build_structured_tet_mesh(design: Design, nx: int = 7, ny: int = 5, nz: int = 5) -> Mesh: |
| length = clamp(design.base_length_mm, 30, 240) |
| width = clamp(design.base_width_mm, 10, 120) |
| thickness = clamp(design.base_thickness_mm, 1, 30) |
| max_feature_height = max( |
| [feature.height for feature in design.features if feature.type in {"rib", "boss"}] or [0] |
| ) |
| height = max(thickness + max_feature_height, thickness * 2) |
| nodes: list[dict] = [] |
| node_id: dict[tuple[int, int, int], int] = {} |
| tets: list[list[int]] = [] |
|
|
| def grid_point(i: int, j: int, k: int) -> dict: |
| return { |
| "x": length * i / nx, |
| "y": -width / 2 + width * j / ny, |
| "z": height * k / nz, |
| } |
|
|
| def add_node(i: int, j: int, k: int) -> int: |
| key = (i, j, k) |
| if key in node_id: |
| return node_id[key] |
| idx = len(nodes) |
| nodes.append({"id": idx, **grid_point(i, j, k)}) |
| node_id[key] = idx |
| return idx |
|
|
| tet_pattern = [ |
| [0, 1, 3, 7], |
| [0, 3, 2, 7], |
| [0, 2, 6, 7], |
| [0, 6, 4, 7], |
| [0, 4, 5, 7], |
| [0, 5, 1, 7], |
| ] |
|
|
| for i in range(nx): |
| for j in range(ny): |
| for k in range(nz): |
| p0 = grid_point(i, j, k) |
| p1 = grid_point(i + 1, j + 1, k + 1) |
| center = {"x": (p0["x"] + p1["x"]) / 2, "y": (p0["y"] + p1["y"]) / 2, "z": (p0["z"] + p1["z"]) / 2} |
| if not point_in_feature_volume(center, design): |
| continue |
| corners = [ |
| add_node(i, j, k), |
| add_node(i + 1, j, k), |
| add_node(i, j + 1, k), |
| add_node(i + 1, j + 1, k), |
| add_node(i, j, k + 1), |
| add_node(i + 1, j, k + 1), |
| add_node(i, j + 1, k + 1), |
| add_node(i + 1, j + 1, k + 1), |
| ] |
| for tet in tet_pattern: |
| tets.append([corners[idx] for idx in tet]) |
|
|
| return Mesh(nodes=nodes, tets=tets, length=length, width=width, height=height) |
|
|
|
|