77"""
88
99from __future__ import annotations
10-
1110from dataclasses import dataclass
1211from pathlib import Path
1312import struct
1413from typing import Dict , List , Tuple
15-
1614import numpy as np
1715
18-
1916# Quantization tolerance used to merge numerically close vertices.
2017VERTEX_MERGE_TOL = 1e-6
2118
22- # ======================================================================================
23- # VS CODE QUICK-RUN SETTINGS
24- # ======================================================================================
25- # Set your STL path here, then run this file directly in VS Code.
26- INPUT_STL_PATH = "src/ceasiompy/STL2CPACS/SOARminSting-Shell.stl"
27-
28- # Set output directory for split parts. One STL per detected component.
29- OUTPUT_SPLIT_DIR = "src/ceasiompy/STL2CPACS/split_output"
30-
3119# Connectivity tolerance used while grouping triangles into disconnected parts.
3220DEFAULT_VERTEX_TOL = VERTEX_MERGE_TOL
3321DEFAULT_FEATURE_ANGLE_DEG = 55.0
3422SIGNIFICANT_COMPONENT_MIN_TRIS = 100
3523
3624# ======================================================================================
37- # HOW THE SPLIT WORKS (high-level)
25+ # HOW THE SPLIT WORKS
3826# ======================================================================================
39- # The splitter does NOT try to understand aircraft semantics (wing/fuselage/etc.).
40- # It only uses mesh connectivity.
41- #
4227# 1) Read STL triangles as an array with shape (N, 3, 3)
4328# N triangles, each triangle has 3 vertices, each vertex has (x, y, z).
4429#
5641# - A fully connected STL => one component file.
5742# - A multi-part STL => one file per disconnected part.
5843
59-
6044@dataclass
6145class ComponentInfo :
6246 """Container for one split component."""
@@ -79,8 +63,6 @@ def read_ascii_stl(path: str | Path) -> np.ndarray:
7963 _ , x , y , z = line .split ()[:4 ]
8064 tri .append ([float (x ), float (y ), float (z )])
8165
82- if len (tri ) % 3 != 0 :
83- raise ValueError (f"Malformed ASCII STL: { path } " )
8466
8567 return np .asarray (tri , dtype = float ).reshape (- 1 , 3 , 3 )
8668
@@ -96,14 +78,14 @@ def read_binary_stl(path: str | Path) -> np.ndarray:
9678 tri = []
9779 offset = 0
9880 for _ in range (ntri ):
99- offset += 12 # normal
81+ offset += 12
10082 v1 = struct .unpack_from ("<fff" , data , offset )
10183 offset += 12
10284 v2 = struct .unpack_from ("<fff" , data , offset )
10385 offset += 12
10486 v3 = struct .unpack_from ("<fff" , data , offset )
10587 offset += 12
106- offset += 2 # attribute byte count
88+ offset += 2
10789 tri .append ([v1 , v2 , v3 ])
10890
10991 return np .asarray (tri , dtype = float )
@@ -125,7 +107,10 @@ def load_stl_auto(path: str | Path) -> np.ndarray:
125107 return read_binary_stl (path )
126108
127109
128- def write_binary_stl (path : str | Path , triangles : np .ndarray , solid_name : str = "component" ) -> None :
110+ def write_binary_stl (path : str | Path ,
111+ triangles : np .ndarray ,
112+ solid_name : str = "component"
113+ ) -> None :
129114 """Write triangles to a binary STL file."""
130115
131116 tris = np .asarray (triangles , dtype = np .float32 ).reshape (- 1 , 3 , 3 )
@@ -171,7 +156,9 @@ def _triangle_adjacency_from_shared_vertices(triangles: np.ndarray, tol: float =
171156 return [list (nei ) for nei in adjacency ]
172157
173158
174- def _triangle_adjacency_from_shared_edges (triangles : np .ndarray , tol : float = VERTEX_MERGE_TOL ) -> List [List [int ]]:
159+ def _triangle_adjacency_from_shared_edges (triangles : np .ndarray ,
160+ tol : float = VERTEX_MERGE_TOL
161+ ) -> List [List [int ]]:
175162 """Build triangle adjacency from shared *manifold* edges.
176163
177164 Used as a fallback when vertex-connectivity yields a single component.
@@ -252,7 +239,9 @@ def _triangle_normals(triangles: np.ndarray) -> np.ndarray:
252239
253240
254241def _triangle_adjacency_from_smooth_shared_edges (
255- triangles : np .ndarray , tol : float = VERTEX_MERGE_TOL , max_dihedral_deg : float = DEFAULT_FEATURE_ANGLE_DEG
242+ triangles : np .ndarray ,
243+ tol : float = VERTEX_MERGE_TOL ,
244+ max_dihedral_deg : float = DEFAULT_FEATURE_ANGLE_DEG
256245) -> List [List [int ]]:
257246 """Build adjacency using only manifold edges with smooth dihedral angle."""
258247
@@ -336,7 +325,9 @@ def _extract_components_from_adjacency(adjacency: List[List[int]]) -> List[np.nd
336325 return components
337326
338327
339- def _connected_triangle_components (triangles : np .ndarray , tol : float = VERTEX_MERGE_TOL ) -> List [np .ndarray ]:
328+ def _connected_triangle_components (triangles : np .ndarray ,
329+ tol : float = VERTEX_MERGE_TOL
330+ ) -> List [np .ndarray ]:
340331 """Split triangles into components with robust auto-connectivity.
341332
342333 We treat triangles as nodes of a graph:
@@ -359,7 +350,9 @@ def _connected_triangle_components(triangles: np.ndarray, tol: float = VERTEX_ME
359350 return _extract_components_from_adjacency (edge_adj )
360351
361352
362- def _count_significant_components (components : List [np .ndarray ], min_triangles : int = SIGNIFICANT_COMPONENT_MIN_TRIS ) -> int :
353+ def _count_significant_components (components : List [np .ndarray ],
354+ min_triangles : int = SIGNIFICANT_COMPONENT_MIN_TRIS
355+ ) -> int :
363356 """Count components with enough triangles to be meaningful geometric parts."""
364357
365358 return int (sum (1 for c in components if len (c ) >= min_triangles ))
@@ -434,7 +427,9 @@ def _histogram_valley_threshold(values: np.ndarray, n_bins: int = 80) -> float |
434427 return float (0.5 * (edges [best_i ] + edges [best_i + 1 ]))
435428
436429
437- def _induced_components_from_mask (adjacency : List [List [int ]], mask : np .ndarray ) -> List [np .ndarray ]:
430+ def _induced_components_from_mask (adjacency : List [List [int ]],
431+ mask : np .ndarray
432+ ) -> List [np .ndarray ]:
438433 """Connected components of the subgraph induced by `mask`."""
439434
440435 mask = np .asarray (mask , dtype = bool )
@@ -458,7 +453,9 @@ def _induced_components_from_mask(adjacency: List[List[int]], mask: np.ndarray)
458453
459454
460455def _span_split_largest_component (
461- triangles : np .ndarray , comp_indices : List [np .ndarray ], tol : float = VERTEX_MERGE_TOL
456+ triangles : np .ndarray ,
457+ comp_indices : List [np .ndarray ],
458+ tol : float = VERTEX_MERGE_TOL
462459) -> List [np .ndarray ]:
463460 """Split one dominant shell into inboard/outboard parts using |y| valley."""
464461
@@ -498,7 +495,9 @@ def _span_split_largest_component(
498495 return remapped
499496
500497
501- def _build_generic_components (disconnected_components : List [np .ndarray ], triangles : np .ndarray ) -> List [ComponentInfo ]:
498+ def _build_generic_components (disconnected_components : List [np .ndarray ],
499+ triangles : np .ndarray
500+ ) -> List [ComponentInfo ]:
502501 """Build generic component objects from connected triangle groups.
503502
504503 This function only packages metadata and names:
@@ -667,7 +666,6 @@ def split_aircraft_stl(
667666 return components
668667
669668
670-
671669def split_main (stl_path : str | Path , namefile : str , out_dir : str | Path ) -> Path :
672670
673671 vertex_tol = DEFAULT_VERTEX_TOL
@@ -683,11 +681,10 @@ def split_main(stl_path: str | Path, namefile: str, out_dir: str | Path) -> Path
683681 split_dir = Path (out_dir ) / "STL2CPACS"
684682 print (f"Output dir: { split_dir } " )
685683
686- comps = split_aircraft_stl (stl_path , output_dir = out_dir , vertex_tol = vertex_tol ,name = namefile )
684+ comps = split_aircraft_stl (stl_path , output_dir = out_dir , vertex_tol = vertex_tol , name = namefile )
687685 if not comps :
688686 print ("No triangles found." )
689687 else :
690688 print (f"\n Wrote { len (comps )} split STL file(s) into: { split_dir } " )
691689
692690 return split_dir
693-
0 commit comments