@@ -121,7 +121,9 @@ def _reverse(edge: _Ctx) -> _Ctx:
return output
def _join ( edges : list [ _Ctx ] ) - > list [ _Ctx ] :
def _join ( edges : list [ _Ctx ] , * , allow_open : bool = False ) - > list [ _Ctx ] :
# 将边排序成一条连通链。allow_open=False(默认)要求首尾相接成闭合环;
# allow_open=True 时允许链首尾不接(供开放轮廓先拼链、后补闭合边)。
if not edges :
return [ ]
remaining = [ deepcopy ( edge ) for edge in edges ]
@@ -137,11 +139,22 @@ def _join(edges: list[_Ctx]) -> list[_Ctx]:
break
else :
raise ValueError ( " analytic_contours: segments do not form a connected contour " )
if _distance ( ordered [ 0 ] [ " start_mm " ] , ordered [ - 1 ] [ " end_mm " ] ) > _TOLERANCE_MM :
if not allow_open and _distance ( ordered [ 0 ] [ " start_mm " ] , ordered [ - 1 ] [ " end_mm " ] ) > _TOLERANCE_MM :
raise ValueError ( " analytic_contours: closed contour endpoints do not meet " )
return ordered
def _close_open_contour ( edges : list [ _Ctx ] ) - > tuple [ list [ _Ctx ] , bool ] :
# 开放链补闭合边:若首尾未相接,则沿两点连线补一条直线边形成闭合环。
# 返回 (edges, opened); opened=False 表示首尾已天然相接(无需补边)。
if len ( edges ) < 2 :
raise ValueError ( " analytic_contours: open contour needs at least 2 connected segments " )
if _distance ( edges [ 0 ] [ " start_mm " ] , edges [ - 1 ] [ " end_mm " ] ) < = _TOLERANCE_MM :
return edges , False
closing = _contour_line ( edges [ - 1 ] [ " end_mm " ] [ : 2 ] , edges [ 0 ] [ " start_mm " ] [ : 2 ] )
return [ * edges , closing ] , True
def _circle_edges ( segment : _Ctx ) - > list [ _Ctx ] :
center = segment . get ( " center " ) or [ 0.0 , 0.0 ]
radius = float ( segment . get ( " radius_mm " ) or 0.0 )
@@ -239,22 +252,35 @@ def _gen_analytic_contours(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[
loops : list [ _Ctx ] = [ ]
entities : list [ _Ctx ] = [ ]
for index , contour in enumerate ( profile . get ( " contours " ) or [ ] ) :
if not contour . get ( " closed " ) :
raise ValueError ( f " analytic_contours: contour { index } is open " )
# 开放轮廓(closed=false / role=open):先按开放链拼装,再在首尾间补一条
# 闭合边成闭合 region —— V 槽/开放型腔切除的刀具截面本就靠这条"槽口边"
# 闭合,故 B-rep 层可完全复用闭合链路。
contour_open = not bool ( contour . get ( " closed " , False ) )
if contour_open and contour . get ( " role " ) == " inner " :
raise ValueError ( f " analytic_contours: inner contour { index } cannot be open " )
raw_edges : list [ _Ctx ] = [ ]
for segment in contour . get ( " segments " ) or [ ] :
if segment . get ( " type " ) == " line " :
entities . append ( _line ( segment [ " start " ] , segment [ " end " ] ) )
elif segment . get ( " type " ) == " circle " :
if contour_open :
raise ValueError ( f " analytic_contours: open contour { index } cannot contain a full circle segment " )
entities . append ( _circle ( segment . get ( " center " ) or [ 0.0 , 0.0 ] , float ( segment . get ( " radius_mm " ) or 0.0 ) ) )
raw_edges . extend ( _segment_edges ( segment ) )
if raw_edges :
edges = _join ( raw_edges )
edges = _join ( raw_edges , allow_open = contour_open )
contour_opened = False
if contour_open :
if _distance ( edges [ 0 ] [ " start_mm " ] , edges [ - 1 ] [ " end_mm " ] ) < = _TOLERANCE_MM :
raise ValueError ( f " analytic_contours: contour { index } is geometrically closed; use closed=true " )
edges , contour_opened = _close_open_contour ( edges )
meta [ " _has_open_contour " ] = True
_normalize_quarter_rounding_direction ( edges )
sample = _sample_loop ( edges )
if _area ( sample ) < = _TOLERANCE_MM * _TOLERANCE_MM :
raise ValueError ( f " analytic_contours: contour { index } is degenerate " )
loops . append ( { " edges " : edges , " points " : sample , " area " : _area ( sample ) } )
loops . append ( { " edges " : edges , " points " : sample , " area " : _area ( sample ) ,
" open " : contour_open and contour_opened } )
for segment in profile . get ( " construction " ) or [ ] :
if segment . get ( " type " ) == " line " :
entities . append ( _line ( segment [ " start " ] , segment [ " end " ] , construction = True ) )
@@ -265,11 +291,13 @@ def _gen_analytic_contours(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[
for loop in loops :
loop [ " role " ] = " inner " if sum ( _contains ( loop [ " points " ] [ 0 ] , other [ " points " ] ) for other in loops if other is not loop ) % 2 else " outer "
outers = [ loop for loop in loops if loop [ " role " ] == " outer " ]
regions = [ { " outer " : outer [ " edges " ] , " holes " : [ ] } for outer in outers ]
regions = [ { " outer " : outer [ " edges " ] , " holes " : [ ] , " open " : bool ( outer . get ( " open " ) ) } for outer in outers ]
for inner in ( loop for loop in loops if loop [ " role " ] == " inner " ) :
containing = [ outer for outer in outers if _contains ( inner [ " points " ] [ 0 ] , outer [ " points " ] ) ]
if not containing :
raise ValueError ( " analytic_contours: inner contour has no containing outer contour " )
if any ( outer . get ( " open " ) for outer in containing ) :
raise ValueError ( " analytic_contours: an open outer contour cannot contain nested holes " )
selected = min ( containing , key = lambda outer : outer [ " area " ] )
regions [ outers . index ( selected ) ] [ " holes " ] . append ( inner [ " edges " ] )
meta [ " _regions " ] = regions
@@ -300,7 +328,7 @@ def resolve_profile(sketch: _Ctx) -> _Ctx:
generator = SHAPE_GENERATORS . get ( profile . get ( " type " ) )
if generator is None :
raise ValueError ( f " sketch { sketch . get ( ' id ' ) } : unsupported profile type { profile . get ( ' type ' ) !r} " )
meta : _Ctx = { " id " : sketch . get ( " id " ) , " _entities " : sketch . get ( " entities " ) , " _regions " : [ ] }
meta : _Ctx = { " id " : sketch . get ( " id " ) , " _entities " : sketch . get ( " entities " ) , " _regions " : [ ] , " _has_open_contour " : False }
entities , contour = generator ( profile , meta )
output = deepcopy ( sketch )
original_circles = [ entity for entity in sketch . get ( " entities " ) or [ ] if entity . get ( " type " ) == " circle " and not entity . get ( " construction " ) ]
@@ -310,9 +338,13 @@ def resolve_profile(sketch: _Ctx) -> _Ctx:
output [ " contour_edges_mm " ] = _transform_contours ( contour , workplane ) if workplane else contour
if meta [ " _regions " ] :
output [ " contour_regions_mm " ] = [
{ " outer " : _transform_contours ( region [ " outer " ] , workplane ) if workplane else region [ " outer " ] , " holes " : [ _transform_contours ( hole , workplane ) if workplane else hole for hole in region . get ( " holes " ) or [ ] ] }
{ " outer " : _transform_contours ( region [ " outer " ] , workplane ) if workplane else region [ " outer " ] ,
" holes " : [ _transform_contours ( hole , workplane ) if workplane else hole for hole in region . get ( " holes " ) or [ ] ] ,
" open " : bool ( region . get ( " open " ) ) }
for region in meta [ " _regions " ]
]
if meta [ " _has_open_contour " ] :
output [ " _open_contour " ] = True
return output