# Benchtop model (3-matic 14.0)

In this scripting tutorial we will be creating a benchtop model similiar to Design Exercise 4 in the 3-matic tutorial.

``````import math
import os
import sys

import trimatic

def reset_view():
# set a view to follow progress & see result
# values obtained by using trimatic.get_view()
global_view_dir = [0.31, 0.72, -0.62]
global_view_up = [0.84, -0.51, -0.17]
trimatic.view_custom(view_vector=global_view_dir, up_vector=global_view_up)

def magnitude(v):
# length of a vector
return math.sqrt(sum(v[i] * v[i] for i in range(len(v))))

def distance(p1, p2):
# distance between 2 points
v = [p2[i] - p1[i] for i in range(3)]
return magnitude(v)

def normalize(v):
# returns normalized vector
vmag = magnitude(v)
return [v[i] / vmag for i in range(len(v))]

def cut_and_sort_by_volume(the_part, cutting_plane):
# Cuts a part with a plane. Returns pieces ordered by volume (largest first)
cut_parts = trimatic.cut(cutting_entity=cutting_plane, entities=the_part)
# sort parts by volume
cut_parts = sorted(cut_parts, key=lambda k: k.volume, reverse=True)
return cut_parts

def create_cut_plane(branch, branch_direction, distance_from_end):
#creates_cut
# since compute_extrema_analysis_points is not directly available for curves, we copy the curve to a seperate part first
tmp_part = trimatic.copy_to_part(branch)
extremal = trimatic.compute_extrema_analysis_points(direction=branch_direction, entities=tmp_part,
global_extrema_only=True, maxima=False, minima=True)
trimatic.delete(tmp_part)
assert (len(extremal) is 1)
end_point = extremal[0]
# from the endpoint, travel a distance over the curve
# we should have gotten first or last point of the curve
branch_points = branch.points
first_point = branch_points[0]
last_point = branch_points[len(branch_points) - 1]
if (distance(first_point, end_point) > distance(last_point, end_point)):
branch_points = list(reversed(list(branch_points)))

size = len(branch_points)
accum_distance = 0
prev_distance = 0
selected_point = [branch_points[0][0], branch_points[0][0], branch_points[0][0]]
tangent_at_selected_point = [0, 0, 0]
for i in range(len(branch_points)):
next_i = (i + 1 + size) % size
current_point = branch_points[i]
next_point = branch_points[next_i]
prev_distance = accum_distance
accum_distance += distance(current_point, next_point)
if accum_distance > distance_from_end and next_i > 0:
factor = (distance_from_end - prev_distance) / distance_btw_adjacant_points
for j in range(3):
selected_point[j] = current_point[j] + (next_point[j] - current_point[j]) * factor
tangent_at_selected_point[j] = (next_point[j] - current_point[j]) / distance_btw_adjacant_points
break
return trimatic.create_plane_normal_origin(normal=tangent_at_selected_point, origin=selected_point)

def create_flange(flange_surface):
# in contrast to the manual workflow, we don't create entities directly in sketch
# sketch API is limited to importing & exporting
# no entites can be created directly inside the sketch
# therefore, we create all entities in 3D and import them into the sketch to perform extrude on the sketch

# keep track of temporary objects
tmp_objects = []
to_extrude = []
work_plane = trimatic.create_plane_fit(flange_surface)
tmp_objects.append(work_plane)
extrude_direction = work_plane.object_coordinate_system.z_axis
origin = work_plane.object_coordinate_system.origin
# find inner contour: inner contour is the shortest one
# we can't get length from contour directly: convert to curve first
contours = flange_surface.get_border().get_contours()
assert (len(contours) is 2)
tmp_part = trimatic.convert_to_curve(contours)
tmp_objects.append(tmp_part)
contours_as_curves = tmp_part.get_curves()
assert (len(contours_as_curves) is 2)
if contours_as_curves[0].length < contours_as_curves[1].length:
inner_contour = contours[0]
else:
inner_contour = contours[1]

to_extrude.append(inner_contour)

# create 3D circles
# outercircle
normal=extrude_direction)
tmp_objects.append(outer_circle)
tmp_part = trimatic.convert_to_curve(outer_circle)
tmp_objects.append(tmp_part)

to_extrude = to_extrude + list(tmp_part.get_curves())
# innercircle
normal=extrude_direction)
tmp_objects.append(inner_circle)

# to get to a starting point for patterning, convert inner circle to curve
tmp_part = trimatic.convert_to_curve(inner_circle)
tmp_objects.append(tmp_part)
inner_circle_curve = tmp_part.get_curves()[0]
# create the 1st pattern point
normal=extrude_direction)
tmp_objects.append(hole_circle)

# circular pattern with rotate
rotated_circles = trimatic.rotate(entities=hole_circle, angle_deg=45, axis_direction=extrude_direction,
axis_origin=origin, number_of_copies=8)
to_extrude = to_extrude + list(rotated_circles)

# extrude the flange
# not all entities can be extruded directly (e.g. trimatic.Arc)
# therefore, project into sketch and extrude the sketch
sketch = trimatic.create_sketch(planes=work_plane)
tmp_objects.append(sketch)
trimatic.import_projection(entities=to_extrude, sketch=sketch)
thickness_of_flange = 2.0
flange = trimatic.extrude(entities=sketch, depth1=thickness_of_flange, direction=extrude_direction,
solid=True)
trimatic.delete(tmp_objects)
flange.name = "Flange"
flange.color = (0.2, 0.9, 0.2)

return flange

def create_outlet_attachments(outlet_surfaces):
tmp_objects = []
outlet_attachments = []
i = 1
for outlet_surface in outlet_surfaces:
# find longest contour by converting to curve
tmp_part = trimatic.convert_to_curve(outlet_surface.get_border())
tmp_objects.append(tmp_part)
# sort by length
sorted_curves = sorted(tmp_part.get_curves(), key=lambda k: k.length, reverse=True)
longest_curve = sorted_curves[0]
# compute tangent to the curve
point_1 = longest_curve.points[0]
point_2 = longest_curve.points[1]

curve_tangent = [point_2[0] - point_1[0], point_2[1] - point_1[1], point_2[2] - point_1[2]]
# normalize
curve_tangent = normalize(curve_tangent)
# temporary plane to create sketch
sweep_plane = trimatic.create_plane_normal_origin(normal=curve_tangent, origin=point_1)
tmp_objects.append(sweep_plane)
# create sketch
sweep_sketch = trimatic.create_sketch(planes=sweep_plane)
tmp_objects.append(sweep_sketch)
# create circle to sweep (in 3D because we can't create entities in the sketch directly)
tmp_objects.append(circle_to_sweep)
# project circle in the sketch
trimatic.import_projection(entities=circle_to_sweep, sketch=sweep_sketch)
outlet = trimatic.sweep(path=longest_curve, profile=sweep_sketch)
outlet.name = "Outlet " + str(i)
outlet.color = (0.1, 0.1, 0.8)
i += 1
outlet_attachments.append(outlet)

trimatic.delete(tmp_objects)
return outlet_attachments

cog = trimatic.compute_center_of_gravity(ref_part)
tmp_objects = []
# supports will be centered at the projection of the centerline
# project the centerline in a sketch
bottom_base_sketch = trimatic.create_sketch(planes=ref_plane)
tmp_objects.append(bottom_base_sketch)
tmp_sketch = trimatic.duplicate(bottom_base_sketch)
tmp_objects.append(tmp_sketch)
trimatic.import_projection(entities=ref_curves, sketch=tmp_sketch)
# convert the projected centerline to curves
part_with_curves = trimatic.sketch_to_curves(sketch=tmp_sketch)
tmp_objects.append(part_with_curves)
centerline_flat = part_with_curves.get_curves()
# distance between supports
target_distance_between_supports = 50
circles_centers = []
#sort curves by length, longest first (make outcome deterministic)
centerline_flat = sorted(centerline_flat, key=lambda k: k.length, reverse=True)
# for every curve, place support every 50 mm
for curve in centerline_flat:
curve_points = curve.points
#orient curve such that we move from outside to center (make outcome deterministic)
if distance(curve_points[0],cog) < distance(curve_points[len(curve.points)-1],cog):
curve_points = list(reversed(list(curve_points)))

distance_travelled = 0
for i in range(len(curve_points) - 1):
point = curve_points[i]
next_point = curve_points[i + 1]
d = distance(point, next_point)
distance_travelled += d
if distance_travelled >= target_distance_between_supports:
distance_travelled = 0
circles_centers.append(next_point)
# We have too many centers because the part of the centerline of the trimmed off parts is still present
# remove points that don't project on the part
filtered_points = []
for point in circles_centers:
try:
prj = trimatic.project_point(point_to_project=point, direction=ref_plane.z_axis, parts=ref_part)
filtered_points.append(point)
except:
pass
circles_centers = filtered_points
circles = []
#create the circles
for cc in circles_centers:
circles.append(

#project the circles in the sketch
trimatic.import_projection(bottom_base_sketch, circles)
#also project the centerline: for vizualization only => construction=True
trimatic.import_projection(entities=ref_curves, sketch=bottom_base_sketch, construction=True)
#extrude the sketch
supports = trimatic.extrude(entities=bottom_base_sketch, upto_entities1=ref_part,
direction=bottom_base_sketch.object_coordinate_system.z_axis)
#delete temporary objects
trimatic.delete(tmp_objects)
return [supports, circles_centers]

def create_base(ref_object, ref_plane):
#creates base plate
corner = list(ref_object.dimension_min)
base_thickness = 6
corner[1] = ref_plane.origin[1] - base_thickness
del_point = ref_object.dimension_delta
box = trimatic.create_box_part(corner, x_extent=del_point[0], y_extent=base_thickness, z_extent=del_point[2])
return box

def main():
# user prepared file:
# similar to manual workflow, unwanted featured were trimmed
# centerline branches corresponding to unwanted features were deleted
# inlet and outlet branches from the centerline were named Big, Small1 and Small2
path = os.path.dirname(trimatic.get_application_path()) + r"\DemoFiles\AAA_trimmed.mxp"
# open file
trimatic.open_project(path)

reset_view()

# find data prepared by user
the_part = trimatic.find_part(name="Smoothed_Wrapped_AAA")
centerline_part = trimatic.find_part("Centerline  1")
branch_big = centerline_part.find_curve('Big')
small_branches = centerline_part.find_curves('Small.*')
centerline_part.visible = False
init_color = the_part.color

# STEP1: #hollow
trimatic.hollow_both(distance=1, entities=the_part, reduce=False, smallest_detail=1)

# STEP2: cut off end parts

# The direction of the analysis is taken along z axis because of the default position of the AAA model when it was imported from Mimics.
# Do note that this direction can change based on different models and orientations.

# for every branch, find a cut plane
# small branches
# to construct datumplanes to do the cut, we will use point 10 mm from the extremal points of the branches
cut_planes = []
for branch in small_branches:
cut_planes.append(create_cut_plane(branch, (0, 0, 1), 10))
# we cut off a bit more from the big branch
cut_planes.append(create_cut_plane(branch_big, (0, 0, -1), 30))

connection_surfaces = []
tmp_objects = []
for cutting_plane in cut_planes:
# to avoid the (infinite) plane to cut too much, we convert the analytical plane to a not infinite part
# first increase size of datumplane from default 10
cutting_plane.delta_x = 20
cutting_plane.delta_y = 20
cutting_object = trimatic.convert_analytical_primitive_to_part(cutting_plane)
# keep the part with largest volume
cut_parts = cut_and_sort_by_volume(the_part, cutting_object)
the_part = cut_parts[0]
# form the part that was cut off, create sketch with outline
surface_with_smallest_area = sorted(cut_parts[1].get_surfaces(), key=lambda k: k.area)[0]
connection_surfaces.append(surface_with_smallest_area)
trimatic.delete([cutting_object, cutting_plane])
cut_parts[1].visible = False
tmp_objects.append(cut_parts[1])

# STEP 5: Create flange on the big branch

flange = create_flange(connection_surfaces[2])

# STEP 5b: Create attachments to connect flexible hosing on both of the femoral artery outlets.

outlet_attachments = create_outlet_attachments(connection_surfaces[0:2])

# Step 6: Create supporting cylinders
# base plane: 65mm below cog of part
org = list(trimatic.compute_center_of_gravity(the_part))
org[1] += 65
translation_plane = trimatic.create_plane_normal_origin(normal=[0, -1, 0], origin=org)

[supports, support_centers] = create_supports(translation_plane, centerline_part.get_curves(), the_part,

#create base
base = create_base(the_part, translation_plane)
#filletting & labeling will be done on the top of the base plate
base_plate_top_y = base.dimension_min[1]

# Step 7 : union
#reduce the size of the trangles to avoid troubles with boolean & fillet afterwards
#subdivsion for the base plate
trimatic.subdivide(entities = base , number_of_iterations=5)
trimatic.improve_mesh(entities = supports, shape_quality_high = True, maximum_geometrical_error = 0.05)

#Translate the supports 1 mm up towards the AAA model. This is to ensure some intersection between the “AAA” model, the “supports”, and the “base” to mitigate overlapping surfaces and aid the subsequent Boolean Union operation.
trimatic.translate(entities = supports, translation_vector = (0,-1,0))

the_part = trimatic.boolean_union([supports, base, the_part])

fillet_surface = the_part.find_surface('Bottom')

# Step : create quick label
labelpos = list(the_part.dimension_min)
labelpos[0] += 10
labelpos[1] = base_plate_top_y
labelpos[2] = the_part.dimension_max[2] - 10
trimatic.quick_label(entity=the_part, text="Made in 3-matic", point=labelpos, direction=[0, 0, -1], bold=True,
follow_surface=False, font_height=the_part.dimension_delta[0] / 10, label_height=2)

# clean up
trimatic.delete(tmp_objects)

#naming & colors

the_part.name = "Benchtop Model and Supports"
the_part.color = init_color

reset_view()