ecopus commited on
Commit
b8f6abf
·
verified ·
1 Parent(s): 7c69e45

Upload 4 files

Browse files
mesh_service/feature_level_augmented.csv ADDED
The diff for this file is too large to render. See raw diff
 
mesh_service/part_level.csv ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ part_id,num_fillet,num_hole,num_plane,smallest_fillet_r,smallest_hole_r,load_face,load_direction,load_scale,global_size_initial,global_size_final,converged,top2_features,local_refine_needed,local_size,,thickness,maxsize,load_id
2
+ 1,4,4,6,3.0226,2.4,bend_bottom,-1,low,3.775,2.4160000000000004,0,"FILLET,FACE",1,0.7644375000000001,,3.02,25.399999618530273,1
3
+ 1,4,4,6,3.0226,2.4,bend_bottom,-1,medium,3.775,2.4160000000000004,1,"FILLET,FACE",1,1.0192500000000002,,3.02,25.399999618530273,2
4
+ 1,4,4,6,3.0226,2.4,bend_bottom,-1,high,3.775,2.4160000000000004,1,"FILLET,FACE",1,1.0192500000000002,,3.02,25.399999618530273,3
5
+ 1,4,4,6,3.0226,2.4,tension,-1,low,3.775,2.4160000000000004,1,"CIRC_HOLE,FACE",1,0.7644375000000001,,3.02,25.399999618530273,4
6
+ 1,4,4,6,3.0226,2.4,tension,-1,medium,3.775,2.4160000000000004,1,"CIRC_HOLE,FACE",1,0.7644375000000001,,3.02,25.399999618530273,5
7
+ 1,4,4,6,3.0226,2.4,tension,-1,high,3.775,2.4160000000000004,1,"CIRC_HOLE,FACE",1,0.7644375000000001,,3.02,25.399999618530273,6
8
+ 1,4,4,6,3.0226,2.4,bend_bottom,1,low,3.775,2.4160000000000004,1,"FILLET,FACE",1,1.0192500000000002,,3.02,25.399999618530273,7
9
+ 1,4,4,6,3.0226,2.4,bend_bottom,1,medium,3.775,2.4160000000000004,1,"FILLET,FACE",1,1.0192500000000002,,3.02,25.399999618530273,8
10
+ 1,4,4,6,3.0226,2.4,bend_bottom,1,high,3.775,2.4160000000000004,1,"FILLET,FACE",1,1.0192500000000002,,3.02,25.399999618530273,9
11
+ 1,4,4,6,3.0226,2.4,tension,1,low,3.775,2.4160000000000004,1,"CIRC_HOLE,FACE",1,1.0192500000000002,,3.02,25.399999618530273,10
12
+ 1,4,4,6,3.0226,2.4,tension,1,medium,3.775,2.4160000000000004,1,"CIRC_HOLE,CIRC_HOLE",1,0.7644375000000001,,3.02,25.399999618530273,11
13
+ 1,4,4,6,3.0226,2.4,tension,1,high,3.775,2.4160000000000004,1,"CIRC_HOLE,CIRC_HOLE",1,1.0192500000000002,,3.02,25.399999618530273,12
14
+ 2,2,4,9,0.80264,2.4,bend_bottom,-1,low,2.5,1.6,1,"FILLET,FACE",1,0.675,,2,23.012399673461914,13
15
+ 2,2,4,9,0.80264,2.4,bend_bottom,-1,medium,2.5,1.6,1,"FILLET,FACE",1,0.675,,2,23.012399673461914,14
16
+ 2,2,4,9,0.80264,2.4,bend_bottom,-1,high,2.5,1.6,1,"FILLET,FACE",1,0.675,,2,23.012399673461914,15
17
+ 2,2,4,9,0.80264,2.4,tension,-1,low,2.5,1.6,1,"CIRC_HOLE,CIRC_HOLE",1,0.5062500000000001,,2,23.012399673461914,16
18
+ 2,2,4,9,0.80264,2.4,tension,-1,medium,2.5,1.6,1,"CIRC_HOLE,CIRC_HOLE",1,0.5062500000000001,,2,23.012399673461914,17
19
+ 2,2,4,9,0.80264,2.4,tension,-1,high,2.5,1.6,1,"CIRC_HOLE,CIRC_HOLE",1,0.5062500000000001,,2,23.012399673461914,18
20
+ 2,2,4,9,0.80264,2.4,bend_bottom,1,low,2.5,1.6,1,"FILLET,FACE",1,0.675,,2,23.012399673461914,19
21
+ 2,2,4,9,0.80264,2.4,bend_bottom,1,medium,2.5,1.6,1,"FILLET,FACE",1,0.675,,2,23.012399673461914,20
22
+ 2,2,4,9,0.80264,2.4,bend_bottom,1,high,2.5,1.6,1,"FILLET,FACE",1,0.675,,2,23.012399673461914,21
23
+ 2,2,4,9,0.80264,2.4,tension,1,low,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.675,,2,23.012399673461914,22
24
+ 2,2,4,9,0.80264,2.4,tension,1,medium,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.37968750000000007,,2,23.012399673461914,23
25
+ 2,2,4,9,0.80264,2.4,tension,1,high,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.675,,2,23.012399673461914,24
26
+ 8,3,8,17,0.66675,3.5433,bend_bottom,-1,low,3.3375,1.8773437499999999,1,"FACE,FACE",1,1.8773437499999999,,2.67,63.5,25
27
+ 8,3,8,17,0.66675,3.5433,bend_bottom,-1,medium,3.3375,1.8773437499999999,1,"FACE,FACE",1,1.8773437499999999,,2.67,63.5,26
28
+ 8,3,8,17,0.66675,3.5433,bend_bottom,-1,high,3.3375,1.8773437499999999,1,"FACE,FACE",1,1.8773437499999999,,2.67,63.5,27
29
+ 8,3,8,17,0.66675,3.5433,tension,-1,low,3.3375,2.136,1,"CIRC_HOLE,FACE",1,0.67584375,,2.67,63.5,28
30
+ 8,3,8,17,0.66675,3.5433,tension,-1,medium,3.3375,2.136,0,"CIRC_HOLE,FACE",1,0.67584375,,2.67,63.5,29
31
+ 8,3,8,17,0.66675,3.5433,tension,-1,high,3.3375,2.136,0,"CIRC_HOLE,FACE",1,0.67584375,,2.67,63.5,30
32
+ 8,3,8,17,0.66675,3.5433,bend_bottom,1,low,3.3375,2.136,1,"FACE,CIRC_HOLE",1,0.901125,,2.67,63.5,31
33
+ 8,3,8,17,0.66675,3.5433,bend_bottom,1,medium,3.3375,2.136,1,"FACE,CIRC_HOLE",1,0.901125,,2.67,63.5,32
34
+ 8,3,8,17,0.66675,3.5433,bend_bottom,1,high,3.3375,2.136,1,"FACE,CIRC_HOLE",1,0.901125,,2.67,63.5,33
35
+ 8,3,8,17,0.66675,3.5433,tension,1,low,3.3375,2.136,1,"CIRC_HOLE,FACE",1,0.901125,,2.67,63.5,34
36
+ 8,3,8,17,0.66675,3.5433,tension,1,medium,3.3375,2.136,1,"CIRC_HOLE,FACE",1,0.901125,,2.67,63.5,35
37
+ 8,3,8,17,0.66675,3.5433,tension,1,high,3.3375,2.136,1,"CIRC_HOLE,FACE",1,0.901125,,2.67,63.5,36
38
+ 12,1,12,21,2.032,2.7432,bend_bottom,-1,low,6.35,2.143125,0,"FACE,FACE",1,1.60734375,,4,127,37
39
+ 12,1,12,21,2.032,2.7432,bend_bottom,-1,medium,6.35,2.143125,0,"FACE,FACE",1,1.60734375,,4,127,38
40
+ 12,1,12,21,2.032,2.7432,bend_bottom,-1,high,6.35,2.143125,0,"FACE,FACE",1,1.60734375,,4,127,39
41
+ 12,1,12,21,2.032,2.7432,tension,-1,low,6.35,4.064,1,"CIRC_HOLE,FACE",1,2.286,,4,127,40
42
+ 12,1,12,21,2.032,2.7432,tension,-1,medium,6.35,4.064,1,"CIRC_HOLE,FACE",1,2.286,,4,127,41
43
+ 12,1,12,21,2.032,2.7432,tension,-1,high,6.35,4.064,1,"CIRC_HOLE,FACE",1,2.286,,4,127,42
44
+ 12,1,12,21,2.032,2.7432,bend_bottom,1,low,6.35,3.2512000000000003,1,"CIRC_HOLE,FACE",1,0.7715250000000001,,4,127,43
45
+ 12,1,12,21,2.032,2.7432,bend_bottom,1,medium,6.35,3.2512000000000003,1,"CIRC_HOLE,FACE",1,0.7715250000000001,,4,127,44
46
+ 12,1,12,21,2.032,2.7432,bend_bottom,1,high,6.35,3.2512000000000003,0,"FACE,CIRC_HOLE",1,1.0287000000000002,,4,127,45
47
+ 12,1,12,21,2.032,2.7432,tension,1,low,6.35,4.064,1,"CIRC_HOLE,FACE",1,2.286,,4,127,46
48
+ 12,1,12,21,2.032,2.7432,tension,1,medium,6.35,4.064,1,"CIRC_HOLE,FACE",1,2.286,,4,127,47
49
+ 12,1,12,21,2.032,2.7432,tension,1,high,6.35,4.064,1,"CIRC_HOLE,FACE",1,2.286,,4,127,48
50
+ 13,1,12,21,2.413,2.8956,bend_bottom,-1,low,7.619999694824244,4.286249828338637,1,"FILLET,FACE",1,2.4110155284404837,,4.83,152.3999938964849,49
51
+ 13,1,12,21,2.413,2.8956,bend_bottom,-1,medium,7.619999694824244,4.286249828338637,1,"FILLET,FACE",1,2.4110155284404837,,4.83,152.3999938964849,50
52
+ 13,1,12,21,2.413,2.8956,bend_bottom,-1,high,7.619999694824244,4.286249828338637,1,"FILLET,FACE",1,2.4110155284404837,,4.83,152.3999938964849,51
53
+ 13,1,12,21,2.413,2.8956,tension,-1,low,7.619999694824244,3.9014398437500137,1,"FACE,CIRC_HOLE",1,1.2344399505615278,,4.83,152.3999938964849,52
54
+ 13,1,12,21,2.413,2.8956,tension,-1,medium,7.619999694824244,3.9014398437500137,1,"FACE,CIRC_HOLE",1,1.2344399505615278,,4.83,152.3999938964849,53
55
+ 13,1,12,21,2.413,2.8956,tension,-1,high,7.619999694824244,3.9014398437500137,1,"FACE,CIRC_HOLE",1,1.2344399505615278,,4.83,152.3999938964849,54
56
+ 13,1,12,21,2.413,2.8956,bend_bottom,1,low,7.619999694824244,3.9014398437500137,0,"CIRC_HOLE,FACE",1,1.2344399505615278,,4.83,152.3999938964849,55
57
+ 13,1,12,21,2.413,2.8956,bend_bottom,1,medium,7.619999694824244,3.9014398437500137,0,"CIRC_HOLE,FACE",1,1.2344399505615278,,4.83,152.3999938964849,56
58
+ 13,1,12,21,2.413,2.8956,bend_bottom,1,high,7.619999694824244,4.286249828338637,1,"FACE,CIRC_HOLE",1,3.2146873712539783,,4.83,152.3999938964849,57
59
+ 13,1,12,21,2.413,2.8956,tension,1,low,7.619999694824244,3.9014398437500137,0,"FACE,CIRC_HOLE",1,1.2344399505615278,,4.83,152.3999938964849,58
60
+ 13,1,12,21,2.413,2.8956,tension,1,medium,7.619999694824244,3.9014398437500137,0,"FACE,CIRC_HOLE",1,1.2344399505615278,,4.83,152.3999938964849,59
61
+ 13,1,12,21,2.413,2.8956,tension,1,high,7.619999694824244,3.9014398437500137,0,"FACE,CIRC_HOLE",1,1.2344399505615278,,4.83,152.3999938964849,60
62
+ 14,0,12,22,4.191,2.921,bend_bottom,-1,low,10.159999525547027,4.286249799840152,1,"FACE,FACE",1,4.286249799840152,,5.6,203.19999051094055,61
63
+ 14,0,12,22,4.191,2.921,bend_bottom,-1,medium,10.159999525547027,4.286249799840152,1,"FACE,FACE",1,4.286249799840152,,5.6,203.19999051094055,62
64
+ 14,0,12,22,4.191,2.921,bend_bottom,-1,high,10.159999525547027,6.095999715328216,1,"FACE,FACE",1,4.571999786496162,,5.6,203.19999051094055,63
65
+ 14,0,12,22,4.191,2.921,tension,-1,low,10.159999525547027,6.502399696350098,1,"CIRC_HOLE,FACE",1,3.6575998291969305,,5.6,203.19999051094055,64
66
+ 14,0,12,22,4.191,2.921,tension,-1,medium,10.159999525547027,6.502399696350098,1,"CIRC_HOLE,FACE",1,3.6575998291969305,,5.6,203.19999051094055,65
67
+ 14,0,12,22,4.191,2.921,tension,-1,high,10.159999525547027,6.502399696350098,1,"CIRC_HOLE,FACE",1,3.6575998291969305,,5.6,203.19999051094055,66
68
+ 14,0,12,22,4.191,2.921,bend_bottom,1,low,10.159999525547027,5.201919757080079,1,"CIRC_HOLE,FACE",1,1.6459199231386188,,5.6,203.19999051094055,67
69
+ 14,0,12,22,4.191,2.921,bend_bottom,1,medium,10.159999525547027,5.201919757080079,1,"CIRC_HOLE,FACE",1,1.6459199231386188,,5.6,203.19999051094055,68
70
+ 14,0,12,22,4.191,2.921,bend_bottom,1,high,10.159999525547027,5.201919757080079,1,"CIRC_HOLE,FACE",1,1.6459199231386188,,5.6,203.19999051094055,69
71
+ 14,0,12,22,4.191,2.921,tension,1,low,10.159999525547027,6.502399696350098,1,"CIRC_HOLE,FACE",1,3.6575998291969305,,5.6,203.19999051094055,70
72
+ 14,0,12,22,4.191,2.921,tension,1,medium,10.159999525547027,6.502399696350098,1,"CIRC_HOLE,FACE",1,3.6575998291969305,,5.6,203.19999051094055,71
73
+ 14,0,12,22,4.191,2.921,tension,1,high,10.159999525547027,6.502399696350098,1,"CIRC_HOLE,FACE",1,3.6575998291969305,,5.6,203.19999051094055,72
74
+ 15,1,16,25,3.175,2.7432,bend_bottom,-1,low,12.7,6.096,0,"FILLET,FACE",1,2.57175,,6.35,254,73
75
+ 15,1,16,25,3.175,2.7432,bend_bottom,-1,medium,12.7,6.096,0,"FILLET,FACE",1,2.57175,,6.35,254,74
76
+ 15,1,16,25,3.175,2.7432,bend_bottom,-1,high,12.7,6.096,0,"FILLET,CIRC_HOLE",1,1.9288125000000003,,6.35,254,75
77
+ 15,1,16,25,3.175,2.7432,tension,-1,low,12.7,6.502400000000001,1,"CIRC_HOLE,FACE",1,2.7432000000000003,,6.35,254,76
78
+ 15,1,16,25,3.175,2.7432,tension,-1,medium,12.7,6.502400000000001,1,"CIRC_HOLE,FACE",1,2.7432000000000003,,6.35,254,77
79
+ 15,1,16,25,3.175,2.7432,tension,-1,high,12.7,6.502400000000001,1,"CIRC_HOLE,FACE",1,2.7432000000000003,,6.35,254,78
80
+ 15,1,16,25,3.175,2.7432,bend_bottom,1,low,12.7,5.201920000000001,1,"CIRC_HOLE,FACE",1,1.6459200000000003,,6.35,254,79
81
+ 15,1,16,25,3.175,2.7432,bend_bottom,1,medium,12.7,5.201920000000001,1,"CIRC_HOLE,FACE",1,1.6459200000000003,,6.35,254,80
82
+ 15,1,16,25,3.175,2.7432,bend_bottom,1,high,12.7,5.201920000000001,1,"CIRC_HOLE,FACE",1,1.6459200000000003,,6.35,254,81
83
+ 15,1,16,25,3.175,2.7432,tension,1,low,12.7,6.502400000000001,1,"CIRC_HOLE,FACE",1,2.7432000000000003,,6.35,254,82
84
+ 15,1,16,25,3.175,2.7432,tension,1,medium,12.7,6.502400000000001,1,"CIRC_HOLE,FACE",1,2.7432000000000003,,6.35,254,83
85
+ 15,1,16,25,3.175,2.7432,tension,1,high,12.7,6.502400000000001,1,"CIRC_HOLE,FACE",1,2.7432000000000003,,6.35,254,84
86
+ 16,2,4,40,1.524,2.3495,bend_bottom,-1,low,2.5,1.6,0,"FACE,FILLET",1,0.5062500000000001,,2,25.399999499320984,85
87
+ 16,2,4,40,1.524,2.3495,bend_bottom,-1,medium,2.5,1.6,0,"FACE,FILLET",1,0.5062500000000001,,2,25.399999499320984,86
88
+ 16,2,4,40,1.524,2.3495,bend_bottom,-1,high,2.5,1.6,0,"FACE,FILLET",1,0.5062500000000001,,2,25.399999499320984,87
89
+ 16,2,4,40,1.524,2.3495,tension,-1,low,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.5062500000000001,,2,25.399999499320984,88
90
+ 16,2,4,40,1.524,2.3495,tension,-1,medium,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.675,,2,25.399999499320984,89
91
+ 16,2,4,40,1.524,2.3495,tension,-1,high,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.5062500000000001,,2,25.399999499320984,90
92
+ 16,2,4,40,1.524,2.3495,bend_bottom,1,low,2.5,1.6,0,"FACE,FILLET",1,0.5062500000000001,,2,25.399999499320984,91
93
+ 16,2,4,40,1.524,2.3495,bend_bottom,1,medium,2.5,1.6,0,"FACE,FILLET",1,0.5062500000000001,,2,25.399999499320984,92
94
+ 16,2,4,40,1.524,2.3495,bend_bottom,1,high,2.5,1.6,0,"FACE,FILLET",1,0.5062500000000001,,2,25.399999499320984,93
95
+ 16,2,4,40,1.524,2.3495,tension,1,low,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.675,,2,25.399999499320984,94
96
+ 16,2,4,40,1.524,2.3495,tension,1,medium,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.675,,2,25.399999499320984,95
97
+ 16,2,4,40,1.524,2.3495,tension,1,high,2.5,1.6,1,"CIRC_HOLE,FACE",1,0.675,,2,25.399999499320984,96
98
+ 17,2,8,16,0.889,2.3495,bend_bottom,-1,low,2.225,1.4240000000000004,0,"FILLET,FACE",1,0.4505625000000001,,1.78,38.099998474121094,97
99
+ 17,2,8,16,0.889,2.3495,bend_bottom,-1,medium,2.225,1.4240000000000004,0,"FILLET,FACE",1,0.4505625000000001,,1.78,38.099998474121094,98
100
+ 17,2,8,16,0.889,2.3495,bend_bottom,-1,high,2.225,1.4240000000000004,0,"FILLET,FACE",1,0.4505625000000001,,1.78,38.099998474121094,99
101
+ 17,2,8,16,0.889,2.3495,tension,-1,low,2.225,1.4240000000000004,1,"FACE,CIRC_HOLE",1,0.6007500000000001,,1.78,38.099998474121094,100
102
+ 17,2,8,16,0.889,2.3495,tension,-1,medium,2.225,1.4240000000000004,1,"CIRC_HOLE,FACE",1,0.6007500000000001,,1.78,38.099998474121094,101
103
+ 17,2,8,16,0.889,2.3495,tension,-1,high,2.225,1.4240000000000004,1,"CIRC_HOLE,FACE",1,0.6007500000000001,,1.78,38.099998474121094,102
104
+ 17,2,8,16,0.889,2.3495,bend_bottom,1,low,2.225,1.4240000000000004,1,"FILLET,CIRC_HOLE",1,0.1900810546875,,1.78,38.099998474121094,103
105
+ 17,2,8,16,0.889,2.3495,bend_bottom,1,medium,2.225,1.4240000000000004,1,"FILLET,CIRC_HOLE",1,0.1900810546875,,1.78,38.099998474121094,104
106
+ 17,2,8,16,0.889,2.3495,bend_bottom,1,high,2.225,1.4240000000000004,1,"FILLET,FACE",1,0.4505625000000001,,1.78,38.099998474121094,105
107
+ 17,2,8,16,0.889,2.3495,tension,1,low,2.225,1.4240000000000004,1,"FACE,CIRC_HOLE",1,0.6007500000000001,,1.78,38.099998474121094,106
108
+ 17,2,8,16,0.889,2.3495,tension,1,medium,2.225,1.4240000000000004,1,"FACE,CIRC_HOLE",1,0.6007500000000001,,1.78,38.099998474121094,107
109
+ 17,2,8,16,0.889,2.3495,tension,1,high,2.225,1.4240000000000004,1,"CIRC_HOLE,FACE",1,0.6007500000000001,,1.78,38.099998474121094,108
110
+ 18,2,8,16,1.3335,2.159,bend_bottom,-1,low,2.5399999648332594,1.4287499802187085,1,"FILLET,FACE",1,0.8036718638730235,,1.78,50.79999929666519,109
111
+ 18,2,8,16,1.3335,2.159,bend_bottom,-1,medium,2.5399999648332594,1.4287499802187085,1,"FILLET,FACE",1,0.8036718638730235,,1.78,50.79999929666519,110
112
+ 18,2,8,16,1.3335,2.159,bend_bottom,-1,high,2.5399999648332594,1.4287499802187085,1,"FILLET,FACE",1,0.8036718638730235,,1.78,50.79999929666519,111
113
+ 18,2,8,16,1.3335,2.159,tension,-1,low,2.5399999648332594,1.625599977493286,1,"FACE,CIRC_HOLE",1,0.6857999905049801,,1.78,50.79999929666519,112
114
+ 18,2,8,16,1.3335,2.159,tension,-1,medium,2.5399999648332594,1.625599977493286,1,"FACE,CIRC_HOLE",1,0.6857999905049801,,1.78,50.79999929666519,113
115
+ 18,2,8,16,1.3335,2.159,tension,-1,high,2.5399999648332594,1.625599977493286,1,"CIRC_HOLE,FACE",1,0.6857999905049801,,1.78,50.79999929666519,114
116
+ 18,2,8,16,1.3335,2.159,bend_bottom,1,low,2.5399999648332594,1.5239999788999556,0,"FACE,CIRC_HOLE",1,0.6429374910984187,,1.78,50.79999929666519,115
117
+ 18,2,8,16,1.3335,2.159,bend_bottom,1,medium,2.5399999648332594,1.5239999788999556,0,"FACE,CIRC_HOLE",1,0.6429374910984187,,1.78,50.79999929666519,116
118
+ 18,2,8,16,1.3335,2.159,bend_bottom,1,high,2.5399999648332594,1.5239999788999556,0,"FACE,CIRC_HOLE",1,0.6429374910984187,,1.78,50.79999929666519,117
119
+ 18,2,8,16,1.3335,2.159,tension,1,low,2.5399999648332594,1.625599977493286,1,"FACE,CIRC_HOLE",1,0.6857999905049801,,1.78,50.79999929666519,118
120
+ 18,2,8,16,1.3335,2.159,tension,1,medium,2.5399999648332594,1.625599977493286,1,"FACE,CIRC_HOLE",1,0.6857999905049801,,1.78,50.79999929666519,119
121
+ 18,2,8,16,1.3335,2.159,tension,1,high,2.5399999648332594,1.625599977493286,1,"FACE,CIRC_HOLE",1,0.6857999905049801,,1.78,50.79999929666519,120
122
+ 19,2,8,16,1.3335,2.159,bend_bottom,-1,low,3.174999812245369,1.1429999324083329,0,"FACE,FILLET",1,0.6429374619796873,,1.78,63.49999624490738,121
123
+ 19,2,8,16,1.3335,2.159,bend_bottom,-1,medium,3.174999812245369,1.1429999324083329,0,"FACE,FILLET",1,0.6429374619796873,,1.78,63.49999624490738,122
124
+ 19,2,8,16,1.3335,2.159,bend_bottom,-1,high,3.174999812245369,1.1429999324083329,0,"FACE,FILLET",1,0.6429374619796873,,1.78,63.49999624490738,123
125
+ 19,2,8,16,1.3335,2.159,tension,-1,low,3.174999812245369,1.6255999038696292,1,"CIRC_HOLE,FACE",1,0.6857999594449999,,1.78,63.49999624490738,124
126
+ 19,2,8,16,1.3335,2.159,tension,-1,medium,3.174999812245369,1.6255999038696292,1,"CIRC_HOLE,FACE",1,0.6857999594449999,,1.78,63.49999624490738,125
127
+ 19,2,8,16,1.3335,2.159,tension,-1,high,3.174999812245369,1.6255999038696292,1,"CIRC_HOLE,FACE",1,0.6857999594449999,,1.78,63.49999624490738,126
128
+ 19,2,8,16,1.3335,2.159,bend_bottom,1,low,3.174999812245369,1.5239999098777774,0,"FACE,CIRC_HOLE",1,0.6429374619796873,,1.78,63.49999624490738,127
129
+ 19,2,8,16,1.3335,2.159,bend_bottom,1,medium,3.174999812245369,1.1429999324083329,0,"FACE,FILLET",1,0.6429374619796873,,1.78,63.49999624490738,128
130
+ 19,2,8,16,1.3335,2.159,bend_bottom,1,high,3.174999812245369,1.1429999324083329,0,"FACE,FILLET",1,0.6429374619796873,,1.78,63.49999624490738,129
131
+ 19,2,8,16,1.3335,2.159,tension,1,low,3.174999812245369,1.6255999038696292,1,"CIRC_HOLE,FACE",1,0.6857999594449999,,1.78,63.49999624490738,130
132
+ 19,2,8,16,1.3335,2.159,tension,1,medium,3.174999812245369,1.6255999038696292,1,"CIRC_HOLE,FACE",1,0.6857999594449999,,1.78,63.49999624490738,131
133
+ 19,2,8,16,1.3335,2.159,tension,1,high,3.174999812245369,1.6255999038696292,1,"FACE,CIRC_HOLE",1,0.5143499695837499,,1.78,63.49999624490738,132
134
+ 20,1,8,17,1.905,2.2479,bend_bottom,-1,low,3.810000014305115,2.4384000091552736,1,"FILLET,FACE",1,0.7715250028967857,,2.54,76.2000002861023,133
135
+ 20,1,8,17,1.905,2.2479,bend_bottom,-1,medium,3.810000014305115,2.4384000091552736,1,"FILLET,FACE",1,0.7715250028967857,,2.54,76.2000002861023,134
136
+ 20,1,8,17,1.905,2.2479,bend_bottom,-1,high,3.810000014305115,2.4384000091552736,1,"FILLET,FACE",1,0.7715250028967857,,2.54,76.2000002861023,135
137
+ 20,1,8,17,1.905,2.2479,tension,-1,low,3.810000014305115,2.4384000091552736,1,"CIRC_HOLE,FACE",1,0.7715250028967857,,2.54,76.2000002861023,136
138
+ 20,1,8,17,1.905,2.2479,tension,-1,medium,3.810000014305115,2.4384000091552736,1,"CIRC_HOLE,FACE",1,0.7715250028967857,,2.54,76.2000002861023,137
139
+ 20,1,8,17,1.905,2.2479,tension,-1,high,3.810000014305115,2.4384000091552736,1,"CIRC_HOLE,FACE",1,0.7715250028967857,,2.54,76.2000002861023,138
140
+ 20,1,8,17,1.905,2.2479,bend_bottom,1,low,3.810000014305115,2.4384000091552736,1,"FACE,CIRC_HOLE",1,1.028700003862381,,2.54,76.2000002861023,139
141
+ 20,1,8,17,1.905,2.2479,bend_bottom,1,medium,3.810000014305115,2.4384000091552736,1,"FACE,CIRC_HOLE",1,1.3716000051498414,,2.54,76.2000002861023,140
142
+ 20,1,8,17,1.905,2.2479,bend_bottom,1,high,3.810000014305115,2.4384000091552736,1,"FILLET,FACE",1,0.7715250028967857,,2.54,76.2000002861023,141
143
+ 20,1,8,17,1.905,2.2479,tension,1,low,3.810000014305115,2.4384000091552736,1,"CIRC_HOLE,FACE",1,0.7715250028967857,,2.54,76.2000002861023,142
144
+ 20,1,8,17,1.905,2.2479,tension,1,medium,3.810000014305115,2.4384000091552736,1,"CIRC_HOLE,FACE",1,0.7715250028967857,,2.54,76.2000002861023,143
145
+ 20,1,8,17,1.905,2.2479,tension,1,high,3.810000014305115,2.4384000091552736,1,"FACE,CIRC_HOLE",1,0.7715250028967857,,2.54,76.2000002861023,144
146
+ 21,1,8,17,2.4765,2.8067,bend_bottom,-1,low,5.079999870061874,1.6073437088867648,1,"FACE,FACE",1,1.6073437088867648,,3.3,101.59999740123749,145
147
+ 21,1,8,17,2.4765,2.8067,bend_bottom,-1,medium,5.079999870061874,1.6073437088867648,0,"FACE,FACE",1,1.6073437088867648,,3.3,101.59999740123749,146
148
+ 21,1,8,17,2.4765,2.8067,bend_bottom,-1,high,5.079999870061874,2.8574999269098043,1,"FACE,CIRC_HOLE",1,2.8574999269098043,,3.3,101.59999740123749,147
149
+ 21,1,8,17,2.4765,2.8067,tension,-1,low,5.079999870061874,3.2511999168395995,0,"CIRC_HOLE,FACE",1,1.0286999736875297,,3.3,101.59999740123749,148
150
+ 21,1,8,17,2.4765,2.8067,tension,-1,medium,5.079999870061874,3.2511999168395995,0,"CIRC_HOLE,FACE",1,1.0286999736875297,,3.3,101.59999740123749,149
151
+ 21,1,8,17,2.4765,2.8067,tension,-1,high,5.079999870061874,3.2511999168395995,0,"CIRC_HOLE,FACE",1,1.0286999736875297,,3.3,101.59999740123749,150
152
+ 21,1,8,17,2.4765,2.8067,bend_bottom,1,low,5.079999870061874,3.2511999168395995,0,"CIRC_HOLE,FACE",1,1.0286999736875297,,3.3,101.59999740123749,151
153
+ 21,1,8,17,2.4765,2.8067,bend_bottom,1,medium,5.079999870061874,3.0479999220371248,1,"CIRC_HOLE,FACE",1,1.7144999561458827,,3.3,101.59999740123749,152
154
+ 21,1,8,17,2.4765,2.8067,bend_bottom,1,high,5.079999870061874,3.0479999220371248,1,"CIRC_HOLE,FACE",1,1.7144999561458827,,3.3,101.59999740123749,153
155
+ 21,1,8,17,2.4765,2.8067,tension,1,low,5.079999870061874,3.2511999168395995,0,"CIRC_HOLE,FACE",1,1.0286999736875297,,3.3,101.59999740123749,154
156
+ 21,1,8,17,2.4765,2.8067,tension,1,medium,5.079999870061874,3.2511999168395995,0,"CIRC_HOLE,FACE",1,1.0286999736875297,,3.3,101.59999740123749,155
157
+ 21,1,8,17,2.4765,2.8067,tension,1,high,5.079999870061874,3.2511999168395995,0,"CIRC_HOLE,FACE",1,1.0286999736875297,,3.3,101.59999740123749,156
158
+ 22,1,12,21,2.286,3.1242,bend_bottom,-1,low,7.619999694824219,3.2146873712539668,1,"FACE,FACE",1,3.2146873712539668,,4.57,152.39999389648438,157
159
+ 22,1,12,21,2.286,3.1242,bend_bottom,-1,medium,7.619999694824219,3.2146873712539668,1,"FACE,FACE",1,3.2146873712539668,,4.57,152.39999389648438,158
160
+ 22,1,12,21,2.286,3.1242,bend_bottom,-1,high,7.619999694824219,3.2146873712539668,1,"FACE,CIRC_HOLE",1,3.2146873712539668,,4.57,152.39999389648438,159
161
+ 22,1,12,21,2.286,3.1242,tension,-1,low,7.619999694824219,3.901439843750001,1,"CIRC_HOLE,FACE",1,1.6459199340820316,,4.57,152.39999389648438,160
162
+ 22,1,12,21,2.286,3.1242,tension,-1,medium,7.619999694824219,3.901439843750001,1,"CIRC_HOLE,FACE",1,1.6459199340820316,,4.57,152.39999389648438,161
163
+ 22,1,12,21,2.286,3.1242,tension,-1,high,7.619999694824219,3.901439843750001,1,"CIRC_HOLE,FACE",1,1.6459199340820316,,4.57,152.39999389648438,162
164
+ 22,1,12,21,2.286,3.1242,bend_bottom,1,low,7.619999694824219,3.901439843750001,0,"FACE,CIRC_HOLE",1,1.2344399505615238,,4.57,152.39999389648438,163
165
+ 22,1,12,21,2.286,3.1242,bend_bottom,1,medium,7.619999694824219,3.901439843750001,0,"FACE,CIRC_HOLE",1,1.2344399505615238,,4.57,152.39999389648438,164
166
+ 22,1,12,21,2.286,3.1242,bend_bottom,1,high,7.619999694824219,3.901439843750001,0,"FACE,CIRC_HOLE",1,1.2344399505615238,,4.57,152.39999389648438,165
167
+ 22,1,12,21,2.286,3.1242,tension,1,low,7.619999694824219,3.901439843750001,1,"CIRC_HOLE,FACE",1,1.6459199340820316,,4.57,152.39999389648438,166
168
+ 22,1,12,21,2.286,3.1242,tension,1,medium,7.619999694824219,3.901439843750001,1,"CIRC_HOLE,FACE",1,1.6459199340820316,,4.57,152.39999389648438,167
169
+ 22,1,12,21,2.286,3.1242,tension,1,high,7.619999694824219,3.901439843750001,1,"CIRC_HOLE,FACE",1,1.6459199340820316,,4.57,152.39999389648438,168
170
+ 25,5,8,17,1.27,1.55,bend_bottom,-1,low,3.125,2.34375,0,"FILLET,FACE",1,0.988769531,,2.5,30.162500381469727,169
171
+ 25,5,8,17,1.27,1.55,bend_bottom,-1,medium,3.125,2.34375,0,"FILLET,FACE",1,0.988769531,,2.5,30.162500381469727,170
172
+ 25,5,8,17,1.27,1.55,bend_bottom,-1,high,3.125,2.34375,0,"FILLET,FACE",1,0.988769531,,2.5,30.162500381469727,171
173
+ 25,5,8,17,1.27,1.55,tension,-1,low,3.125,2,0,"CIRC_HOLE,FACE",1,0.6328125,,2.5,30.162500381469727,172
174
+ 25,5,8,17,1.27,1.55,tension,-1,medium,3.125,2,0,"CIRC_HOLE,FACE",1,0.6328125,,2.5,30.162500381469727,173
175
+ 25,5,8,17,1.27,1.55,tension,-1,high,3.125,2,1,"CIRC_HOLE,FACE",1,0.6328125,,2.5,30.162500381469727,174
176
+ 25,5,8,17,1.27,1.55,bend_bottom,1,low,3.125,2.34375,0,"FILLET,FACE",1,0.988769531,,2.5,30.162500381469727,175
177
+ 25,5,8,17,1.27,1.55,bend_bottom,1,medium,3.125,2.34375,0,"FILLET,FACE",1,0.988769531,,2.5,30.162500381469727,176
178
+ 25,5,8,17,1.27,1.55,bend_bottom,1,high,3.125,2.34375,0,"FILLET,FACE",1,0.988769531,,2.5,30.162500381469727,177
179
+ 25,5,8,17,1.27,1.55,tension,1,low,3.125,2,0,"CIRC_HOLE,FACE",1,0.6328125,,2.5,30.162500381469727,178
180
+ 25,5,8,17,1.27,1.55,tension,1,medium,3.125,2,0,"CIRC_HOLE,FACE",1,0.6328125,,2.5,30.162500381469727,179
181
+ 25,5,8,17,1.27,1.55,tension,1,high,3.125,2,0,"FACE,CIRC_HOLE",1,0.6328125,,2.5,30.162500381469727,180
182
+ 26,5,8,17,0.81,1.55,bend_bottom,-1,low,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,44.84687423706055,181
183
+ 26,5,8,17,0.81,1.55,bend_bottom,-1,medium,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,44.84687423706055,182
184
+ 26,5,8,17,0.81,1.55,bend_bottom,-1,high,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,44.84687423706055,183
185
+ 26,5,8,17,0.81,1.55,tension,-1,low,3.75,2.4000000000000004,1,"CIRC_HOLE,FACE",1,1.0125000000000002,,3,44.84687423706055,184
186
+ 26,5,8,17,0.81,1.55,tension,-1,medium,3.75,2.4000000000000004,1,"CIRC_HOLE,FACE",1,1.0125000000000002,,3,44.84687423706055,185
187
+ 26,5,8,17,0.81,1.55,tension,-1,high,3.75,2.4000000000000004,0,"CIRC_HOLE,FACE",1,0.7593750000000001,,3,44.84687423706055,186
188
+ 26,5,8,17,0.81,1.55,bend_bottom,1,low,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,44.84687423706055,187
189
+ 26,5,8,17,0.81,1.55,bend_bottom,1,medium,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,44.84687423706055,188
190
+ 26,5,8,17,0.81,1.55,bend_bottom,1,high,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,44.84687423706055,189
191
+ 26,5,8,17,0.81,1.55,tension,1,low,3.75,2.4000000000000004,1,"CIRC_HOLE,FACE",1,1.35,,3,44.84687423706055,190
192
+ 26,5,8,17,0.81,1.55,tension,1,medium,3.75,2.4000000000000004,1,"FACE,CIRC_HOLE",1,1.35,,3,44.84687423706055,191
193
+ 26,5,8,17,0.81,1.55,tension,1,high,3.75,2.4000000000000004,1,"FACE,CIRC_HOLE",1,1.35,,3,44.84687423706055,192
194
+ 27,5,8,17,0.81,1.75,bend_bottom,-1,low,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,59.928123474121094,193
195
+ 27,5,8,17,0.81,1.75,bend_bottom,-1,medium,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,59.928123474121094,194
196
+ 27,5,8,17,0.81,1.75,bend_bottom,-1,high,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,59.928123474121094,195
197
+ 27,5,8,17,0.81,1.75,tension,-1,low,3.75,2.4000000000000004,1,"CIRC_HOLE,FACE",1,1.35,,3,59.928123474121094,196
198
+ 27,5,8,17,0.81,1.75,tension,-1,medium,3.75,2.4000000000000004,1,"CIRC_HOLE,FACE",1,1.35,,3,59.928123474121094,197
199
+ 27,5,8,17,0.81,1.75,tension,-1,high,3.75,2.4000000000000004,1,"CIRC_HOLE,FACE",1,1.35,,3,59.928123474121094,198
200
+ 27,5,8,17,0.81,1.75,bend_bottom,1,low,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,59.928123474121094,199
201
+ 27,5,8,17,0.81,1.75,bend_bottom,1,medium,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,59.928123474121094,200
202
+ 27,5,8,17,0.81,1.75,bend_bottom,1,high,3.75,2.4000000000000004,0,"FILLET,FACE",1,0.7593750000000001,,3,59.928123474121094,201
203
+ 27,5,8,17,0.81,1.75,tension,1,low,3.75,2.4000000000000004,0,"CIRC_HOLE,FACE",1,0.7593750000000001,,3,59.928123474121094,202
204
+ 27,5,8,17,0.81,1.75,tension,1,medium,3.75,2.4000000000000004,0,"CIRC_HOLE,FACE",1,0.7593750000000001,,3,59.928123474121094,203
205
+ 27,5,8,17,0.81,1.75,tension,1,high,3.75,2.4000000000000004,0,"CIRC_HOLE,FACE",1,0.7593750000000001,,3,59.928123474121094,204
206
+ 29,0,4,17,16,2.1,bend_bottom,-1,low,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,205
207
+ 29,0,4,17,16,2.1,bend_bottom,-1,medium,4.375,2.8000000000000003,0,"CIRC_HOLE,FACE",1,0.6644531250000001,,3.5,23,206
208
+ 29,0,4,17,16,2.1,bend_bottom,-1,high,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,0.49833984375000007,,3.5,23,207
209
+ 29,0,4,17,16,2.1,tension,-1,low,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,208
210
+ 29,0,4,17,16,2.1,tension,-1,medium,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,209
211
+ 29,0,4,17,16,2.1,tension,-1,high,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,210
212
+ 29,0,4,17,16,2.1,bend_bottom,1,low,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,211
213
+ 29,0,4,17,16,2.1,bend_bottom,1,medium,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,212
214
+ 29,0,4,17,16,2.1,bend_bottom,1,high,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,213
215
+ 29,0,4,17,16,2.1,tension,1,low,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,214
216
+ 29,0,4,17,16,2.1,tension,1,medium,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,215
217
+ 29,0,4,17,16,2.1,tension,1,high,4.375,2.8000000000000003,1,"CIRC_HOLE,FACE",1,1.1812500000000001,,3.5,23,216
218
+ 36,2,16,12,0.762,1.7526,bend_bottom,-1,low,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.3847500000000001,,1.52,34.92500197887421,217
219
+ 36,2,16,12,0.762,1.7526,bend_bottom,-1,medium,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.3847500000000001,,1.52,34.92500197887421,218
220
+ 36,2,16,12,0.762,1.7526,bend_bottom,-1,high,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.3847500000000001,,1.52,34.92500197887421,219
221
+ 36,2,16,12,0.762,1.7526,tension,-1,low,1.9,1.2160000000000002,1,"CIRC_HOLE,CIRC_HOLE",1,0.28856250000000006,,1.52,34.92500197887421,220
222
+ 36,2,16,12,0.762,1.7526,tension,-1,medium,1.9,1.2160000000000002,1,"CIRC_HOLE,CIRC_HOLE",1,0.28856250000000006,,1.52,34.92500197887421,221
223
+ 36,2,16,12,0.762,1.7526,tension,-1,high,1.9,1.2160000000000002,1,"CIRC_HOLE,CIRC_HOLE",1,0.28856250000000006,,1.52,34.92500197887421,222
224
+ 36,2,16,12,0.762,1.7526,bend_bottom,1,low,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.3847500000000001,,1.52,34.92500197887421,223
225
+ 36,2,16,12,0.762,1.7526,bend_bottom,1,medium,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.3847500000000001,,1.52,34.92500197887421,224
226
+ 36,2,16,12,0.762,1.7526,bend_bottom,1,high,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.5130000000000001,,1.52,34.92500197887421,225
227
+ 36,2,16,12,0.762,1.7526,tension,1,low,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.6840000000000002,,1.52,34.92500197887421,226
228
+ 36,2,16,12,0.762,1.7526,tension,1,medium,1.9,1.2160000000000002,1,"CIRC_HOLE,FACE",1,0.6840000000000002,,1.52,34.92500197887421,227
229
+ 36,2,16,12,0.762,1.7526,tension,1,high,1.9,1.2160000000000002,1,"CIRC_HOLE,CIRC_HOLE",1,0.6840000000000002,,1.52,34.92500197887421,228
230
+ 37,2,8,12,0.762,1.7526,bend_bottom,-1,low,1.9050000607967377,1.2192000389099122,0,"FACE,CIRC_HOLE",1,0.3857625123113394,,1.52,38.10000121593475,229
231
+ 37,2,8,12,0.762,1.7526,bend_bottom,-1,medium,1.9050000607967377,1.2192000389099122,0,"FACE,CIRC_HOLE",1,0.3857625123113394,,1.52,38.10000121593475,230
232
+ 37,2,8,12,0.762,1.7526,bend_bottom,-1,high,1.9050000607967377,1.2192000389099122,0,"FACE,CIRC_HOLE",1,0.3857625123113394,,1.52,38.10000121593475,231
233
+ 37,2,8,12,0.762,1.7526,tension,-1,low,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,CIRC_HOLE",1,0.2893218842335046,,1.52,38.10000121593475,232
234
+ 37,2,8,12,0.762,1.7526,tension,-1,medium,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,CIRC_HOLE",1,0.2893218842335046,,1.52,38.10000121593475,233
235
+ 37,2,8,12,0.762,1.7526,tension,-1,high,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,CIRC_HOLE",1,0.21699141317512843,,1.52,38.10000121593475,234
236
+ 37,2,8,12,0.762,1.7526,bend_bottom,1,low,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,FACE",1,0.3857625123113394,,1.52,38.10000121593475,235
237
+ 37,2,8,12,0.762,1.7526,bend_bottom,1,medium,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,FACE",1,0.3857625123113394,,1.52,38.10000121593475,236
238
+ 37,2,8,12,0.762,1.7526,bend_bottom,1,high,1.9050000607967377,1.2192000389099122,1,"FACE,FILLET",1,0.3857625123113394,,1.52,38.10000121593475,237
239
+ 37,2,8,12,0.762,1.7526,tension,1,low,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,FACE",1,0.5143500164151192,,1.52,38.10000121593475,238
240
+ 37,2,8,12,0.762,1.7526,tension,1,medium,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,CIRC_HOLE",1,0.5143500164151192,,1.52,38.10000121593475,239
241
+ 37,2,8,12,0.762,1.7526,tension,1,high,1.9050000607967377,1.2192000389099122,1,"CIRC_HOLE,FACE",1,0.5143500164151192,,1.52,38.10000121593475,240
242
+ 43,6,16,8,0.6985,2.0828,bend_bottom,-1,low,3.5,2.24,0,"FACE,CIRC_HOLE",1,0.7087500000000001,,2.8,66.674999833107,241
243
+ 43,6,16,8,0.6985,2.0828,bend_bottom,-1,medium,3.5,2.24,0,"FACE,CIRC_HOLE",1,0.7087500000000001,,2.8,66.674999833107,242
244
+ 43,6,16,8,0.6985,2.0828,bend_bottom,-1,high,3.5,2.24,1,"FILLET,FACE",1,0.7087500000000001,,2.8,66.674999833107,243
245
+ 43,6,16,8,0.6985,2.0828,tension,-1,low,3.5,2.24,1,"CIRC_HOLE,CIRC_HOLE",1,0.7087500000000001,,2.8,66.674999833107,244
246
+ 43,6,16,8,0.6985,2.0828,tension,-1,medium,3.5,2.24,1,"CIRC_HOLE,CIRC_HOLE",1,0.7087500000000001,,2.8,66.674999833107,245
247
+ 43,6,16,8,0.6985,2.0828,tension,-1,high,3.5,2.24,1,"CIRC_HOLE,CIRC_HOLE",1,0.7087500000000001,,2.8,66.674999833107,246
248
+ 43,6,16,8,0.6985,2.0828,bend_bottom,1,low,3.5,2.24,0,"CIRC_HOLE,FACE",1,0.7087500000000001,,2.8,66.674999833107,247
249
+ 43,6,16,8,0.6985,2.0828,bend_bottom,1,medium,3.5,2.24,0,"FILLET,FACE",1,0.7087500000000001,,2.8,66.674999833107,248
250
+ 43,6,16,8,0.6985,2.0828,bend_bottom,1,high,3.5,2.24,1,"FILLET,FACE",1,0.7087500000000001,,2.8,66.674999833107,249
251
+ 43,6,16,8,0.6985,2.0828,tension,1,low,3.5,2.24,1,"CIRC_HOLE,CIRC_HOLE",1,0.398671875,,2.8,66.674999833107,250
252
+ 43,6,16,8,0.6985,2.0828,tension,1,medium,3.5,2.24,1,"CIRC_HOLE,CIRC_HOLE",1,0.398671875,,2.8,66.674999833107,251
253
+ 43,6,16,8,0.6985,2.0828,tension,1,high,3.5,2.24,1,"CIRC_HOLE,FACE",1,0.7087500000000001,,2.8,66.674999833107,252
254
+ 44,6,16,8,0.6985,2.0828,bend_bottom,-1,low,4.603749686479569,2.7622498118877417,1,"CIRC_HOLE,FACE",1,1.5537655191868547,,2.8,92.07499372959137,253
255
+ 44,6,16,8,0.6985,2.0828,bend_bottom,-1,medium,4.603749686479569,2.5896091986447574,0,"FILLET,FACE",1,1.456655174237676,,2.8,92.07499372959137,254
256
+ 44,6,16,8,0.6985,2.0828,bend_bottom,-1,high,4.603749686479569,2.5896091986447574,0,"FILLET,FACE",1,1.456655174237676,,2.8,92.07499372959137,255
257
+ 44,6,16,8,0.6985,2.0828,tension,-1,low,4.603749686479569,2.946399799346924,1,"FACE,CIRC_HOLE",1,1.657349887132645,,2.8,92.07499372959137,256
258
+ 44,6,16,8,0.6985,2.0828,tension,-1,medium,4.603749686479569,2.946399799346924,1,"FACE,CIRC_HOLE",1,1.657349887132645,,2.8,92.07499372959137,257
259
+ 44,6,16,8,0.6985,2.0828,tension,-1,high,4.603749686479569,2.946399799346924,1,"FACE,CIRC_HOLE",1,1.657349887132645,,2.8,92.07499372959137,258
260
+ 44,6,16,8,0.6985,2.0828,bend_bottom,1,low,4.603749686479569,2.7622498118877417,0,"CIRC_HOLE,FACE",1,1.165324139390141,,2.8,92.07499372959137,259
261
+ 44,6,16,8,0.6985,2.0828,bend_bottom,1,medium,4.603749686479569,2.5896091986447574,0,"FILLET,FACE",1,1.456655174237676,,2.8,92.07499372959137,260
262
+ 44,6,16,8,0.6985,2.0828,bend_bottom,1,high,4.603749686479569,2.5896091986447574,0,"FILLET,FACE",1,1.456655174237676,,2.8,92.07499372959137,261
263
+ 44,6,16,8,0.6985,2.0828,tension,1,low,4.603749686479569,2.946399799346924,1,"FACE,CIRC_HOLE",1,1.657349887132645,,2.8,92.07499372959137,262
264
+ 44,6,16,8,0.6985,2.0828,tension,1,medium,4.603749686479569,2.946399799346924,1,"FACE,CIRC_HOLE",1,1.657349887132645,,2.8,92.07499372959137,263
265
+ 44,6,16,8,0.6985,2.0828,tension,1,high,4.603749686479569,2.946399799346924,1,"FACE,CIRC_HOLE",1,1.657349887132645,,2.8,92.07499372959137,264
266
+ 50,1,0,25,3.0988,0,bend_bottom,-1,low,7.75,4.960000000000001,1,"FILLET,FACE",1,1.5693750000000002,,6.2,65.02399444580078,265
267
+ 50,1,0,25,3.0988,0,bend_bottom,-1,medium,7.75,4.960000000000001,1,"FILLET,FACE",1,1.5693750000000002,,6.2,65.02399444580078,266
268
+ 50,1,0,25,3.0988,0,bend_bottom,-1,high,7.75,4.960000000000001,1,"FILLET,FACE",1,1.5693750000000002,,6.2,65.02399444580078,267
269
+ 50,1,0,25,3.0988,0,tension,-1,low,7.75,3.26953125,1,"FACE,FACE",1,3.26953125,,6.2,65.02399444580078,268
270
+ 50,1,0,25,3.0988,0,tension,-1,medium,7.75,3.26953125,1,"FACE,FACE",1,3.26953125,,6.2,65.02399444580078,269
271
+ 50,1,0,25,3.0988,0,tension,-1,high,7.75,3.26953125,1,"FACE,FACE",1,3.26953125,,6.2,65.02399444580078,270
272
+ 50,1,0,25,3.0988,0,bend_bottom,1,low,7.75,4.960000000000001,1,"FILLET,FACE",1,1.5693750000000002,,6.2,65.02399444580078,271
273
+ 50,1,0,25,3.0988,0,bend_bottom,1,medium,7.75,4.960000000000001,1,"FILLET,FACE",1,1.5693750000000002,,6.2,65.02399444580078,272
274
+ 50,1,0,25,3.0988,0,bend_bottom,1,high,7.75,4.960000000000001,1,"FILLET,FACE",1,1.5693750000000002,,6.2,65.02399444580078,273
275
+ 50,1,0,25,3.0988,0,tension,1,low,7.75,3.26953125,1,"FACE,FACE",1,3.26953125,,6.2,65.02399444580078,274
276
+ 50,1,0,25,3.0988,0,tension,1,medium,7.75,3.26953125,1,"FACE,FACE",1,3.26953125,,6.2,65.02399444580078,275
277
+ 50,1,0,25,3.0988,0,tension,1,high,7.75,3.26953125,1,"FACE,FACE",1,3.26953125,,6.2,65.02399444580078,276
278
+ 51,2,16,12,1.397,2.0828,bend_bottom,-1,low,3.968749839067459,2.539999897003174,1,"FACE,CIRC_HOLE",1,1.071562456548214,,2.8,79.37499678134918,277
279
+ 51,2,16,12,1.397,2.0828,bend_bottom,-1,medium,3.968749839067459,2.2324217844754455,1,"FACE,FILLET",1,1.255737253767438,,2.8,79.37499678134918,278
280
+ 51,2,16,12,1.397,2.0828,bend_bottom,-1,high,3.968749839067459,2.2324217844754455,1,"FACE,FILLET",1,1.255737253767438,,2.8,79.37499678134918,279
281
+ 51,2,16,12,1.397,2.0828,tension,-1,low,3.968749839067459,2.539999897003174,1,"CIRC_HOLE,CIRC_HOLE",1,0.8036718424111605,,2.8,79.37499678134918,280
282
+ 51,2,16,12,1.397,2.0828,tension,-1,medium,3.968749839067459,2.539999897003174,0,"CIRC_HOLE,CIRC_HOLE",1,0.4520654113562778,,2.8,79.37499678134918,281
283
+ 51,2,16,12,1.397,2.0828,tension,-1,high,3.968749839067459,2.539999897003174,0,"CIRC_HOLE,CIRC_HOLE",1,0.4520654113562778,,2.8,79.37499678134918,282
284
+ 51,2,16,12,1.397,2.0828,bend_bottom,1,low,3.968749839067459,2.3812499034404757,0,"FILLET,FACE",1,1.0045898030139506,,2.8,79.37499678134918,283
285
+ 51,2,16,12,1.397,2.0828,bend_bottom,1,medium,3.968749839067459,2.2324217844754455,1,"FACE,FILLET",1,1.255737253767438,,2.8,79.37499678134918,284
286
+ 51,2,16,12,1.397,2.0828,bend_bottom,1,high,3.968749839067459,2.2324217844754455,1,"FACE,FILLET",1,1.255737253767438,,2.8,79.37499678134918,285
287
+ 51,2,16,12,1.397,2.0828,tension,1,low,3.968749839067459,2.539999897003174,1,"CIRC_HOLE,CIRC_HOLE",1,0.8036718424111605,,2.8,79.37499678134918,286
288
+ 51,2,16,12,1.397,2.0828,tension,1,medium,3.968749839067459,2.539999897003174,1,"CIRC_HOLE,CIRC_HOLE",1,0.8036718424111605,,2.8,79.37499678134918,287
289
+ 51,2,16,12,1.397,2.0828,tension,1,high,3.968749839067459,2.539999897003174,1,"CIRC_HOLE,CIRC_HOLE",1,1.4287499420642855,,2.8,79.37499678134918,288
290
+ 52,2,20,12,1.397,2.0828,bend_bottom,-1,low,6.032500296831131,3.0886401519775397,1,"FACE,CIRC_HOLE",1,1.3030200641155245,,2.8,120.65000593662262,289
291
+ 52,2,20,12,1.397,2.0828,bend_bottom,-1,medium,6.032500296831131,3.0886401519775397,1,"FACE,CIRC_HOLE",1,1.3030200641155245,,2.8,120.65000593662262,290
292
+ 52,2,20,12,1.397,2.0828,bend_bottom,-1,high,6.032500296831131,3.393281416967511,1,"FACE,FACE",1,3.393281416967511,,2.8,120.65000593662262,291
293
+ 52,2,20,12,1.397,2.0828,tension,-1,low,6.032500296831131,3.0886401519775397,1,"CIRC_HOLE,CIRC_HOLE",1,1.3030200641155245,,2.8,120.65000593662262,292
294
+ 52,2,20,12,1.397,2.0828,tension,-1,medium,6.032500296831131,3.0886401519775397,1,"CIRC_HOLE,CIRC_HOLE",1,1.3030200641155245,,2.8,120.65000593662262,293
295
+ 52,2,20,12,1.397,2.0828,tension,-1,high,6.032500296831131,3.0886401519775397,1,"CIRC_HOLE,CIRC_HOLE",1,1.3030200641155245,,2.8,120.65000593662262,294
296
+ 52,2,20,12,1.397,2.0828,bend_bottom,1,low,6.032500296831131,3.0886401519775397,1,"FILLET,CIRC_HOLE",1,1.3030200641155245,,2.8,120.65000593662262,295
297
+ 52,2,20,12,1.397,2.0828,bend_bottom,1,medium,6.032500296831131,2.316480113983155,0,"FACE,CIRC_HOLE",1,0.9772650480866434,,2.8,120.65000593662262,296
298
+ 52,2,20,12,1.397,2.0828,bend_bottom,1,high,6.032500296831131,3.393281416967511,1,"FACE,FACE",1,3.393281416967511,,2.8,120.65000593662262,297
299
+ 52,2,20,12,1.397,2.0828,tension,1,low,6.032500296831131,3.0886401519775397,1,"CIRC_HOLE,CIRC_HOLE",1,1.3030200641155245,,2.8,120.65000593662262,298
300
+ 52,2,20,12,1.397,2.0828,tension,1,medium,6.032500296831131,3.0886401519775397,1,"CIRC_HOLE,FACE",1,0.9772650480866434,,2.8,120.65000593662262,299
301
+ 52,2,20,12,1.397,2.0828,tension,1,high,6.032500296831131,2.4709121215820318,0,"CIRC_HOLE,CIRC_HOLE",1,0.5863590288519861,,2.8,120.65000593662262,300
mesh_service/run_interface.py ADDED
@@ -0,0 +1,1150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## MVP 1 FILE!
2
+ from .step_feature_detection import (
3
+ load_step_model,
4
+ load_step_unit,
5
+ detect_faces,
6
+ detect_circular_holes,
7
+ detect_non_circular_holes,
8
+ detect_fillets,
9
+ mode_thickness,
10
+ detect_all_edges,
11
+ detect_all_faces,
12
+ )
13
+ import os, json
14
+ import pandas as pd
15
+ import gmsh
16
+ from OCP.Bnd import Bnd_Box
17
+ from OCP.BRepBndLib import BRepBndLib
18
+ import joblib
19
+ from OCP.TopExp import TopExp_Explorer
20
+ from OCP.TopAbs import TopAbs_FACE
21
+ from OCP.TopoDS import TopoDS
22
+ import torch
23
+ from torch import nn
24
+ import numpy as np
25
+ import matplotlib.pyplot as plt
26
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401
27
+ import plotly.graph_objects as go
28
+
29
+
30
+ # ---------------------------------------------------------------------
31
+ # Matplotlib static PNG plotter (unchanged)
32
+ # ---------------------------------------------------------------------
33
+ def plot_mesh(
34
+ nodes,
35
+ elements,
36
+ title,
37
+ highlight_faces=None,
38
+ face_to_nodes=None,
39
+ save_path="mesh.png",
40
+ ):
41
+ """
42
+ Static 3D line-plot of the surface mesh for PNG export.
43
+ nodes: (N,3)
44
+ elements: list[(etype, conn)], conn is array of 0-based node indices
45
+ """
46
+ fig = plt.figure(figsize=(7, 7))
47
+ ax = fig.add_subplot(111, projection="3d")
48
+ ax.view_init(elev=20, azim=45)
49
+
50
+ # normal mesh (black)
51
+ for (etype, conn) in elements:
52
+ for e in conn:
53
+ pts = nodes[e][:, :3]
54
+ pts = np.vstack([pts, pts[0]])
55
+ ax.plot(pts[:, 0], pts[:, 1], pts[:, 2], "k-", linewidth=0.4)
56
+
57
+ # optional red overlay for selected faces
58
+ if highlight_faces and face_to_nodes:
59
+ for sf in highlight_faces:
60
+ if sf not in face_to_nodes:
61
+ continue
62
+ face_nodes = face_to_nodes[sf]
63
+ for fn in face_nodes:
64
+ pts = nodes[fn][:, :3]
65
+ pts = np.vstack([pts, pts[0]])
66
+ ax.plot(pts[:, 0], pts[:, 1], pts[:, 2], "r-", linewidth=1.2)
67
+
68
+ ax.set_title(title)
69
+ ax.set_axis_off()
70
+ plt.tight_layout()
71
+ plt.savefig(save_path, dpi=300)
72
+ plt.close()
73
+
74
+
75
+ # ---------------------------------------------------------------------
76
+ # Face list helper for GUI (unchanged)
77
+ # ---------------------------------------------------------------------
78
+ def extract_faces_for_gui(step_path):
79
+ """
80
+ Load the STEP file, detect faces, and return a list of
81
+ label/metadata dicts that the GUI can use.
82
+ """
83
+ shape = load_step_model(step_path)
84
+ faces = detect_all_faces(shape)
85
+
86
+ face_list = []
87
+ for f in faces:
88
+ center = f["center"]
89
+ normal = f["normal"]
90
+ area = f.get("area", 0.0)
91
+ label = f"Face {f['face_id']} | normal={np.round(normal,3)} | area={area:.2f}"
92
+
93
+ face_list.append(
94
+ {
95
+ "label": label,
96
+ "face_id": f["face_id"],
97
+ "center": list(center),
98
+ "normal": list(normal),
99
+ "area": float(area),
100
+ }
101
+ )
102
+
103
+ return face_list
104
+
105
+
106
+ # ---------------------------------------------------------------------
107
+ # Helpers for Plotly 3D mesh
108
+ # ---------------------------------------------------------------------
109
+ def mesh_edges_from_triangles(triangles):
110
+ """
111
+ Given (M,3) triangle connectivity, return unique edges as (E,2).
112
+ """
113
+ edges = set()
114
+ for tri in triangles:
115
+ a, b, c = tri
116
+ edges.add(tuple(sorted((a, b))))
117
+ edges.add(tuple(sorted((b, c))))
118
+ edges.add(tuple(sorted((c, a))))
119
+ return np.array(list(edges), dtype=int)
120
+
121
+
122
+ def make_plotly_mesh(
123
+ nodes,
124
+ triangles,
125
+ title="Refined Mesh",
126
+ view_mode="wireframe",
127
+ refined_tri_mask=None,
128
+ vertex_refinement_values=None,
129
+ ):
130
+ """
131
+ Build an interactive Plotly figure.
132
+
133
+ Parameters
134
+ ----------
135
+ nodes : (N,3) ndarray
136
+ triangles : (M,3) ndarray of 0-based node indices
137
+ view_mode : {"wireframe", "highlight", "heatmap"}
138
+ - "wireframe": light-blue surface + black edges
139
+ - "highlight": base surface + red overlay on refined triangles
140
+ - "heatmap": surface colored by vertex_refinement_values
141
+ refined_tri_mask : (M,) bool array
142
+ True for triangles in locally-refined region (used in "highlight").
143
+ vertex_refinement_values : (N,) array
144
+ Per-node refinement intensity used for "heatmap".
145
+ Higher values → more refined → hotter colors.
146
+ """
147
+ nodes = np.asarray(nodes)
148
+ triangles = np.asarray(triangles, dtype=int)
149
+ edges = mesh_edges_from_triangles(triangles)
150
+ n_nodes = nodes.shape[0]
151
+
152
+ if refined_tri_mask is None:
153
+ refined_tri_mask = np.zeros(triangles.shape[0], dtype=bool)
154
+
155
+ if vertex_refinement_values is None:
156
+ vertex_refinement_values = np.zeros(n_nodes, dtype=float)
157
+
158
+ fig = go.Figure()
159
+
160
+ # ----------------------------------------
161
+ # Base surface for all modes
162
+ # ----------------------------------------
163
+ if view_mode == "heatmap":
164
+ # Heatmap using per-vertex refinement values
165
+ fig.add_trace(
166
+ go.Mesh3d(
167
+ x=nodes[:, 0],
168
+ y=nodes[:, 1],
169
+ z=nodes[:, 2],
170
+ i=triangles[:, 0],
171
+ j=triangles[:, 1],
172
+ k=triangles[:, 2],
173
+ intensity=vertex_refinement_values,
174
+ colorscale="Turbo",
175
+ showscale=True,
176
+ flatshading=True,
177
+ opacity=0.9,
178
+ showlegend=False,
179
+ name="Local mesh size heatmap",
180
+ )
181
+ )
182
+ else:
183
+ # Simple uniform surface color
184
+ fig.add_trace(
185
+ go.Mesh3d(
186
+ x=nodes[:, 0],
187
+ y=nodes[:, 1],
188
+ z=nodes[:, 2],
189
+ i=triangles[:, 0],
190
+ j=triangles[:, 1],
191
+ k=triangles[:, 2],
192
+ color="lightblue",
193
+ flatshading=True,
194
+ lighting=dict(
195
+ ambient=0.6, diffuse=0.8, specular=0.2, roughness=0.9
196
+ ),
197
+ lightposition=dict(x=0, y=0, z=1),
198
+ opacity=0.45 if view_mode == "wireframe" else 0.55,
199
+ showlegend=False,
200
+ showscale=False,
201
+ name="Surface",
202
+ )
203
+ )
204
+
205
+ # ----------------------------------------
206
+ # Highlight overlay (local refinement in red)
207
+ # ----------------------------------------
208
+ if view_mode == "highlight" and refined_tri_mask.any():
209
+ ref_tris = triangles[refined_tri_mask]
210
+ fig.add_trace(
211
+ go.Mesh3d(
212
+ x=nodes[:, 0],
213
+ y=nodes[:, 1],
214
+ z=nodes[:, 2],
215
+ i=ref_tris[:, 0],
216
+ j=ref_tris[:, 1],
217
+ k=ref_tris[:, 2],
218
+ color="red",
219
+ opacity=0.9,
220
+ flatshading=True,
221
+ showscale=False,
222
+ name="Locally refined",
223
+ )
224
+ )
225
+
226
+ # ----------------------------------------
227
+ # Wireframe edges for all modes
228
+ # ----------------------------------------
229
+ edge_width = 2 if view_mode != "heatmap" else 1
230
+ edge_color = "black" if view_mode != "heatmap" else "rgba(0,0,0,0.4)"
231
+
232
+ for e0, e1 in edges:
233
+ fig.add_trace(
234
+ go.Scatter3d(
235
+ x=[nodes[e0, 0], nodes[e1, 0]],
236
+ y=[nodes[e0, 1], nodes[e1, 1]],
237
+ z=[nodes[e0, 2], nodes[e1, 2]],
238
+ mode="lines",
239
+ line=dict(color=edge_color, width=edge_width),
240
+ showlegend=False,
241
+ )
242
+ )
243
+
244
+ fig.update_layout(
245
+ title=title,
246
+ scene=dict(aspectmode="data"),
247
+ margin=dict(l=0, r=0, t=30, b=0),
248
+ )
249
+
250
+ return fig
251
+
252
+
253
+ # ---------------------------------------------------------------------
254
+ # Main pipeline
255
+ # ---------------------------------------------------------------------
256
+ def get_result(
257
+ step_path,
258
+ thickness,
259
+ load_face,
260
+ load_direction,
261
+ load_scale,
262
+ selected_face_label=None,
263
+ faces_json=None,
264
+ view_mode="wireframe", # NEW optional flag, default keeps current behavior
265
+ ):
266
+ # -----------------------------------------------------------------
267
+ # STEP → JSON features
268
+ # -----------------------------------------------------------------
269
+ def step_to_json(step_path, thickness, part_id=None):
270
+ shape = load_step_model(step_path)
271
+ part_id = part_id or os.path.splitext(os.path.basename(step_path))[0]
272
+ circular_holes = detect_circular_holes(shape)
273
+ fillets = detect_fillets(shape)
274
+ faces = detect_all_faces(shape)
275
+ print(f"Detected {len(faces)} faces")
276
+
277
+ features = {
278
+ "part_id": part_id,
279
+ "thickness": thickness,
280
+ "circular_holes": circular_holes,
281
+ "fillets": fillets,
282
+ "faces": faces,
283
+ }
284
+ out_path = "output/example.json"
285
+ with open(out_path, "w") as f:
286
+ json.dump(features, f, indent=2)
287
+
288
+ step_to_json(step_path, thickness)
289
+
290
+ # -----------------------------------------------------------------
291
+ # Load geometry into gmsh
292
+ # -----------------------------------------------------------------
293
+ gmsh.open(step_path)
294
+ gmsh.model.occ.synchronize()
295
+
296
+ surfaces = gmsh.model.getEntities(dim=2)
297
+ surface_info = []
298
+ for dim, tag in surfaces:
299
+ mass_props = gmsh.model.occ.getMass(dim, tag)
300
+ com = gmsh.model.occ.getCenterOfMass(dim, tag)
301
+ surface_info.append({"tag": tag, "center": com, "area": mass_props})
302
+
303
+ with open("output/example.json") as f:
304
+ features = json.load(f)
305
+ faces = features["faces"]
306
+
307
+ num_fillet = len(features["fillets"])
308
+ num_hole = len(features["circular_holes"])
309
+ num_faces = len(features["faces"])
310
+ num_plane = num_faces - num_fillet - num_hole
311
+
312
+ smallest_fillet_r = min(
313
+ (f["radius"] for f in features["fillets"]), default=0.0
314
+ )
315
+ smallest_hole_r = min(
316
+ (h["radius"] for h in features["circular_holes"]), default=0.0
317
+ )
318
+
319
+ print(
320
+ f"num_fillet: {num_fillet}, num_hole: {num_hole}, num_plane: {num_plane}"
321
+ )
322
+ print(
323
+ f"smallest_fillet_r: {smallest_fillet_r}, smallest_hole_r: {smallest_hole_r}"
324
+ )
325
+
326
+ # -----------------------------------------------------------------
327
+ # Bounding box → max dimension
328
+ # -----------------------------------------------------------------
329
+ def detect_maxsize(shape):
330
+ bbox = Bnd_Box()
331
+ BRepBndLib.Add_s(shape, bbox)
332
+ xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
333
+ size_x = xmax - xmin
334
+ size_y = ymax - ymin
335
+ size_z = zmax - zmin
336
+ maxsize = max(size_x, size_y, size_z)
337
+ return maxsize, (size_x, size_y, size_z)
338
+
339
+ shape = load_step_model(step_path)
340
+ maxsize, (sx, sy, sz) = detect_maxsize(shape)
341
+ print("maxsize:", maxsize)
342
+
343
+ # -----------------------------------------------------------------
344
+ # Heuristic face roles (bottom/left/front/back)
345
+ # -----------------------------------------------------------------
346
+ def find_the_faces(step_path):
347
+ shape = load_step_model(step_path)
348
+ faces_local = detect_all_faces(shape)
349
+ z_vals = [f["center"][2] for f in faces_local]
350
+ x_vals = [f["center"][0] for f in faces_local]
351
+ y_vals = [f["center"][1] for f in faces_local]
352
+
353
+ idx_bottom = z_vals.index(min(z_vals))
354
+ idx_left = x_vals.index(min(x_vals))
355
+ idx_front = y_vals.index(min(y_vals))
356
+ idx_back = y_vals.index(max(y_vals))
357
+
358
+ return {
359
+ "bottom": faces_local[idx_bottom],
360
+ "left": faces_local[idx_left],
361
+ "front": faces_local[idx_front],
362
+ "back": faces_local[idx_back],
363
+ "bottom_idx": idx_bottom,
364
+ "left_idx": idx_left,
365
+ "front_idx": idx_front,
366
+ "back_idx": idx_back,
367
+ }
368
+
369
+ face_dict = find_the_faces(step_path)
370
+
371
+ part_row = {
372
+ "num_fillet": num_fillet,
373
+ "num_hole": num_hole,
374
+ "num_plane": num_plane,
375
+ "smallest_fillet_r": smallest_fillet_r,
376
+ "smallest_hole_r": smallest_hole_r,
377
+ "maxsize": maxsize,
378
+ "thickness": thickness,
379
+ "load_face": load_face,
380
+ "load_direction": load_direction,
381
+ "load_scale": load_scale,
382
+ }
383
+ df_part = pd.DataFrame([part_row])
384
+
385
+ # classify face types
386
+ face_type_dict = {}
387
+ for hole in features["circular_holes"]:
388
+ for f in hole["faces"]:
389
+ face_type_dict[f["face_id"]] = "CIRC_HOLE"
390
+ for fillet in features["fillets"]:
391
+ for f in fillet["faces"]:
392
+ face_type_dict[f["face_id"]] = "FILLET"
393
+ for f in features["faces"]:
394
+ if f["face_id"] not in face_type_dict:
395
+ face_type_dict[f["face_id"]] = "FACE"
396
+
397
+ # -----------------------------------------------------------------
398
+ # Choose load/fixed faces (GUI-aware)
399
+ # -----------------------------------------------------------------
400
+ load_face_data = None
401
+ fix_face_data = None
402
+
403
+ if selected_face_label is not None and faces_json is not None and faces_json != "":
404
+ faces_from_gui = json.loads(faces_json)
405
+ label_to_face = {f["label"]: f for f in faces_from_gui}
406
+
407
+ if selected_face_label not in label_to_face:
408
+ raise ValueError(
409
+ f"Selected load face '{selected_face_label}' not found in faces_json."
410
+ )
411
+
412
+ load_face_data = label_to_face[selected_face_label]
413
+
414
+ load_center = np.array(load_face_data["center"], dtype=float)
415
+ all_faces_gui = list(label_to_face.values())
416
+ distances = [
417
+ np.linalg.norm(np.array(f["center"], dtype=float) - load_center)
418
+ for f in all_faces_gui
419
+ ]
420
+ fix_face_data = all_faces_gui[int(np.argmax(distances))]
421
+ else:
422
+ if load_face == "bend_bottom":
423
+ load_face_data = face_dict["bottom"]
424
+ fix_face_data = face_dict["left"]
425
+ elif load_face == "tension":
426
+ load_face_data = face_dict["back"]
427
+ fix_face_data = face_dict["front"]
428
+ else:
429
+ raise ValueError(
430
+ f"No GUI selection and unsupported load_face value '{load_face}'."
431
+ )
432
+
433
+ load_face_center = np.array(load_face_data["center"])
434
+ load_face_normal = np.array(load_face_data["normal"])
435
+ fix_face_center = np.array(fix_face_data["center"])
436
+ fix_face_normal = np.array(fix_face_data["normal"])
437
+
438
+ # -----------------------------------------------------------------
439
+ # BBox per CAD face
440
+ # -----------------------------------------------------------------
441
+ def get_face_bbox_dict(shape):
442
+ bbox_dict = {}
443
+ exp = TopExp_Explorer(shape, TopAbs_FACE)
444
+ face_id = 0
445
+ while exp.More():
446
+ face_shape = exp.Current()
447
+ face = TopoDS.Face_s(face_shape)
448
+ bbox = Bnd_Box()
449
+ BRepBndLib.Add_s(face, bbox)
450
+ xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
451
+ bbox_dx = xmax - xmin
452
+ bbox_dy = ymax - ymin
453
+ bbox_dz = zmax - zmin
454
+ bbox_dict[face_id] = (bbox_dx, bbox_dy, bbox_dz)
455
+ face_id += 1
456
+ exp.Next()
457
+ return bbox_dict
458
+
459
+ shape = load_step_model(step_path)
460
+ bbox_dict = get_face_bbox_dict(shape)
461
+
462
+ # -----------------------------------------------------------------
463
+ # Build per-face feature table
464
+ # -----------------------------------------------------------------
465
+ rows1 = []
466
+ for f in faces:
467
+ row = {}
468
+ row["face_id"] = f["face_id"]
469
+ row["ftype"] = face_type_dict[f["face_id"]]
470
+ row["cx"], row["cy"], row["cz"] = f["center"]
471
+ row["nx"], row["ny"], row["nz"] = f["normal"]
472
+ row["area"] = f.get("area")
473
+ if f["face_id"] == load_face_data["face_id"]:
474
+ row["role"] = "load"
475
+ elif f["face_id"] == fix_face_data["face_id"]:
476
+ row["role"] = "fixed"
477
+ else:
478
+ row["role"] = "free"
479
+ dx, dy, dz = bbox_dict[f["face_id"]]
480
+ row["bbox_dx"] = dx
481
+ row["bbox_dy"] = dy
482
+ row["bbox_dz"] = dz
483
+
484
+ fc = np.array(f["center"])
485
+ row["dist_load"] = np.linalg.norm(fc - np.array(load_face_center))
486
+ row["nlx"], row["nly"], row["nlz"] = load_face_normal
487
+ row["dist_fixed"] = np.linalg.norm(fc - np.array(fix_face_center))
488
+ row["nfx"], row["nfy"], row["nfz"] = fix_face_normal
489
+
490
+ row["load_face"] = load_face
491
+ row["load_direction"] = load_direction
492
+ row["load_scale"] = load_scale
493
+ row["maxsize"] = maxsize
494
+ row["thickness"] = thickness
495
+ rows1.append(row)
496
+ df_feature = pd.DataFrame(rows1)
497
+
498
+ df_feature = df_feature.fillna(0.0)
499
+ df_part = df_part.fillna(0.0)
500
+
501
+ # -----------------------------------------------------------------
502
+ # Model 1: refine_needed classifier
503
+ # -----------------------------------------------------------------
504
+ class RefineClassifier(nn.Module):
505
+ def __init__(self, in_dim):
506
+ super().__init__()
507
+ self.encoder = nn.Sequential(
508
+ nn.Linear(in_dim, 256),
509
+ nn.BatchNorm1d(256),
510
+ nn.ReLU(),
511
+ nn.Dropout(0.2),
512
+ nn.Linear(256, 128),
513
+ nn.BatchNorm1d(128),
514
+ nn.ReLU(),
515
+ nn.Dropout(0.1),
516
+ nn.Linear(128, 64),
517
+ nn.ReLU(),
518
+ )
519
+ self.head = nn.Linear(64, 1)
520
+
521
+ def forward(self, x):
522
+ h = self.encoder(x)
523
+ return self.head(h)
524
+
525
+ preprocessor1 = joblib.load("saved_model/preprocessor1.pkl")
526
+ feature_cols1 = [
527
+ c
528
+ for c in df_feature.columns
529
+ if c
530
+ not in [
531
+ "part_id",
532
+ "refine_needed",
533
+ "converged",
534
+ "local_size",
535
+ "global_size_initial",
536
+ "global_size_final",
537
+ "load_id",
538
+ ]
539
+ ]
540
+ X1 = preprocessor1.transform(df_feature[feature_cols1])
541
+ X1_t = torch.tensor(
542
+ X1.toarray() if hasattr(X1, "toarray") else X1, dtype=torch.float32
543
+ )
544
+
545
+ model1 = RefineClassifier(X1_t.shape[1])
546
+ state_dict = torch.load("saved_model/model1.pt", map_location="cpu")
547
+ model1.load_state_dict(state_dict)
548
+ model1.eval()
549
+
550
+ with torch.no_grad():
551
+ logits1 = model1(X1_t)
552
+ probs1 = torch.sigmoid(logits1).cpu().numpy().flatten()
553
+ df_feature["refine_prob"] = probs1
554
+
555
+ top2 = df_feature.sort_values("refine_prob", ascending=False).head(2)
556
+ df_feature.drop(columns=["refine_prob"], inplace=True)
557
+
558
+ # -----------------------------------------------------------------
559
+ # Model 2: part-level convergence
560
+ # -----------------------------------------------------------------
561
+ preprocessor2 = joblib.load("saved_model/preprocessor2.pkl")
562
+
563
+ feature_cols = [
564
+ col
565
+ for col in df_feature.columns
566
+ if col
567
+ not in [
568
+ "part_id",
569
+ "load_id",
570
+ "refine_needed",
571
+ "converged",
572
+ "local_size",
573
+ "global_size_initial",
574
+ "global_size_final",
575
+ ]
576
+ ]
577
+ part_level_cols = [
578
+ col
579
+ for col in df_part.columns
580
+ if col
581
+ not in [
582
+ "part_id",
583
+ "converged",
584
+ "load_id",
585
+ "top2_features",
586
+ "global_size_initial",
587
+ "global_size_final",
588
+ "local_refine_needed",
589
+ "local_size",
590
+ ]
591
+ ]
592
+
593
+ def extract_supervised_features_model2():
594
+ part_row_local = df_part.iloc[0]
595
+ feat_rows = df_feature
596
+
597
+ # fixed
598
+ fixed_row = feat_rows[feat_rows.role == "fixed"]
599
+ if len(fixed_row):
600
+ X_fixed = preprocessor1.transform(fixed_row[feature_cols])
601
+ with torch.no_grad():
602
+ fixed_prob = torch.sigmoid(
603
+ model1(torch.tensor(X_fixed, dtype=torch.float32))
604
+ ).item()
605
+ fixed_feats = list(fixed_row.iloc[0][feature_cols]) + [fixed_prob]
606
+ else:
607
+ fixed_feats = [0] * (len(feature_cols) + 1)
608
+
609
+ # load
610
+ load_row = feat_rows[feat_rows.role == "load"]
611
+ if len(load_row):
612
+ X_load = preprocessor1.transform(load_row[feature_cols])
613
+ with torch.no_grad():
614
+ load_prob = torch.sigmoid(
615
+ model1(torch.tensor(X_load, dtype=torch.float32))
616
+ ).item()
617
+ load_feats = list(load_row.iloc[0][feature_cols]) + [load_prob]
618
+ else:
619
+ load_feats = [0] * (len(feature_cols) + 1)
620
+
621
+ # free
622
+ free_rows = feat_rows[feat_rows.role == "free"]
623
+ if len(free_rows):
624
+ X_free = preprocessor1.transform(free_rows[feature_cols])
625
+ with torch.no_grad():
626
+ free_probs = torch.sigmoid(
627
+ model1(torch.tensor(X_free, dtype=torch.float32))
628
+ ).cpu().numpy().flatten()
629
+ best_idx = np.argmax(free_probs)
630
+ free_feats = (
631
+ list(free_rows.iloc[best_idx][feature_cols])
632
+ + [free_probs[best_idx]]
633
+ )
634
+ else:
635
+ free_feats = [0] * (len(feature_cols) + 1)
636
+
637
+ global_feats = list(part_row_local[part_level_cols])
638
+ return fixed_feats + load_feats + free_feats + global_feats
639
+
640
+ col_names = (
641
+ [f"fixed_{c}" for c in feature_cols]
642
+ + ["fixed_refine_prob"]
643
+ + [f"load_{c}" for c in feature_cols]
644
+ + ["load_refine_prob"]
645
+ + [f"free_{c}" for c in feature_cols]
646
+ + ["free_refine_prob"]
647
+ + part_level_cols
648
+ )
649
+
650
+ row = extract_supervised_features_model2()
651
+ df_sup = pd.DataFrame([row], columns=col_names)
652
+
653
+ missing_cols = set(preprocessor2.feature_names_in_) - set(df_sup.columns)
654
+ for c in missing_cols:
655
+ df_sup[c] = 0.0
656
+ df_sup = df_sup[preprocessor2.feature_names_in_]
657
+
658
+ X2 = preprocessor2.transform(df_sup)
659
+ print("X2 contains NaN?", np.isnan(X2).any())
660
+ X2_t = torch.tensor(
661
+ X2.toarray() if hasattr(X2, "toarray") else X2, dtype=torch.float32
662
+ )
663
+
664
+ class ConvergeClassifier(nn.Module):
665
+ def __init__(self, in_dim):
666
+ super().__init__()
667
+ self.encoder = nn.Sequential(
668
+ nn.Linear(in_dim, 64),
669
+ nn.BatchNorm1d(64),
670
+ nn.ReLU(),
671
+ nn.Dropout(0.2),
672
+ nn.Linear(64, 32),
673
+ nn.ReLU(),
674
+ )
675
+ self.head = nn.Linear(32, 2)
676
+
677
+ def forward(self, x):
678
+ h = self.encoder(x)
679
+ return self.head(h)
680
+
681
+ model2 = ConvergeClassifier(X2_t.shape[1])
682
+ state_dict = torch.load("saved_model/model2.pt", map_location="cpu")
683
+ model2.load_state_dict(state_dict)
684
+ model2.eval()
685
+
686
+ with torch.no_grad():
687
+ logits2 = model2(X2_t)
688
+ probs2 = torch.softmax(logits2, dim=1).cpu().numpy()
689
+ preds2 = probs2.argmax(1)
690
+
691
+ print("Model 2 (part-level converge) predictions:")
692
+ print("Probability not converged:", probs2[:, 0])
693
+ print("Probability converged:", probs2[:, 1])
694
+ print("Predicted class (0: unconverged, 1: converged):", preds2)
695
+
696
+ # -----------------------------------------------------------------
697
+ # Model 3: local size regressor
698
+ # -----------------------------------------------------------------
699
+ class LocalRefineRegressor(nn.Module):
700
+ def __init__(self, in_dim):
701
+ super().__init__()
702
+ self.encoder = nn.Sequential(
703
+ nn.Linear(in_dim, 256),
704
+ nn.BatchNorm1d(256),
705
+ nn.ReLU(),
706
+ nn.Dropout(0.2),
707
+ nn.Linear(256, 128),
708
+ nn.BatchNorm1d(128),
709
+ nn.ReLU(),
710
+ nn.Dropout(0.1),
711
+ nn.Linear(128, 64),
712
+ nn.ReLU(),
713
+ )
714
+ self.head = nn.Linear(64, 1)
715
+
716
+ def forward(self, x):
717
+ h = self.encoder(x)
718
+ return self.head(h)
719
+
720
+ preprocessor3 = joblib.load("saved_model/preprocessor3.pkl")
721
+ feature_cols3 = [
722
+ c
723
+ for c in df_feature.columns
724
+ if c
725
+ not in [
726
+ "part_id",
727
+ "refine_needed",
728
+ "converged",
729
+ "local_size",
730
+ "global_size_initial",
731
+ "global_size_final",
732
+ "load_id",
733
+ ]
734
+ ]
735
+ X3 = preprocessor3.transform(df_feature[feature_cols3])
736
+ X3_t = torch.tensor(
737
+ X3.toarray() if hasattr(X3, "toarray") else X3, dtype=torch.float32
738
+ )
739
+
740
+ model3 = LocalRefineRegressor(X3_t.shape[1])
741
+ state_dict = torch.load("saved_model/model3.pt", map_location="cpu")
742
+ model3.load_state_dict(state_dict)
743
+ model3.eval()
744
+
745
+ with torch.no_grad():
746
+ logits1 = model1(X1_t)
747
+ probs1 = torch.sigmoid(logits1).cpu().numpy().flatten()
748
+ df_feature["refine_prob"] = probs1
749
+
750
+ top2 = df_feature.sort_values("refine_prob", ascending=False).head(2)
751
+
752
+ with torch.no_grad():
753
+ local_size_pred = model3(X3_t).cpu().numpy().flatten()
754
+ df_feature["predicted_local_size"] = local_size_pred
755
+
756
+ top2_with_local_size = top2.copy()
757
+ top2_with_local_size["predicted_local_size"] = df_feature.loc[
758
+ top2.index, "predicted_local_size"
759
+ ]
760
+
761
+ print("Top-2 faces needing refinement and their predicted local sizes:")
762
+ print(
763
+ top2_with_local_size[
764
+ ["face_id", "ftype", "predicted_local_size"]
765
+ ].to_string(index=False)
766
+ )
767
+ df_feature.drop(columns=["refine_prob", "predicted_local_size"], inplace=True)
768
+
769
+ # -----------------------------------------------------------------
770
+ # Model 4: global size regressor
771
+ # -----------------------------------------------------------------
772
+ preprocessor4 = joblib.load("saved_model/preprocessor4.pkl")
773
+
774
+ feature_cols = [
775
+ col
776
+ for col in df_feature.columns
777
+ if col
778
+ not in [
779
+ "part_id",
780
+ "load_id",
781
+ "refine_needed",
782
+ "converged",
783
+ "local_size",
784
+ "global_size_initial",
785
+ "global_size_final",
786
+ ]
787
+ ]
788
+ part_level_cols = [
789
+ col
790
+ for col in df_part.columns
791
+ if col
792
+ not in [
793
+ "part_id",
794
+ "converged",
795
+ "load_id",
796
+ "top2_features",
797
+ "global_size_initial",
798
+ "global_size_final",
799
+ "local_refine_needed",
800
+ "local_size",
801
+ ]
802
+ ]
803
+
804
+ def extract_supervised_features_model4():
805
+ part_row_local = df_part.iloc[0]
806
+ feat_rows = df_feature
807
+
808
+ fixed_row = feat_rows[feat_rows.role == "fixed"]
809
+ if len(fixed_row):
810
+ X_fixed = preprocessor1.transform(fixed_row[feature_cols])
811
+ with torch.no_grad():
812
+ fixed_prob = torch.sigmoid(
813
+ model1(torch.tensor(X_fixed, dtype=torch.float32))
814
+ ).item()
815
+ fixed_feats = list(fixed_row.iloc[0][feature_cols]) + [fixed_prob]
816
+ else:
817
+ fixed_feats = [0] * (len(feature_cols) + 1)
818
+
819
+ load_row = feat_rows[feat_rows.role == "load"]
820
+ if len(load_row):
821
+ X_load = preprocessor1.transform(load_row[feature_cols])
822
+ with torch.no_grad():
823
+ load_prob = torch.sigmoid(
824
+ model1(torch.tensor(X_load, dtype=torch.float32))
825
+ ).item()
826
+ load_feats = list(load_row.iloc[0][feature_cols]) + [load_prob]
827
+ else:
828
+ load_feats = [0] * (len(feature_cols) + 1)
829
+
830
+ free_rows = feat_rows[feat_rows.role == "free"]
831
+ if len(free_rows):
832
+ X_free = preprocessor1.transform(free_rows[feature_cols])
833
+ with torch.no_grad():
834
+ free_probs = torch.sigmoid(
835
+ model1(torch.tensor(X_free, dtype=torch.float32))
836
+ ).cpu().numpy().flatten()
837
+ best_idx = np.argmax(free_probs)
838
+ free_feats = (
839
+ list(free_rows.iloc[best_idx][feature_cols])
840
+ + [free_probs[best_idx]]
841
+ )
842
+ else:
843
+ free_feats = [0] * (len(feature_cols) + 1)
844
+
845
+ global_feats = list(part_row_local[part_level_cols])
846
+ return fixed_feats + load_feats + free_feats + global_feats
847
+
848
+ col_names = (
849
+ [f"fixed_{c}" for c in feature_cols]
850
+ + ["fixed_refine_prob"]
851
+ + [f"load_{c}" for c in feature_cols]
852
+ + ["load_refine_prob"]
853
+ + [f"free_{c}" for c in feature_cols]
854
+ + ["free_refine_prob"]
855
+ + part_level_cols
856
+ )
857
+ row = extract_supervised_features_model4()
858
+ df_sup = pd.DataFrame([row], columns=col_names)
859
+
860
+ missing_cols = set(preprocessor4.feature_names_in_) - set(df_sup.columns)
861
+ for c in missing_cols:
862
+ df_sup[c] = 0.0
863
+ df_sup = df_sup[preprocessor4.feature_names_in_]
864
+
865
+ X4 = preprocessor4.transform(df_sup)
866
+ X4_t = torch.tensor(
867
+ X4.toarray() if hasattr(X4, "toarray") else X4, dtype=torch.float32
868
+ )
869
+
870
+ class GlobalSizeRegressor(nn.Module):
871
+ def __init__(self, in_dim):
872
+ super().__init__()
873
+ self.encoder = nn.Sequential(
874
+ nn.Linear(in_dim, 64),
875
+ nn.BatchNorm1d(64),
876
+ nn.ReLU(),
877
+ nn.Dropout(0.2),
878
+ nn.Linear(64, 32),
879
+ nn.ReLU(),
880
+ )
881
+ self.head = nn.Linear(32, 1)
882
+
883
+ def forward(self, x):
884
+ h = self.encoder(x)
885
+ return self.head(h)
886
+
887
+ model4 = GlobalSizeRegressor(X4_t.shape[1])
888
+ state_dict = torch.load("saved_model/model4.pt", map_location="cpu")
889
+ model4.load_state_dict(state_dict)
890
+ model4.eval()
891
+
892
+ with torch.no_grad():
893
+ pred4 = model4(X4_t).item()
894
+
895
+ print("Model 4 (part-level global_size) prediction:")
896
+ print(f"Predicted global_size_final: {pred4:.4f}")
897
+
898
+ # -----------------------------------------------------------------
899
+ # Human-readable sentence
900
+ # -----------------------------------------------------------------
901
+ prob_converged = probs2[0, 1]
902
+ pred_class = preds2[0]
903
+ global_size = pred4
904
+ face1, face2 = top2_with_local_size.itertuples(index=False)
905
+
906
+ if prob_converged > 0.7:
907
+ converge_text = "very likely to converge"
908
+ elif prob_converged > 0.45:
909
+ converge_text = "likely to converge"
910
+ else:
911
+ converge_text = "unlikely to converge"
912
+
913
+ sentence = (
914
+ f"The model predicts that the simulation is {converge_text} "
915
+ f"(confidence: {prob_converged:.2%}). "
916
+ f"To improve accuracy, local refinement will be applied on a {face1.ftype.lower()} (ID {face1.face_id}) "
917
+ f"and a {face2.ftype.lower()} (ID {face2.face_id}), "
918
+ f"with predicted mesh sizes {face1.predicted_local_size:.3f} mm and {face2.predicted_local_size:.3f} mm respectively. "
919
+ f"The overall global mesh size is set to {global_size:.3f} mm."
920
+ )
921
+ print(sentence)
922
+
923
+ # -----------------------------------------------------------------
924
+ # Baseline global-only mesh (for PNG)
925
+ # -----------------------------------------------------------------
926
+ gmsh.model.mesh.clear()
927
+ gmsh.option.setNumber("Mesh.Optimize", 1)
928
+ gmsh.option.setNumber("Mesh.OptimizeNetgen", 1)
929
+ gmsh.option.setNumber("Mesh.Smoothing", 10)
930
+ gmsh.option.setNumber("Mesh.Algorithm", 5)
931
+ gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 0)
932
+ gmsh.option.setNumber("Mesh.MeshSizeFromPoints", 0)
933
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMin", 0)
934
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMax", float(global_size))
935
+
936
+ gmsh.model.mesh.generate(2)
937
+ node_tags_b, node_coords_b, _ = gmsh.model.mesh.getNodes()
938
+ nodes_b = node_coords_b.reshape(-1, 3)
939
+
940
+ types_b, elem_tags_list_b, node_tags_list_b = gmsh.model.mesh.getElements(
941
+ dim=2
942
+ )
943
+
944
+ elements_b = []
945
+ for etype, ntags in zip(types_b, node_tags_list_b):
946
+ name, dim, order, nnode, *_ = gmsh.model.mesh.getElementProperties(etype)
947
+ if nnode in (3, 4):
948
+ conn = ntags.reshape(-1, nnode).astype(int) - 1
949
+ elements_b.append((etype, conn))
950
+
951
+ plot_mesh(
952
+ nodes_b,
953
+ elements_b,
954
+ title="Baseline Mesh (Global Only)",
955
+ highlight_faces=None,
956
+ face_to_nodes=None,
957
+ save_path="output/mesh_global_only.png",
958
+ )
959
+
960
+ # -----------------------------------------------------------------
961
+ # Refined mesh with background field
962
+ # -----------------------------------------------------------------
963
+ gmsh.model.mesh.clear()
964
+ gmsh.option.setNumber("Mesh.Optimize", 1)
965
+ gmsh.option.setNumber("Mesh.OptimizeNetgen", 1)
966
+ gmsh.option.setNumber("Mesh.Smoothing", 10)
967
+ gmsh.option.setNumber("Mesh.Algorithm", 5)
968
+ gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 0)
969
+
970
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMin", global_size / 4)
971
+ gmsh.option.setNumber("Mesh.CharacteristicLengthMax", float(global_size))
972
+
973
+ face_ids = top2_with_local_size["face_id"].tolist()
974
+ local_sizes = top2_with_local_size["predicted_local_size"].round(3).tolist()
975
+
976
+ # Collect curves for distance
977
+ curve_ids = []
978
+ for fid in face_ids:
979
+ surf = fid - 1
980
+ boundary = gmsh.model.getBoundary([(2, surf)], oriented=False)
981
+ for (dim, ctag) in boundary:
982
+ if dim == 1:
983
+ curve_ids.append(ctag)
984
+
985
+ # Distance field
986
+ f_dist = gmsh.model.mesh.field.add("Distance")
987
+ gmsh.model.mesh.field.setNumbers(f_dist, "CurvesList", curve_ids)
988
+ gmsh.model.mesh.field.setNumber(f_dist, "Sampling", 200)
989
+
990
+ # Threshold field
991
+ min_local = min(local_sizes) * 2
992
+ f_th = gmsh.model.mesh.field.add("Threshold")
993
+ gmsh.model.mesh.field.setNumber(f_th, "InField", f_dist)
994
+ gmsh.model.mesh.field.setNumber(f_th, "SizeMin", float(min_local))
995
+ gmsh.model.mesh.field.setNumber(f_th, "SizeMax", float(global_size))
996
+ gmsh.model.mesh.field.setNumber(f_th, "DistMin", 0.0)
997
+ gmsh.model.mesh.field.setNumber(f_th, "DistMax", global_size * 2.0)
998
+
999
+ # Min field (clamped)
1000
+ f_min = gmsh.model.mesh.field.add("Min")
1001
+ gmsh.model.mesh.field.setNumbers(f_min, "FieldsList", [f_th])
1002
+
1003
+ gmsh.model.mesh.field.setAsBackgroundMesh(f_min)
1004
+
1005
+ gmsh.model.occ.synchronize()
1006
+ gmsh.model.mesh.generate(2)
1007
+ gmsh.model.mesh.optimize("Netgen")
1008
+ gmsh.model.occ.synchronize()
1009
+
1010
+ # -----------------------------------------------------------------
1011
+ # Extract FE mesh nodes & triangles (compact indexing)
1012
+ # -----------------------------------------------------------------
1013
+ all_node_tags, all_node_coords, _ = gmsh.model.mesh.getNodes()
1014
+ all_nodes = all_node_coords.reshape(-1, 3)
1015
+
1016
+ types_all, elem_tags_all, elem_nodes_all = gmsh.model.mesh.getElements()
1017
+
1018
+ used_node_tags = np.unique(
1019
+ np.concatenate([arr for arr in elem_nodes_all])
1020
+ )
1021
+ used_idx = used_node_tags - 1
1022
+ nodes_r = all_nodes[used_idx]
1023
+
1024
+ tag_to_new_index = {tag: i for i, tag in enumerate(used_node_tags)}
1025
+
1026
+ triangle_list = []
1027
+ for etype, conn in zip(types_all, elem_nodes_all):
1028
+ name, dim, order, nnode, *_ = gmsh.model.mesh.getElementProperties(etype)
1029
+ if dim != 2:
1030
+ continue
1031
+
1032
+ if nnode == 3:
1033
+ raw = conn.reshape(-1, 3)
1034
+ remapped = np.vectorize(tag_to_new_index.get)(raw)
1035
+ triangle_list.extend(remapped)
1036
+ elif nnode == 4:
1037
+ raw = conn.reshape(-1, 4)
1038
+ remapped = np.vectorize(tag_to_new_index.get)(raw)
1039
+ for quad in remapped:
1040
+ triangle_list.append([quad[0], quad[1], quad[2]])
1041
+ triangle_list.append([quad[0], quad[2], quad[3]])
1042
+
1043
+ triangle_array = np.array(triangle_list, dtype=int)
1044
+
1045
+ print("Triangles extracted:", triangle_array.shape)
1046
+ print("FE MESH NODES:", len(nodes_r))
1047
+ print("FE MESH TRIANGLES:", len(triangle_array))
1048
+
1049
+ # For PNG plotter
1050
+ elements_r = [(0, triangle_array)]
1051
+
1052
+ # -----------------------------------------------------------------
1053
+ # Mapping gmsh surfaces → local node indices (for PNG highlight)
1054
+ # -----------------------------------------------------------------
1055
+ face_to_nodes = {}
1056
+ for (dim, sid) in gmsh.model.getEntities(dim=2):
1057
+ etypes_s, elem_tags_s, ntags_s = gmsh.model.mesh.getElements(
1058
+ dim=2, tag=sid
1059
+ )
1060
+ surface_conn_local = []
1061
+ for etype, ntags in zip(etypes_s, ntags_s):
1062
+ _, _, _, nnode, *_ = gmsh.model.mesh.getElementProperties(etype)
1063
+ if nnode in (3, 4):
1064
+ raw = ntags.reshape(-1, nnode)
1065
+ # map gmsh node tags → compact indices
1066
+ mapped = np.vectorize(tag_to_new_index.get)(raw)
1067
+ surface_conn_local.extend(mapped)
1068
+ face_to_nodes[sid] = surface_conn_local
1069
+
1070
+ highlight_gmsh_ids = [fid - 1 for fid in face_ids]
1071
+
1072
+ # -----------------------------------------------------------------
1073
+ # Local refinement mask (which triangles are "near" refined faces)
1074
+ # -----------------------------------------------------------------
1075
+ refined_nodes_set = set()
1076
+ for sid in highlight_gmsh_ids:
1077
+ if sid in face_to_nodes:
1078
+ for conn in face_to_nodes[sid]:
1079
+ for n in conn:
1080
+ refined_nodes_set.add(int(n))
1081
+
1082
+ refined_tri_mask = np.zeros(triangle_array.shape[0], dtype=bool)
1083
+ for t_idx, tri in enumerate(triangle_array):
1084
+ if any(int(v) in refined_nodes_set for v in tri):
1085
+ refined_tri_mask[t_idx] = True
1086
+
1087
+ # -----------------------------------------------------------------
1088
+ # Heatmap: per-vertex "local mesh size" (smaller → more refined)
1089
+ # -----------------------------------------------------------------
1090
+ n_nodes = nodes_r.shape[0]
1091
+ n_tris = triangle_array.shape[0]
1092
+
1093
+ tri_sizes = np.zeros(n_tris, dtype=float)
1094
+ for t_idx, (a, b, c) in enumerate(triangle_array):
1095
+ pa, pb, pc = nodes_r[[a, b, c]]
1096
+ area = 0.5 * np.linalg.norm(np.cross(pb - pa, pc - pa))
1097
+ # characteristic length ~ sqrt(area)
1098
+ tri_sizes[t_idx] = np.sqrt(area + 1e-16)
1099
+
1100
+ vertex_sum = np.zeros(n_nodes, dtype=float)
1101
+ vertex_count = np.zeros(n_nodes, dtype=float)
1102
+ for t_idx, tri in enumerate(triangle_array):
1103
+ s = tri_sizes[t_idx]
1104
+ for v in tri:
1105
+ vertex_sum[v] += s
1106
+ vertex_count[v] += 1.0
1107
+
1108
+ vertex_size = vertex_sum / np.maximum(vertex_count, 1.0)
1109
+
1110
+ smin, smax = float(vertex_size.min()), float(vertex_size.max())
1111
+ if smax > smin:
1112
+ # invert so small elements → high intensity
1113
+ vertex_refinement_values = 1.0 - (vertex_size - smin) / (smax - smin)
1114
+ else:
1115
+ vertex_refinement_values = np.zeros_like(vertex_size)
1116
+
1117
+ # -----------------------------------------------------------------
1118
+ # Save refined mesh PNG with red outlines on refined faces
1119
+ # -----------------------------------------------------------------
1120
+ plot_mesh(
1121
+ nodes_r,
1122
+ elements_r,
1123
+ title="Refined Mesh (Local + Global)",
1124
+ highlight_faces=highlight_gmsh_ids,
1125
+ face_to_nodes=face_to_nodes,
1126
+ save_path="output/mesh_refined.png",
1127
+ )
1128
+
1129
+ gmsh.write("output/mesh_refined.msh")
1130
+
1131
+ # -----------------------------------------------------------------
1132
+ # Interactive Plotly figure (wireframe / highlight / heatmap)
1133
+ # -----------------------------------------------------------------
1134
+ interactive_fig = make_plotly_mesh(
1135
+ nodes_r,
1136
+ triangle_array,
1137
+ title="Refined Mesh (Local + Global)",
1138
+ view_mode=view_mode,
1139
+ refined_tri_mask=refined_tri_mask,
1140
+ vertex_refinement_values=vertex_refinement_values,
1141
+ )
1142
+
1143
+ msh_path = "output/mesh_refined.msh"
1144
+ return (
1145
+ sentence,
1146
+ "output/mesh_global_only.png",
1147
+ "output/mesh_refined.png",
1148
+ interactive_fig,
1149
+ msh_path,
1150
+ )
mesh_service/step_feature_detection.py ADDED
@@ -0,0 +1,1075 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import json
3
+ from OCP.STEPControl import STEPControl_Reader
4
+ from OCP.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_IN, TopAbs_REVERSED, TopAbs_VERTEX
5
+ from OCP.TopExp import TopExp_Explorer, TopExp
6
+ from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Edge
7
+ from OCP.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
8
+ from OCP.BRep import BRep_Tool
9
+ from OCP.GeomAbs import GeomAbs_Cylinder, GeomAbs_Plane, GeomAbs_Circle, GeomAbs_Line
10
+ from OCP.BRepGProp import BRepGProp
11
+ from OCP.GProp import GProp_GProps
12
+ from OCP.BRepClass3d import BRepClass3d_SolidClassifier
13
+ from OCP.gp import gp_Pnt, gp_Vec
14
+ import numpy as np
15
+ from OCP.Bnd import Bnd_Box
16
+ from OCP.BRepBndLib import BRepBndLib
17
+ from collections import Counter
18
+
19
+ def face_bbox_center(face):
20
+ box = Bnd_Box()
21
+ BRepBndLib.Add_s(face, box)
22
+ xmin, ymin, zmin, xmax, ymax, zmax = box.Get()
23
+ return [
24
+ (xmin + xmax) / 2,
25
+ (ymin + ymax) / 2,
26
+ (zmin + zmax) / 2
27
+ ]
28
+
29
+
30
+ def load_step_model(step_path):
31
+ """Load a STEP file and return the shape object."""
32
+ reader = STEPControl_Reader()
33
+ status = reader.ReadFile(step_path)
34
+ if status != 1:
35
+ raise RuntimeError(f"Failed to load STEP file: {step_path}")
36
+ reader.TransferRoot()
37
+ shape = reader.Shape()
38
+ return shape
39
+
40
+ def load_step_unit(step_path):
41
+ with open(step_path, "r") as f:
42
+ for line in f:
43
+ if "SI_UNIT" in line:
44
+ print("STEP file unit info:", line.strip())
45
+ return line.strip()
46
+
47
+ def detect_faces(shape):
48
+ faces = []
49
+ exp = TopExp_Explorer(shape, TopAbs_FACE)
50
+ idx = 0
51
+ while exp.More():
52
+ face_shape = exp.Current()
53
+ face = TopoDS.Face_s(face_shape)
54
+ surf = BRep_Tool.Surface_s(face)
55
+ faces.append({"face": face, "surface": surf})
56
+ idx = idx+1
57
+ exp.Next()
58
+ return faces
59
+
60
+
61
+ def is_hole(face, shape):
62
+ """
63
+ Determine if a cylindrical face is a hole (concave) rather than a boss (convex).
64
+ Basic approach: check if the face normal points toward the solid interior.
65
+ """
66
+ # Get a point and normal on the cylindrical surface
67
+ adaptor = BRepAdaptor_Surface(face, True)
68
+ u_min, u_max = adaptor.FirstUParameter(), adaptor.LastUParameter()
69
+ v_min, v_max = adaptor.FirstVParameter(), adaptor.LastVParameter()
70
+ u_mid = (u_min + u_max) / 2
71
+ v_mid = (v_min + v_max) / 2
72
+
73
+ # Get point and normal vector at mid-parameter
74
+ point = adaptor.Value(u_mid, v_mid)
75
+ d1u = gp_Vec()
76
+ d1v = gp_Vec()
77
+ adaptor.D1(u_mid, v_mid, point, d1u, d1v)
78
+
79
+ # Calculate normal vector (cross product of partial derivatives)
80
+ normal = d1u.Crossed(d1v)
81
+ normal.Normalize()
82
+
83
+ # Offset point along normal direction
84
+ offset = 0.01 # Small offset distance
85
+ test_point = gp_Pnt(
86
+ point.X() + normal.X() * offset,
87
+ point.Y() + normal.Y() * offset,
88
+ point.Z() + normal.Z() * offset
89
+ )
90
+
91
+ # Check if the offset point is inside or outside the solid
92
+ classifier = BRepClass3d_SolidClassifier(shape)
93
+ classifier.Perform(test_point, 1e-7)
94
+
95
+ # If normal points outward, offset point should be outside -> this is a hole
96
+ # If normal points outward, offset point is still inside -> this is a boss
97
+ return classifier.State() == TopAbs_IN
98
+
99
+
100
+ def compute_face_angle_span(face, axis_location, axis_direction):
101
+ axis_loc = np.array(axis_location, dtype=float)
102
+ axis_dir = np.array(axis_direction, dtype=float)
103
+ axis_dir = axis_dir / np.linalg.norm(axis_dir)
104
+ ref = np.array([1.0, 0.0, 0.0], dtype=float)
105
+ if abs(np.dot(ref, axis_dir)) > 0.9:
106
+ ref = np.array([0.0, 1.0, 0.0], dtype=float)
107
+
108
+ e1 = np.cross(axis_dir, ref)
109
+ e1 = e1 / np.linalg.norm(e1)
110
+ e2 = np.cross(axis_dir, e1)
111
+ e2 = e2 / np.linalg.norm(e2)
112
+
113
+ angles = []
114
+
115
+ edge_exp = TopExp_Explorer(face, TopAbs_EDGE)
116
+ while edge_exp.More():
117
+ edge_shape = edge_exp.Current()
118
+ vert_exp = TopExp_Explorer(edge_shape, TopAbs_VERTEX)
119
+ while vert_exp.More():
120
+ vert_shape = vert_exp.Current()
121
+ vertex = TopoDS.Vertex_s(vert_shape)
122
+ p = BRep_Tool.Pnt_s(vertex)
123
+ v = np.array([p.X(), p.Y(), p.Z()], dtype=float) - axis_loc
124
+
125
+ v_proj = v - np.dot(v, axis_dir) * axis_dir
126
+ norm_v = np.linalg.norm(v_proj)
127
+ if norm_v < 1e-6:
128
+ vert_exp.Next()
129
+ continue
130
+
131
+ x = np.dot(v_proj, e1)
132
+ y = np.dot(v_proj, e2)
133
+ theta = math.atan2(y, x)
134
+ angles.append(theta)
135
+
136
+ vert_exp.Next()
137
+ edge_exp.Next()
138
+
139
+ if not angles:
140
+ return 0.0
141
+
142
+ angles = np.array(angles)
143
+ angles = np.mod(angles, 2.0 * math.pi)
144
+ angles.sort()
145
+
146
+ diffs = np.diff(angles)
147
+ wrap_gap = (angles[0] + 2.0 * math.pi) - angles[-1]
148
+ max_gap = max(diffs.max(initial=0.0), wrap_gap)
149
+
150
+ coverage_rad = 2.0 * math.pi - max_gap
151
+ coverage_deg = math.degrees(coverage_rad)
152
+ return float(coverage_deg)
153
+
154
+
155
+ def detect_circular_holes(shape, min_radius=0.5, max_radius=50.0, merge_tolerance=1e-3):
156
+ """
157
+ Detect all cylindrical (round) holes, including through-holes and blind holes.
158
+ Merges faces belonging to the same hole.
159
+ Returns: List of dicts with center, radius, normal, area, etc.
160
+ """
161
+ candidate_faces = []
162
+ idx = 0
163
+
164
+ for fdict in detect_faces(shape):
165
+ face = fdict["face"]
166
+ surf = fdict["surface"]
167
+
168
+ # Check if surface type is cylindrical
169
+ surface_type = surf.DynamicType().Name()
170
+ if "Cylindrical" not in surface_type:
171
+ continue
172
+
173
+ # Use BRepAdaptor_Surface to get cylinder parameters
174
+ adaptor = BRepAdaptor_Surface(face, True)
175
+
176
+ # Confirm it's a cylinder type
177
+ if adaptor.GetType() != GeomAbs_Cylinder:
178
+ continue
179
+
180
+ cylinder = adaptor.Cylinder()
181
+ radius = cylinder.Radius()
182
+
183
+ # Only consider reasonable hole sizes
184
+ if not (min_radius <= radius <= max_radius):
185
+ continue
186
+
187
+ # **Key judgment: check if it's a hole (not a boss)**
188
+ if not is_hole(face, shape):
189
+ continue # Skip bosses/shafts
190
+
191
+ # Get axis direction and location
192
+ axis_direction = cylinder.Axis().Direction()
193
+ axis_location = cylinder.Axis().Location()
194
+ axis_dir_vec = [axis_direction.X(), axis_direction.Y(), axis_direction.Z()]
195
+ axis_loc_vec = [axis_location.X(), axis_location.Y(), axis_location.Z()]
196
+
197
+ # Calculate surface area and center
198
+ gprops = GProp_GProps()
199
+ BRepGProp.SurfaceProperties_s(face, gprops)
200
+ area = gprops.Mass()
201
+ center = gprops.CentreOfMass()
202
+
203
+ angle_span = compute_face_angle_span(face, axis_loc_vec, axis_dir_vec)
204
+
205
+ candidate_faces.append({
206
+ "face_id": idx,
207
+ "face": face,
208
+ "radius": float(radius),
209
+ "center": [center.X(), center.Y(), center.Z()],
210
+ "axis_direction": [axis_direction.X(), axis_direction.Y(), axis_direction.Z()],
211
+ "axis_location": [axis_location.X(), axis_location.Y(), axis_location.Z()],
212
+ "area": float(area),
213
+ "angle_span": float(angle_span)
214
+ })
215
+
216
+ idx = idx+1
217
+
218
+ # Group faces that belong to the same hole
219
+ holes = merge_coaxial_cylinders(candidate_faces, merge_tolerance)
220
+
221
+ print(f"Detected {len(holes)} circular holes (from {len(candidate_faces)} cylindrical faces)")
222
+ return holes
223
+
224
+
225
+ def merge_coaxial_cylinders(faces, tolerance=1e-3, angle_threshold_deg=350.0):
226
+ """
227
+ Merge cylindrical faces that share the same axis (belong to same hole).
228
+ Two cylinders belong to the same hole if:
229
+ 1. They have the same radius (within tolerance)
230
+ 2. They are coaxial (share the same axis)
231
+ """
232
+ if not faces:
233
+ return []
234
+
235
+ merged_holes = []
236
+ used = [False] * len(faces)
237
+
238
+ for i, face1 in enumerate(faces):
239
+ if used[i]:
240
+ continue
241
+
242
+ # Start a new hole group
243
+ hole_group = [face1]
244
+ used[i] = True
245
+
246
+ # Find all faces belonging to the same hole
247
+ for j, face2 in enumerate(faces):
248
+ if used[j] or i == j:
249
+ continue
250
+
251
+ # Check if same radius
252
+ if abs(face1["radius"] - face2["radius"]) > tolerance:
253
+ continue
254
+
255
+ # Check if coaxial
256
+ if are_coaxial(face1, face2, tolerance):
257
+ hole_group.append(face2)
258
+ used[j] = True
259
+
260
+ total_angle = sum(f.get("angle_span", 0.0) for f in hole_group)
261
+ total_angle = min(total_angle, 360)
262
+ if total_angle < angle_threshold_deg:
263
+ continue
264
+
265
+ # Compute merged hole properties
266
+ merged_hole = merge_hole_group(hole_group, i)
267
+ merged_holes.append(merged_hole)
268
+
269
+ return merged_holes
270
+
271
+
272
+ def are_coaxial(face1, face2, tolerance=1e-3):
273
+ """
274
+ Check if two cylindrical faces are coaxial (share the same axis).
275
+ """
276
+ import numpy as np
277
+
278
+ # Get axis directions
279
+ dir1 = np.array(face1["axis_direction"])
280
+ dir2 = np.array(face2["axis_direction"])
281
+
282
+ # Normalize
283
+ dir1 = dir1 / np.linalg.norm(dir1)
284
+ dir2 = dir2 / np.linalg.norm(dir2)
285
+
286
+ # Check if parallel (dot product close to ±1)
287
+ dot_product = abs(np.dot(dir1, dir2))
288
+ if abs(dot_product - 1.0) > tolerance:
289
+ return False
290
+
291
+ # Check if axes are coincident
292
+ # Distance from point on axis1 to axis2
293
+ loc1 = np.array(face1["axis_location"])
294
+ loc2 = np.array(face2["axis_location"])
295
+
296
+ # Vector from loc1 to loc2
297
+ vec = loc2 - loc1
298
+
299
+ # Distance from loc2 to axis1 (perpendicular distance)
300
+ distance = np.linalg.norm(vec - np.dot(vec, dir1) * dir1)
301
+
302
+ return distance < tolerance
303
+
304
+
305
+ def merge_hole_group(hole_group, id):
306
+ """
307
+ Merge multiple faces belonging to the same hole into one hole feature,
308
+ and record all involved faces for downstream mapping.
309
+ """
310
+ import numpy as np
311
+
312
+ representative = hole_group[0]
313
+ total_area = sum(face["area"] for face in hole_group)
314
+
315
+ centers = np.array([face["center"] for face in hole_group])
316
+ areas = np.array([face["area"] for face in hole_group])
317
+ avg_center = np.average(centers, axis=0, weights=areas)
318
+
319
+ # Clean up floating point errors
320
+ avg_center = np.round(avg_center, decimals=6)
321
+ faces_info = [
322
+ {
323
+ "face_id": face.get("face_id"),
324
+ "center": [round(x, 6) for x in face["center"]],
325
+ "area": round(face["area"], 6),
326
+ "angle_span": round(face.get("angle_span", 0.0), 4),
327
+ "normal": [round(x, 6) for x in face.get("normal", [0,0,0])]
328
+ }
329
+ for face in hole_group
330
+ ]
331
+
332
+ return {
333
+ "name": f"CIRC_HOLE_{id}",
334
+ "type": "circular_hole",
335
+ "radius": round(representative["radius"], 6),
336
+ "center": avg_center.tolist(),
337
+ "normal": representative["axis_direction"],
338
+ "area": round(total_area, 6),
339
+ "num_faces": len(hole_group),
340
+ "faces": faces_info
341
+ }
342
+
343
+
344
+
345
+ def detect_non_circular_holes(shape, min_area=1.0, max_area=1000.0, distance_threshold=10.0):
346
+ """
347
+ Detect non-circular holes (rectangular holes, slots, elongated holes).
348
+ Strategy: Find groups of planar faces that form a closed pocket.
349
+ """
350
+ result = []
351
+ planar_faces = []
352
+
353
+ # 1. Collect all planar faces that might be part of a hole
354
+ for fdict in detect_faces(shape):
355
+ face = fdict["face"]
356
+ surf = fdict["surface"]
357
+
358
+ surface_type = surf.DynamicType().Name()
359
+ if "Plane" not in surface_type:
360
+ continue
361
+
362
+ # Check if this plane is a hole (concave)
363
+ if not is_hole(face, shape):
364
+ continue
365
+
366
+ # Get face properties
367
+ adaptor = BRepAdaptor_Surface(face, True)
368
+
369
+ # Confirm it's a plane type
370
+ if adaptor.GetType() != GeomAbs_Plane:
371
+ continue
372
+
373
+ plane = adaptor.Plane()
374
+ normal = plane.Axis().Direction()
375
+
376
+ gprops = GProp_GProps()
377
+ BRepGProp.SurfaceProperties_s(face, gprops)
378
+ area = gprops.Mass()
379
+ center = gprops.CentreOfMass()
380
+
381
+ # Filter by area
382
+ if not (min_area <= area <= max_area):
383
+ continue
384
+
385
+ planar_faces.append({
386
+ "face": face,
387
+ "center": [center.X(), center.Y(), center.Z()],
388
+ "normal": [normal.X(), normal.Y(), normal.Z()],
389
+ "area": float(area)
390
+ })
391
+
392
+ # 2. Group adjacent planar faces that might form a hole
393
+ holes = group_planar_holes(planar_faces, distance_threshold)
394
+
395
+ # 3. Classify hole type (rectangular, slot, etc.)
396
+ for hole_group in holes:
397
+ hole_type = classify_planar_hole(hole_group)
398
+ result.append(hole_type)
399
+
400
+ print(f"Detected {len(result)} non-circular holes (from {len(planar_faces)} planar faces)")
401
+ return result
402
+
403
+
404
+ def group_planar_holes(faces, distance_threshold=10.0):
405
+ """
406
+ Group planar faces that are close to each other and might form a hole.
407
+ """
408
+ if not faces:
409
+ return []
410
+
411
+ groups = []
412
+ used = [False] * len(faces)
413
+
414
+ for i, face1 in enumerate(faces):
415
+ if used[i]:
416
+ continue
417
+
418
+ # Start a new group
419
+ group = [face1]
420
+ used[i] = True
421
+
422
+ # Find nearby faces with similar normal direction
423
+ for j, face2 in enumerate(faces):
424
+ if used[j] or i == j:
425
+ continue
426
+
427
+ # Check if normals are similar (parallel faces)
428
+ normal1 = np.array(face1["normal"])
429
+ normal2 = np.array(face2["normal"])
430
+
431
+ # Normalize
432
+ normal1 = normal1 / np.linalg.norm(normal1)
433
+ normal2 = normal2 / np.linalg.norm(normal2)
434
+
435
+ dot = abs(np.dot(normal1, normal2))
436
+
437
+ if dot > 0.9: # Nearly parallel
438
+ # Check distance between centers
439
+ center1 = np.array(face1["center"])
440
+ center2 = np.array(face2["center"])
441
+ distance = np.linalg.norm(center2 - center1)
442
+
443
+ if distance < distance_threshold:
444
+ group.append(face2)
445
+ used[j] = True
446
+
447
+ groups.append(group)
448
+
449
+ return groups
450
+
451
+
452
+ def classify_planar_hole(face_group):
453
+ """
454
+ Classify a group of planar faces into hole types.
455
+ Simple classification based on number of faces and geometry.
456
+ """
457
+ num_faces = len(face_group)
458
+
459
+ # Calculate total area and average center
460
+ total_area = sum(f["area"] for f in face_group)
461
+ centers = np.array([f["center"] for f in face_group])
462
+ avg_center = np.mean(centers, axis=0)
463
+
464
+ # Use the first face's normal as representative
465
+ representative_normal = face_group[0]["normal"]
466
+
467
+ # Simple classification based on number of faces
468
+ if num_faces == 1:
469
+ hole_type = "simple_pocket"
470
+ elif num_faces == 4:
471
+ hole_type = "rectangular_hole"
472
+ elif num_faces == 2:
473
+ hole_type = "slot"
474
+ elif num_faces >= 5:
475
+ hole_type = "complex_pocket"
476
+ else:
477
+ hole_type = "irregular_hole"
478
+
479
+ return {
480
+ "type": hole_type,
481
+ "center": np.round(avg_center, 6).tolist(),
482
+ "normal": representative_normal,
483
+ "area": round(total_area, 6),
484
+ "num_faces": num_faces
485
+ }
486
+
487
+ def detect_fillets(shape, min_radius=0.1, max_radius=50.0,
488
+ classify_type=True, hole_merge_tolerance=1e-3,
489
+ fillet_merge_tolerance=1e-3,
490
+ min_total_angle_deg=3.0,
491
+ max_total_angle_deg=330.0):
492
+ hole_features = detect_circular_holes(
493
+ shape,
494
+ min_radius=min_radius,
495
+ max_radius=max_radius,
496
+ merge_tolerance=hole_merge_tolerance
497
+ )
498
+
499
+ hole_cylinders = []
500
+ for h in hole_features:
501
+ hole_cylinders.append({
502
+ "radius": h["radius"],
503
+ "axis_direction": h["normal"],
504
+ "axis_location": h["center"],
505
+ })
506
+
507
+ fillet_candidates = []
508
+ face_idx = 0
509
+
510
+ for fdict in detect_faces(shape):
511
+ face = fdict["face"]
512
+ surf = fdict["surface"]
513
+
514
+ surface_type = surf.DynamicType().Name()
515
+ if "Cylindrical" not in surface_type:
516
+ continue
517
+
518
+ adaptor = BRepAdaptor_Surface(face, True)
519
+ if adaptor.GetType() != GeomAbs_Cylinder:
520
+ continue
521
+
522
+ cylinder = adaptor.Cylinder()
523
+ radius = cylinder.Radius()
524
+
525
+ if not (min_radius <= radius <= max_radius):
526
+ continue
527
+
528
+ axis_direction = cylinder.Axis().Direction()
529
+ axis_location = cylinder.Axis().Location()
530
+ axis_dir_vec = [axis_direction.X(), axis_direction.Y(), axis_direction.Z()]
531
+ axis_loc_vec = [axis_location.X(), axis_location.Y(), axis_location.Z()]
532
+
533
+ is_on_hole_cylinder = False
534
+ for hc in hole_cylinders:
535
+ if abs(hc["radius"] - radius) > hole_merge_tolerance:
536
+ continue
537
+
538
+ if are_coaxial(
539
+ hc,
540
+ {"axis_direction": axis_dir_vec, "axis_location": axis_loc_vec},
541
+ tolerance=hole_merge_tolerance,
542
+ ):
543
+ is_on_hole_cylinder = True
544
+ break
545
+
546
+ if is_on_hole_cylinder:
547
+ continue
548
+
549
+ gprops = GProp_GProps()
550
+ BRepGProp.SurfaceProperties_s(face, gprops)
551
+ area = gprops.Mass()
552
+ center = gprops.CentreOfMass()
553
+
554
+ if not is_fillet_surface(face, shape, radius, area,
555
+ full_angle_threshold_deg=330.0,
556
+ min_angle_threshold_deg=3.0):
557
+ continue
558
+
559
+ angle_span = compute_face_angle_span(face, axis_loc_vec, axis_dir_vec)
560
+
561
+ if classify_type:
562
+ if is_internal_fillet(face, shape):
563
+ fillet_type = "internal_fillet"
564
+ else:
565
+ fillet_type = "external_fillet"
566
+ else:
567
+ fillet_type = "fillet"
568
+
569
+ fillet_candidates.append({
570
+ "face_id": face_idx,
571
+ "face": face,
572
+ "name": f"FILLET_FACE_{face_idx}",
573
+ "type": fillet_type,
574
+ "radius": float(radius),
575
+ "center": [center.X(), center.Y(), center.Z()],
576
+ "axis_direction": axis_dir_vec,
577
+ "axis_location": axis_loc_vec,
578
+ "area": float(area),
579
+ "angle_span": float(angle_span),
580
+ })
581
+
582
+ face_idx += 1
583
+
584
+ merged_fillets = merge_coaxial_fillets(
585
+ fillet_candidates,
586
+ tolerance=fillet_merge_tolerance,
587
+ min_total_angle_deg=min_total_angle_deg,
588
+ max_total_angle_deg=max_total_angle_deg,
589
+ )
590
+
591
+ print(f"Detected {len(merged_fillets)} fillet features (from {len(fillet_candidates)} cylindrical faces)")
592
+
593
+ return merged_fillets
594
+
595
+
596
+ def is_fillet_surface(face, shape, radius, area,
597
+ full_angle_threshold_deg=330.0,
598
+ min_angle_threshold_deg=3.0):
599
+
600
+ adaptor = BRepAdaptor_Surface(face, True)
601
+ if adaptor.GetType() != GeomAbs_Cylinder:
602
+ return False
603
+
604
+ cylinder = adaptor.Cylinder()
605
+ axis_direction = cylinder.Axis().Direction()
606
+ axis_location = cylinder.Axis().Location()
607
+
608
+ axis_dir_vec = [axis_direction.X(), axis_direction.Y(), axis_direction.Z()]
609
+ axis_loc_vec = [axis_location.X(), axis_location.Y(), axis_location.Z()]
610
+
611
+ angle_span = compute_face_angle_span(face, axis_loc_vec, axis_dir_vec)
612
+
613
+ if angle_span >= full_angle_threshold_deg:
614
+ return False
615
+
616
+ if angle_span <= min_angle_threshold_deg:
617
+ return False
618
+
619
+ return True
620
+
621
+
622
+ def is_internal_fillet(face, shape):
623
+ """
624
+ Determine if a cylindrical fillet is internal (concave) or external (convex)
625
+ WITHOUT using adaptor.Normal (which does not exist).
626
+
627
+ Method:
628
+ - Get point P(u,v) on surface
629
+ - Compute radial direction = (P - axis_location) - projection onto axis_dir
630
+ - Normal = normalized radial direction
631
+ - Offset P slightly along normal, test if inside solid
632
+ """
633
+ adaptor = BRepAdaptor_Surface(face, True)
634
+
635
+ # Must be cylinder
636
+ if adaptor.GetType() != GeomAbs_Cylinder:
637
+ return False
638
+
639
+ cylinder = adaptor.Cylinder()
640
+
641
+ # Cylinder axis
642
+ axis = cylinder.Axis()
643
+ axis_loc = axis.Location()
644
+ axis_dir = axis.Direction()
645
+ axis_dir_vec = gp_Vec(axis_dir.X(), axis_dir.Y(), axis_dir.Z())
646
+ axis_dir_vec.Normalize()
647
+
648
+ # Pick midpoint parameter
649
+ u_min, u_max = adaptor.FirstUParameter(), adaptor.LastUParameter()
650
+ v_min, v_max = adaptor.FirstVParameter(), adaptor.LastVParameter()
651
+ u_mid = 0.5 * (u_min + u_max)
652
+ v_mid = 0.5 * (v_min + v_max)
653
+
654
+ # Point on surface
655
+ p = adaptor.Value(u_mid, v_mid)
656
+
657
+ # Compute radial vector: r = (p - axis_loc) - projection onto axis_dir
658
+ vec_p_axis = gp_Vec(p.X() - axis_loc.X(),
659
+ p.Y() - axis_loc.Y(),
660
+ p.Z() - axis_loc.Z())
661
+ # Projection length
662
+ proj_len = vec_p_axis.Dot(axis_dir_vec)
663
+ # Projection vector
664
+ proj_vec = gp_Vec(axis_dir_vec.X() * proj_len,
665
+ axis_dir_vec.Y() * proj_len,
666
+ axis_dir_vec.Z() * proj_len)
667
+ # Radial direction (perpendicular to axis)
668
+ radial_vec = gp_Vec(vec_p_axis.X() - proj_vec.X(),
669
+ vec_p_axis.Y() - proj_vec.Y(),
670
+ vec_p_axis.Z() - proj_vec.Z())
671
+ radial_vec.Normalize()
672
+
673
+ # Normal = radial direction
674
+ normal = radial_vec
675
+
676
+ # Offset point slightly along normal
677
+ offset = 1e-4
678
+ offset_p = gp_Pnt(p.X() + normal.X() * offset,
679
+ p.Y() + normal.Y() * offset,
680
+ p.Z() + normal.Z() * offset)
681
+
682
+ # Test whether it's inside
683
+ classifier = BRepClass3d_SolidClassifier(shape)
684
+ classifier.Perform(offset_p, 1e-7)
685
+
686
+ # If offset point is inside → normal pointing inward → concave fillet
687
+ return classifier.State() == TopAbs_IN
688
+
689
+
690
+ def merge_coaxial_fillets(faces, tolerance=1e-3,
691
+ min_total_angle_deg=3.0,
692
+ max_total_angle_deg=330.0):
693
+ if not faces:
694
+ return []
695
+
696
+ import numpy as np
697
+
698
+ merged_fillets = []
699
+ used = [False] * len(faces)
700
+
701
+ for i, f1 in enumerate(faces):
702
+ if used[i]:
703
+ continue
704
+
705
+ group = [f1]
706
+ used[i] = True
707
+
708
+ for j, f2 in enumerate(faces):
709
+ if used[j] or i == j:
710
+ continue
711
+ if f1.get("type") != f2.get("type"):
712
+ continue
713
+
714
+ if abs(f1["radius"] - f2["radius"]) > tolerance:
715
+ continue
716
+
717
+ if not are_coaxial(f1, f2, tolerance=tolerance):
718
+ continue
719
+
720
+ group.append(f2)
721
+ used[j] = True
722
+
723
+ total_angle = sum(g.get("angle_span", 0.0) for g in group)
724
+ total_angle = min(total_angle, 360.0)
725
+
726
+ if total_angle < min_total_angle_deg:
727
+ continue
728
+
729
+ if total_angle > max_total_angle_deg:
730
+ continue
731
+
732
+ merged = merge_fillet_coaxial_group(group, len(merged_fillets), total_angle)
733
+ merged_fillets.append(merged)
734
+
735
+ return merged_fillets
736
+
737
+
738
+ def merge_fillet_coaxial_group(fillet_group, group_index, total_angle):
739
+ if not fillet_group:
740
+ return {}
741
+
742
+ areas = np.array([f["area"] for f in fillet_group], dtype=float)
743
+ radii = np.array([f["radius"] for f in fillet_group], dtype=float)
744
+ centers = np.array([f["center"] for f in fillet_group], dtype=float)
745
+ axis_dirs = np.array([f["axis_direction"] for f in fillet_group], dtype=float)
746
+ axis_locs = np.array([f["axis_location"] for f in fillet_group], dtype=float)
747
+
748
+ total_area = float(areas.sum()) if areas.size > 0 else 0.0
749
+
750
+ if total_area > 0:
751
+ avg_center = (centers * areas[:, None]).sum(axis=0) / total_area
752
+ avg_radius = float((radii * areas).sum() / total_area)
753
+ avg_axis_dir = (axis_dirs * areas[:, None]).sum(axis=0) / total_area
754
+ avg_axis_loc = (axis_locs * areas[:, None]).sum(axis=0) / total_area
755
+ else:
756
+ avg_center = centers.mean(axis=0)
757
+ avg_radius = float(radii.mean())
758
+ avg_axis_dir = axis_dirs.mean(axis=0)
759
+ avg_axis_loc = axis_locs.mean(axis=0)
760
+
761
+ norm = np.linalg.norm(avg_axis_dir)
762
+ if norm > 0:
763
+ avg_axis_dir = avg_axis_dir / norm
764
+
765
+ avg_center = np.round(avg_center, 6)
766
+ avg_axis_dir = np.round(avg_axis_dir, 6)
767
+ avg_axis_loc = np.round(avg_axis_loc, 6)
768
+
769
+ faces_info = [
770
+ {
771
+ "face_id": f.get("face_id"),
772
+ "center": [round(x, 6) for x in f["center"]],
773
+ "area": round(f["area"], 6),
774
+ "angle_span": round(f.get("angle_span", 0.0), 4),
775
+ "normal": [round(x, 6) for x in f.get("axis_direction", avg_axis_dir.tolist())],
776
+ }
777
+ for f in fillet_group
778
+ ]
779
+
780
+ fillet_type = fillet_group[0].get("type", "fillet")
781
+
782
+ return {
783
+ "name": f"FILLET_{group_index}",
784
+ "type": fillet_type,
785
+ "radius": round(avg_radius, 6),
786
+ "center": [float(x) for x in avg_center],
787
+ "normal": [float(x) for x in avg_axis_dir],
788
+ "axis_direction": [float(x) for x in avg_axis_dir],
789
+ "axis_location": [float(x) for x in avg_axis_loc],
790
+ "area": round(total_area, 6),
791
+ "num_faces": len(fillet_group),
792
+ "total_angle": round(float(total_angle), 4),
793
+ "faces": faces_info,
794
+ }
795
+
796
+
797
+
798
+ # def classify_fillets_by_size(fillets):
799
+ # """
800
+ # Classify fillets into categories based on radius for FEA mesh refinement.
801
+ # """
802
+ # classification = {
803
+ # "small": [], # R < 2mm - need very fine mesh
804
+ # "medium": [], # 2mm <= R < 5mm - need fine mesh
805
+ # "large": [] # R >= 5mm - standard mesh
806
+ # }
807
+
808
+ # for fillet in fillets:
809
+ # radius = fillet["radius"]
810
+ # if radius < 2.0:
811
+ # classification["small"].append(fillet)
812
+ # elif radius < 5.0:
813
+ # classification["medium"].append(fillet)
814
+ # else:
815
+ # classification["large"].append(fillet)
816
+
817
+ # print(f"\nFillet Classification:")
818
+ # print(f" Small fillets (R<2mm): {len(classification['small'])}")
819
+ # print(f" Medium fillets (2-5mm): {len(classification['medium'])}")
820
+ # print(f" Large fillets (R>5mm): {len(classification['large'])}")
821
+
822
+ # return classification
823
+
824
+ def detect_thickness(shape, sample_density=3, min_thickness=0.01, max_thickness=100.0):
825
+ from OCP.BRepClass3d import BRepClass3d_SolidClassifier
826
+ from OCP.gp import gp_Pnt
827
+ thickness_results = []
828
+ exp = TopExp_Explorer(shape, TopAbs_FACE)
829
+ classifier = BRepClass3d_SolidClassifier(shape)
830
+ while exp.More():
831
+ face_shape = exp.Current()
832
+ face = TopoDS.Face_s(face_shape)
833
+ adaptor = BRepAdaptor_Surface(face, True)
834
+ umin, umax = adaptor.FirstUParameter(), adaptor.LastUParameter()
835
+ vmin, vmax = adaptor.FirstVParameter(), adaptor.LastVParameter()
836
+ for i in range(sample_density):
837
+ for j in range(sample_density):
838
+ u = umin + (umax - umin) * i / (sample_density - 1)
839
+ v = vmin + (vmax - vmin) * j / (sample_density - 1)
840
+ pnt = adaptor.Value(u, v)
841
+
842
+ p = gp_Pnt()
843
+ du = gp_Vec()
844
+ dv = gp_Vec()
845
+ adaptor.D1(u, v, p, du, dv)
846
+
847
+ normal_vec = du.Crossed(dv)
848
+ normal_vec.Normalize()
849
+ if normal_vec.Magnitude() < 1e-8:
850
+ continue
851
+ normal_vec.Normalize()
852
+ thickness = shoot_ray_find_thickness(
853
+ shape, pnt, normal_vec, min_thickness, max_thickness
854
+ )
855
+ if thickness:
856
+ thickness_results.append({
857
+ "point": [pnt.X(), pnt.Y(), pnt.Z()],
858
+ "normal": [normal_vec.X(), normal_vec.Y(), normal_vec.Z()],
859
+ "thickness": thickness
860
+ })
861
+ exp.Next()
862
+ return thickness_results
863
+
864
+ def shoot_ray_find_thickness(shape, point, normal, min_thickness, max_thickness):
865
+ from OCP.gp import gp_Pnt, gp_Vec
866
+ from OCP.BRepClass3d import BRepClass3d_SolidClassifier
867
+
868
+ step = min_thickness / 2
869
+ max_dist = max_thickness
870
+ for dist in np.arange(step, max_dist, step):
871
+ test_point = gp_Pnt(
872
+ point.X() + normal.X() * dist,
873
+ point.Y() + normal.Y() * dist,
874
+ point.Z() + normal.Z() * dist,
875
+ )
876
+ classifier = BRepClass3d_SolidClassifier(shape)
877
+ classifier.Perform(test_point, 1e-7)
878
+ if classifier.State() != TopAbs_IN:
879
+ pos_thick = dist
880
+ break
881
+ else:
882
+ pos_thick = max_dist
883
+ for dist in np.arange(step, max_dist, step):
884
+ test_point = gp_Pnt(
885
+ point.X() - normal.X() * dist,
886
+ point.Y() - normal.Y() * dist,
887
+ point.Z() - normal.Z() * dist,
888
+ )
889
+ classifier = BRepClass3d_SolidClassifier(shape)
890
+ classifier.Perform(test_point, 1e-7)
891
+ if classifier.State() != TopAbs_IN:
892
+ neg_thick = dist
893
+ break
894
+ else:
895
+ neg_thick = max_dist
896
+ return pos_thick + neg_thick
897
+
898
+ def mode_thickness(shape, decimal=2, threshold=0.1):
899
+ thickness_results = detect_thickness(shape, sample_density=3, min_thickness=0.01, max_thickness=100.0)
900
+ vals = [round(float(r['thickness']), decimal) for r in thickness_results if float(r['thickness']) >= threshold]
901
+ counter = Counter(vals)
902
+ mode_val, count = counter.most_common(1)[0]
903
+ return mode_val
904
+
905
+ def detect_all_edges(shape):
906
+ """
907
+ Detect all UNIQUE edges shared by two faces.
908
+ Stores: name, start point, end point, angle, normals, type.
909
+ """
910
+
911
+ # 1. Collect all faces
912
+ faces = []
913
+ face_exp = TopExp_Explorer(shape, TopAbs_FACE)
914
+ while face_exp.More():
915
+ faces.append(TopoDS.Face_s(face_exp.Current()))
916
+ face_exp.Next()
917
+
918
+ results = []
919
+ seen = set() # edge deduplication via TShape()
920
+ idx = 0 # MUST initialize once
921
+
922
+ # 2. Iterate edges
923
+ edge_exp = TopExp_Explorer(shape, TopAbs_EDGE)
924
+ while edge_exp.More():
925
+ edge = TopoDS.Edge_s(edge_exp.Current())
926
+
927
+ # --- dedup by underlying TShape ---
928
+ tid = hash(edge.TShape())
929
+ if tid in seen:
930
+ edge_exp.Next()
931
+ continue
932
+ seen.add(tid)
933
+
934
+ # --- find adjacent faces ---
935
+ adjacent_faces = []
936
+ for face in faces:
937
+ ex2 = TopExp_Explorer(face, TopAbs_EDGE)
938
+ while ex2.More():
939
+ if ex2.Current().IsSame(edge):
940
+ adjacent_faces.append(face)
941
+ break
942
+ ex2.Next()
943
+
944
+ if len(adjacent_faces) != 2:
945
+ edge_exp.Next()
946
+ continue
947
+
948
+ # --- compute normals & dihedral angle ---
949
+ normals = []
950
+ for face in adjacent_faces:
951
+ adaptor = BRepAdaptor_Surface(face, True)
952
+ u = 0.5 * (adaptor.FirstUParameter() + adaptor.LastUParameter())
953
+ v = 0.5 * (adaptor.FirstVParameter() + adaptor.LastVParameter())
954
+ du = gp_Vec()
955
+ dv = gp_Vec()
956
+ temp_p = gp_Pnt()
957
+ adaptor.D1(u, v, temp_p, du, dv)
958
+ n = du.Crossed(dv)
959
+ if n.Magnitude() > 1e-6:
960
+ n.Normalize()
961
+ normals.append(n)
962
+ else:
963
+ normals.append(None)
964
+
965
+ if None in normals:
966
+ edge_exp.Next()
967
+ continue
968
+
969
+ # angle
970
+ import numpy as np
971
+ dot = np.clip(normals[0].Dot(normals[1]), -1.0, 1.0)
972
+ angle_deg = float(np.degrees(np.arccos(dot)))
973
+
974
+ # skip perfectly smooth edges
975
+ if abs(angle_deg - 180.0) < 1e-3:
976
+ edge_exp.Next()
977
+ continue
978
+
979
+ # --- get start / end points ---
980
+ verts = []
981
+ vexp = TopExp_Explorer(edge, TopAbs_VERTEX)
982
+ while vexp.More():
983
+ v = TopoDS.Vertex_s(vexp.Current())
984
+ p = BRep_Tool.Pnt_s(v)
985
+ verts.append([p.X(), p.Y(), p.Z()])
986
+ vexp.Next()
987
+
988
+ if len(verts) < 2:
989
+ edge_exp.Next()
990
+ continue
991
+
992
+ start = verts[0]
993
+ end = verts[-1]
994
+
995
+ # --- classify edge type ---
996
+ curve_adaptor = BRepAdaptor_Curve(edge)
997
+ from OCP.GeomAbs import GeomAbs_Line, GeomAbs_Circle
998
+ t = curve_adaptor.GetType()
999
+
1000
+ if t == GeomAbs_Line:
1001
+ e_type = "line"
1002
+ elif t == GeomAbs_Circle:
1003
+ e_type = "circle"
1004
+ else:
1005
+ e_type = "other"
1006
+
1007
+ # --- store result ---
1008
+ results.append({
1009
+ "name": f"EDGE_{idx}",
1010
+ "start": start,
1011
+ "end": end,
1012
+ "angle": angle_deg,
1013
+ "normals": [
1014
+ [normals[0].X(), normals[0].Y(), normals[0].Z()],
1015
+ [normals[1].X(), normals[1].Y(), normals[1].Z()]
1016
+ ],
1017
+ "edge_type": e_type
1018
+ })
1019
+
1020
+ idx += 1 # safe!
1021
+
1022
+ edge_exp.Next()
1023
+
1024
+ return results
1025
+
1026
+
1027
+
1028
+
1029
+ def detect_all_faces(shape):
1030
+ faces = []
1031
+ face_exp = TopExp_Explorer(shape, TopAbs_FACE)
1032
+ face_id = 0
1033
+
1034
+ while face_exp.More():
1035
+ face_shape = face_exp.Current()
1036
+ face = TopoDS.Face_s(face_shape)
1037
+
1038
+ adaptor = BRepAdaptor_Surface(face, True)
1039
+
1040
+ # sample param
1041
+ umin, umax = adaptor.FirstUParameter(), adaptor.LastUParameter()
1042
+ vmin, vmax = adaptor.FirstVParameter(), adaptor.LastVParameter()
1043
+ u = (umin + umax) * 0.5
1044
+ v = (vmin + vmax) * 0.5
1045
+
1046
+ # center
1047
+
1048
+
1049
+ # normal
1050
+ du = gp_Vec()
1051
+ dv = gp_Vec()
1052
+ temp_p = gp_Pnt()
1053
+ adaptor.D1(u, v, temp_p, du, dv)
1054
+ normal = du.Crossed(dv)
1055
+ if normal.Magnitude() > 1e-8:
1056
+ normal.Normalize()
1057
+
1058
+ # --- NEW: compute area ---
1059
+ gprops = GProp_GProps()
1060
+ BRepGProp.SurfaceProperties_s(face, gprops)
1061
+ area = gprops.Mass()
1062
+ center = face_bbox_center(face)
1063
+
1064
+ faces.append({
1065
+ "name": f"FACE_{face_id}",
1066
+ "face_id": face_id,
1067
+ "center": center,
1068
+ "normal": [normal.X(), normal.Y(), normal.Z()],
1069
+ "area": float(area) # ← added
1070
+ })
1071
+
1072
+ face_id += 1
1073
+ face_exp.Next()
1074
+
1075
+ return faces