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:
			distance_btw_adjacant_points = (accum_distance - prev_distance)
			if distance_btw_adjacant_points > 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
	outer_circle = trimatic.create_circle_arc_normal_center_radius(center_point=origin, radius=25,
																   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
	inner_radius = 20
	inner_circle = trimatic.create_circle_arc_normal_center_radius(center_point=origin, radius=inner_radius,
																   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
	hole_circle = trimatic.create_circle_arc_normal_center_radius(center_point=inner_circle_curve.points[0], radius=2,
																  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)
		circle_to_sweep = trimatic.create_circle_arc_normal_center_radius(center_point=point_1, normal=curve_tangent,
																		  radius=2)
		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

def create_supports(ref_plane, ref_curves, ref_part, support_radius):
	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(
			trimatic.create_circle_arc_normal_center_radius(normal=bottom_base_sketch.object_coordinate_system.z_axis,
															center_point=cc, radius=support_radius))

	#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 fillet(support_radius, support_points, surface, fillet_plane_y):
	trimatic.fillet(surface.get_border(), radius=2)

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)

	support_radius = 2

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

	#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')
	fillet(support_radius, support_centers, fillet_surface, base_plate_top_y)

	# 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()
	# adjust zoom
	trimatic.zoom(the_part)


main()