Python - parts missing after booleans

Hi,
I am trying to create python script for netting, where I could change parameters parametrically. This net is in the “frame”. The script nearly works, but only for certain parameters. e.g. if I changed angle some fibers went missing, or the cuts don’t work properly. The bug is in the section, where booleans (probably Intersect, and after then union) are applied.
We want to use the model for CFD simulation, that’s why we want to have it parametrically.

I know it is quite a large script. However did anybody come across this problem? And did you solve it?

Blender v2.82.7, Python 3.7.4

Alpha_top = [0]
Alpha_bottom = [90]

For these settings it works properly:
Alpha_top = [1]
Alpha_bottom = [45]

import bpy
import os
import math
from math import pi
from shutil import copyfile

bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()

bpy.context.scene.unit_settings.length_unit = 'MILLIMETERS'
# -------------DISTRIBUTOR PARAMETERS---------------------------------
# distributor
l_distr = 210  # mm
w_distr = 60  # mm
h_distr = 0.8  # mm

# hole
r_hole = 3  # mm
n_hole = 4  # 1
rel_offset_hole = 2  # 1=sticking to neighbours, 2= hole diameter inbetween
y_offset_hole = 1  # mm the first hole translation in y direction (+=out,-=in)

# channels
w_channel = 2  # mm

hole_in = 2  # connection hole order
y_offset_in = 1  # mm

hole_out = 4  # connection hole order
y_offset_out = -5  # mm
# -------------SPACER PARAMETERS---------------------------------

# Spacer size -
l = 160  # mm
w = 40  # mm
h = 0.8  # mm

# string shape
Anvert = [10]  # 1...............  number of verices in circle(3-256)

# string coincidence
Aspacing = [4]  # mm ............distance beetwen centers of strings
Aoverlap = [0.2]  # 1..............string overlap ratio (1=full diameter 0=lying on top)

Aalpha_top = [1]  # deg ...........angle between x axis and top-string    (-90...90)
Aalpha_bottom = [45]  # deg ...........angle between x axis and bottom-string (-90...90)

x_offset = 1 # mm ............translation of strings layer according x-axis
y_offset = 0  # mm ............translation of strings layer according y-axis

cut = 3  # 1...BETA............ (0=no cut, 1=cut by yz plane, 2=cut to NET size) BETA - IF doesnt work change offset,spacing or turn it off :)

# -----------STL export------------------------------------
separate = True
quality = 0  # 1...BETA...........Refinement level (Negative=turn-off,0=smooth,1=smooth+refinement1)
path = bpy.path.abspath('//stlexport/')

# --------------------------------------------------------
# --------------------MAIN LOOP---------------------------
# --------------------------------------------------------
y_offset =y_offset -20

for nvert in Anvert:
    for spacing in Aspacing:
        for overlap in Aoverlap:
            for alpha_top in Aalpha_top:
                for alpha_bottom in Aalpha_bottom:
                    bpy.ops.object.select_all(action='SELECT')
                    bpy.ops.object.delete()
                    # -----------CALCULATED_PARAMETERS------------------------------------

                    # string radius
                    r = 1 / 2 * h / (2 - overlap)
                    # RASTR size
                    xx = 2.2 * l
                    yy = 2.2 * l

                    y_hole = w / 2 - r_hole + y_offset_hole
                    x_hole = -(l_distr - l) / 4

                    alpha_in = (90 * pi / 180) + math.atan(
                        -x_hole / (y_hole - y_offset_in - (hole_in - 1) * 2 * r_hole * rel_offset_hole))
                    alpha_out = (90 * pi / 180) + math.atan(
                        x_hole / (y_hole - y_offset_out - (hole_out - 1) * 2 * r_hole * rel_offset_hole))
                    l_in = 2 * (x_hole / math.cos(alpha_in))
                    l_out = 2 * (x_hole / math.cos(alpha_out))

                    # ------------------- CUT-OFF--------------------------------------------
                    obj_names = ["Cube", "Channel_in", "Channel_out", "Hole_in", "Hole_out"]
                    if cut == 1:
                        bpy.ops.mesh.primitive_cube_add(size=1, location=(xx / 5, 0, 0))
                        bpy.context.object.scale[0] = xx / 2.5
                        bpy.context.object.scale[1] = yy
                        bpy.context.object.scale[2] = 3 * h

                    elif cut >= 2:
                        bpy.ops.mesh.primitive_cube_add(size=1, location=(l / 2, 0, 0))
                        bpy.context.active_object.name = obj_names[0]
                        bpy.context.object.scale[0] = l
                        bpy.context.object.scale[1] = w
                        bpy.context.object.scale[2] = 3 * h

                        # ------------------- DISTRIBUTOR --------------------------------------------
                        if cut == 3:

                            bpy.ops.mesh.primitive_cube_add(size=1, location=(0, y_offset_in, 0))
                            bpy.context.active_object.name = obj_names[1]  # "Channel_in"
                            bpy.context.object.scale[0] = l_in
                            bpy.context.object.scale[1] = w_channel
                            bpy.context.object.scale[2] = 3 * h

                            bpy.context.object.rotation_euler[2] = alpha_in

                            bpy.ops.mesh.primitive_cube_add(size=1, location=(l, y_offset_out, 0))
                            bpy.context.active_object.name = obj_names[2]  # "Channel_out"

                            bpy.context.object.scale[0] = l_out
                            bpy.context.object.scale[1] = w_channel
                            bpy.context.object.scale[2] = 3 * h

                            bpy.context.object.rotation_euler[2] = alpha_out

                            bpy.ops.mesh.primitive_cylinder_add(vertices=64, radius=r_hole, depth=(3 * h),
                                                                location=(x_hole, y_hole, 0), rotation=(0, 0, 0))
                            bpy.context.active_object.name = obj_names[3]  # "Hole_in"
                            bpy.ops.object.modifier_add(type='ARRAY')
                            bpy.context.object.modifiers["Array"].count = n_hole
                            bpy.context.object.modifiers["Array"].fit_type = 'FIXED_COUNT'
                            bpy.context.object.modifiers["Array"].relative_offset_displace[0] = 0
                            bpy.context.object.modifiers["Array"].relative_offset_displace[1] = -rel_offset_hole
                            bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Array")

                            bpy.ops.mesh.primitive_cylinder_add(vertices=64, radius=r_hole, depth=(3 * h),
                                                                location=(x_hole + (l + l_distr) / 2, y_hole, 0),
                                                                rotation=(0, 0, 0))
                            bpy.context.active_object.name = obj_names[4]  # "Hole_out"
                            bpy.ops.object.modifier_add(type='ARRAY')
                            bpy.context.object.modifiers["Array"].count = n_hole
                            bpy.context.object.modifiers["Array"].fit_type = 'FIXED_COUNT'
                            bpy.context.object.modifiers["Array"].relative_offset_displace[0] = 0
                            bpy.context.object.modifiers["Array"].relative_offset_displace[1] = -rel_offset_hole
                            bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Array")

                            bpy.ops.mesh.primitive_cube_add(size=1, location=(l / 2, 0, h_distr / 2))
                            bpy.context.active_object.name = "Distributor"

                            bpy.context.object.scale[0] = l_distr
                            bpy.context.object.scale[1] = w_distr
                            bpy.context.object.scale[2] = h_distr

                            for obj_name in obj_names:
                                bpy.ops.object.modifier_add(type='BOOLEAN')
                                bpy.context.object.modifiers["Boolean"].object = bpy.data.objects[obj_name]
                                bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Boolean")

                            bpy.ops.object.select_all(action='DESELECT')

                            for i in range(1, len(obj_names)):
                                bpy.data.objects[obj_names[i]].select_set(True)
                                bpy.ops.object.delete()

                    # ------------------- Top ARRAY--------------------------------------------
                    bpy.ops.mesh.primitive_cylinder_add(vertices=nvert, radius=r, depth=xx,
                                                        location=(x_offset, y_offset, (r + 2 * r * (1 - overlap))),
                                                        rotation=((90 * pi / 180), 0, ((90 - alpha_top) * pi / 180)))
                    bpy.context.active_object.name = "Top"
                    obj = bpy.context.object
                    bpy.ops.object.modifier_add(type='SUBSURF')
                    bpy.context.object.modifiers["Subdivision"].render_levels = 4
                    bpy.context.object.modifiers["Subdivision"].levels = 4
                    bpy.context.object.modifiers["Subdivision"].subdivision_type = 'SIMPLE'
                    bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Subdivision")
                    bpy.ops.object.modifier_add(type='ARRAY')
                    bpy.context.object.modifiers["Array"].name = "Array.001"
                    bpy.context.object.modifiers["Array.001"].use_constant_offset = True
                    bpy.context.object.modifiers["Array.001"].constant_offset_displace[0] = spacing
                    bpy.context.object.modifiers["Array.001"].fit_type = 'FIT_LENGTH'
                    bpy.context.object.modifiers["Array.001"].fit_length = yy / 2
                    bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Array.001")

                    if cut >= 1:
                        bpy.ops.object.modifier_add(type='BOOLEAN')
                        obj.modifiers["Boolean"].operation = 'INTERSECT'
                        obj.modifiers["Boolean"].double_threshold = 0
                        obj.modifiers["Boolean"].object = bpy.data.objects["Cube"]
                        bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Boolean")

                    #

                    # ------------------- Bottom ARRAY--------------------------------------------
                    bpy.ops.mesh.primitive_cylinder_add(vertices=nvert, radius=r, depth=yy,
                                                        location=(x_offset, y_offset, r),
                                                        rotation=((90 * pi / 180), 0, ((90 - alpha_bottom) * pi / 180)))
                    bpy.context.active_object.name = "Bottom"
                    bpy.ops.object.modifier_add(type='SUBSURF')
                    bpy.context.object.modifiers["Subdivision"].render_levels = 4
                    bpy.context.object.modifiers["Subdivision"].levels = 4
                    bpy.context.object.modifiers["Subdivision"].subdivision_type = 'SIMPLE'
                    bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Subdivision")
                    bpy.ops.object.modifier_add(type='ARRAY')
                    bpy.context.object.modifiers["Array"].name = "Array.002"
                    bpy.context.object.modifiers["Array.002"].use_constant_offset = True
                    bpy.context.object.modifiers["Array.002"].constant_offset_displace[0] = spacing
                    bpy.context.object.modifiers["Array.002"].fit_type = 'FIT_LENGTH'
                    bpy.context.object.modifiers["Array.002"].fit_length = xx / 2

                    bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Array.002")

                    if cut >= 1:
                        bpy.ops.object.modifier_add(type='BOOLEAN')
                        bpy.context.object.modifiers["Boolean"].operation = 'INTERSECT'
                        bpy.context.object.modifiers["Boolean"].double_threshold = 0
                        bpy.context.object.modifiers["Boolean"].object = bpy.data.objects["Cube"]
                        bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Boolean")

                    bpy.ops.object.select_all(action='DESELECT')

                    if cut >= 1:
                        bpy.data.objects['Cube'].select_set(True)
                        bpy.ops.object.delete()

                    bpy.ops.object.select_all(action='DESELECT')

                    # -----------------UNION OF PARTS-----------------------------------------------
                    if cut >= 3:
                        Top = bpy.context.scene.objects["Top"]
                        bpy.context.view_layer.objects.active = Top
                        Top.select_set(True)

                        bpy.ops.object.modifier_add(type='BOOLEAN')
                        bpy.context.object.modifiers["Boolean"].operation = 'UNION'
                        bpy.context.object.modifiers["Boolean"].double_threshold = 0
                        bpy.context.object.modifiers["Boolean"].object = bpy.data.objects["Bottom"]
                        bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Boolean")

                        bpy.ops.object.modifier_add(type='BOOLEAN')
                        bpy.context.object.modifiers["Boolean"].operation = 'UNION'
                        bpy.context.object.modifiers["Boolean"].double_threshold = 0
                        bpy.context.object.modifiers["Boolean"].object = bpy.data.objects["Distributor"]
                        bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Boolean")

                        bpy.data.objects.remove(bpy.data.objects["Bottom"], do_unlink=True)
                        bpy.data.objects.remove(bpy.data.objects["Distributor"], do_unlink=True)
                        
                    bpy.ops.object.mode_set(mode='EDIT')
                    bpy.ops.mesh.remove_doubles(threshold=0.05)
                    bpy.ops.object.mode_set(mode='OBJECT')

Blender’s current Boolean is not intended to work properly if the objects involved just touch each other rather than really interpenetrate each other. Or if edges in them exactly intersect other edges. It sometimes works accidentally, but you can’t rely on that. If you can, can you make the string diameters slightly different between strings that will intersect each other? And/or have their centers be in slightly different planes? Also, make sure the string ends actually enter inside the frames on each side. (I didn’t read you script carefully so don’t know if you already do that or not).

I am working on a better Boolean that will not have these problems, but am probably some months away from it being in Blender.

Thank you very much for your answer. We hoped, that we could use Blender for this type of modeling. However, now I see, we will have to use another program.

as an alternative- you could use Blender 2.79, the carve boolean method handled these situations without any issues.

Thank you for your answer. I will try it too.