Note: This is a big ol’ WIP and I’m currently cleaning it up and adding more features. Going to release it as a git repo to work as a blender addon. (Also going to break up the file, so its not absolutely illegible.)
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
####TO DO###########
#Fix protein input
#Adjust hybridization and protein import data to more correctly align with dna
#Update dna.blend to be unique
bl_info = {
"name": "DNA Segmentator",
"author": "Ryan Mulqueen <RMulqueen@mdanderson.org>",
"version": (0, 4),
"blender": (3, 2, 2),
"location": "View3D",
"description": "Adds a menu to build DNA molecules",
"warning": "",
"wiki_url": "",
"category": "Mesh",
}
import bpy
import os, sys
from mathutils import Vector
from math import pi
def setup_initial_curve(
curve_name="gDNA_path",
collection_name="gDNA"):
curve=bpy.ops.curve.primitive_bezier_curve_add(
radius=1,
align='WORLD',
location=(0, 0, 0),
scale=(1, 1, 1),
enter_editmode=True)
bpy.context.active_object.name = curve_name
#this is to set handles straight so there isn't a curve
bpy.ops.transform.resize(value=(1, 0, 1),
orient_type='GLOBAL',
orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)),
orient_matrix_type='GLOBAL') #set all handles to 0 so the curve is straight
bpy.ops.object.mode_set(mode='OBJECT')#ensure we are back in object mode
scene = bpy.context.scene
if collection_name not in bpy.data.collections: #add collection if name isn't in collections yet
bpy.data.collections.new(name=collection_name) #make new collection
if bpy.data.collections[collection_name] not in list(scene.collection.children): #link collection to scene if it isn't in yet
scene.collection.children.link(bpy.data.collections[collection_name]) #link new collection to scene
obj=bpy.context.active_object
if obj not in list(bpy.data.collections[collection_name].objects): #add collection if name isn't in collections yet
bpy.data.collections[collection_name].objects.link(obj) #link curve to new collection
if obj in list(bpy.context.scene.collection.objects):
bpy.context.scene.collection.objects.unlink(obj) #remove link from scene collection
if obj in list(bpy.data.collections["Collection"].objects):
bpy.data.collections["Collection"].objects.unlink(obj) #remove link from default collection
def load_in_structure(
file_path="", #change this to the directory for dna.blend
import_object_name="DNA_Surface_real",
collection_name="gDNA",
object_name="gDNA",
curve_name="gDNA_path",
dna_length=100,
dna_color=(0.3, 0.3, 0.8, 1)):
dna_count=int(dna_length/11)#set up count of 11bp turns to use in DNA
inner_path="Object"
file="dna.blend"
#in case the imported file name overlaps with existing data using an old and new set of names for omparison
old_set = set(bpy.data.objects[:])
bpy.ops.wm.append(
filepath=os.path.join(file_path,inner_path, import_object_name),
directory=os.path.join(file_path,inner_path),
filename=import_object_name
)
bpy.ops.object.select_all(action='DESELECT')
new_set = set(bpy.data.objects[:]) - old_set
#Set up modifiers to make generative DNA
#first set up array to duplicate DNA enough to fit the curve
obj=list(new_set)[0]
obj.name= object_name #change name of object we just made
bpy.context.view_layer.objects.active = obj
bpy.ops.object.modifier_add(type='ARRAY')#Add array modifier to dna and assign to path
bpy.context.object.modifiers["Array"].fit_type ="FIXED_COUNT" #fit array of objects to curve
bpy.context.object.modifiers["Array"].count = dna_count
obj.modifiers["Array"].curve = bpy.data.objects[curve_name] #assign the curve as the one we created
obj.modifiers["Array"].relative_offset_displace[0] = 0 #don't go by x value
obj.modifiers["Array"].relative_offset_displace[2] = 0.88 #array by z value (height) and squich a bit to close model gap
obj.modifiers["Array"].use_merge_vertices = True #merge to close any small gaps
#now use curve modifier to deform dna to fit more snuggly
bpy.ops.object.modifier_add(type='CURVE')
obj.modifiers["Curve"].object = bpy.data.objects[curve_name]
obj.modifiers["Curve"].deform_axis = 'POS_Z'
#add a bit of wiggle to the dna in the case of animation
bpy.ops.object.modifier_add(type='WAVE')
obj.modifiers["Wave"].use_y = False
obj.modifiers["Wave"].height = 0.01
obj.modifiers["Wave"].speed = 0.1
#make material and add to DNA
mat = bpy.data.materials.new(name=object_name)
mat.use_nodes = True #use node trees, these can be seen by switching a panel to the shader editor if you want. It will look like the above shader, just not nicely placed.
mat_nodes = mat.node_tree.nodes
mat_links = mat.node_tree.links
mat = bpy.data.materials[object_name] #Get the material you want
bpy.data.materials[object_name].node_tree.nodes["Principled BSDF"].inputs[0].default_value = dna_color
bpy.data.materials[object_name].node_tree.nodes["Principled BSDF"].inputs[1].default_value = 0.2
#Assign material
obj.select_set(True)
obj.data.materials.clear()#clear material from new dna
mat = bpy.data.materials.get(object_name) #set material properties of collection
obj.data.materials.append(mat) #add material
#Add to collection and clean from other collections
if obj not in list(bpy.data.collections[collection_name].objects): #add collection if name isn't in collections yet
bpy.data.collections[collection_name].objects.link(obj) #link curve to new collection
if obj in list(bpy.context.scene.collection.objects):
bpy.context.scene.collection.objects.unlink(obj) #remove link from scene collection
if obj in list(bpy.data.collections["Collection"].objects):
bpy.data.collections["Collection"].objects.unlink(obj) #remove link from default collection
def trim_and_move_curve(
curve_name="gDNA_path",
collection_name="gDNA",
object_name="gDNA",
attach_to=None,
attach_left_or_right=None,
hybridize_to=None,
hybridize_anchor_left_or_right=None):
#Cut curve to size of dna bounding box
ob=bpy.data.objects[object_name]
ob.rotation_mode="XYZ"
spline=bpy.data.objects[curve_name].data.splines[0]#spline defining curve, since all paths are a simple single spline, we can use 0 index
spline.bezier_points[0].co.x=[ob.matrix_world @ Vector(corner) for corner in ob.bound_box][0].x #set left most x value of DNA
spline.bezier_points[1].co.x=[ob.matrix_world @ Vector(corner) for corner in ob.bound_box][7].x #set right most x value of DNA
if attach_to is not None and attach_left_or_right is not None: #get target object to attach to
attach_ob=attach_to
length_curve=abs(spline.bezier_points[0].co.x-spline.bezier_points[1].co.x) #gets DNA repeat defined length
if attach_left_or_right=="left": #attach at right point (so target DNA is left of the attach to DNA)
spline.bezier_points[1].co.x=[attach_ob.matrix_world @ Vector(corner) for corner in attach_ob.bound_box][0].x+0.02 #set right most x value of DNA to left most object point
spline.bezier_points[0].co.x=spline.bezier_points[1].co.x-length_curve+0.02 #set left most DNA point to length distance from right point
if attach_left_or_right=="right":
spline.bezier_points[0].co.x=[attach_ob.matrix_world @ Vector(corner) for corner in attach_ob.bound_box][7].x-0.02#set right most x value of DNA to left most object point
spline.bezier_points[1].co.x=spline.bezier_points[0].co.x+length_curve-0.02 #set left most DNA point to length distance from right point
if hybridize_to is not None and hybridize_anchor_left_or_right is not None: #get target segment to hybridize to
hyb_ob=hybridize_to
length_curve=abs(spline.bezier_points[0].co.x-spline.bezier_points[1].co.x) #gets DNA repeat defined length
if hybridize_anchor_left_or_right=="left": #attach at right point (so target DNA is left of the attach to DNA), this helps with anchoring, since annealed oligoes can hang off edge
spline.bezier_points[1].co.x=[hyb_ob.matrix_world @ Vector(corner) for corner in hyb_ob.bound_box][0].x+0.02 #set right most x value of DNA to left most object point
spline.bezier_points[0].co.x=spline.bezier_points[1].co.x-length_curve+0.02 #set left most DNA point to length distance from right point
rotation=hyb_ob.rotation_euler.z+(270*pi/180)
loc_change=(0.025,0.025,0.025)
if hybridize_anchor_left_or_right=="right":
rotation=hyb_ob.rotation_euler.z+(270*pi/180)
loc_change=(0.025,0.025,-0.025)
spline.bezier_points[0].co.x=[hyb_ob.matrix_world @ Vector(corner) for corner in hyb_ob.bound_box][7].x-0.02#set right most x value of DNA to left most object point
spline.bezier_points[1].co.x=spline.bezier_points[0].co.x+length_curve-0.02 #set left most DNA point to length distance from right point
ob=bpy.data.objects[object_name]
ob.delta_rotation_euler.z=rotation # rotate the object #just euler stuff
ob.matrix_world.translation=loc_change # nudge a bit to properly align
spline.bezier_points[0].handle_left[0]=spline.bezier_points[0].co[0]-0.1 #Also change the bezier handles to be a bit smaller
spline.bezier_points[0].handle_right[0]=spline.bezier_points[0].co[0]+0.1
spline.bezier_points[1].handle_left[0]=spline.bezier_points[1].co[0]-0.1
spline.bezier_points[1].handle_right[0]=spline.bezier_points[1].co[0]+0.1
def add_text_to_curve(
curve_name="gDNA_path",
object_name="gDNA",
collection_name="gDNA",
text_body="gDNA",
yloc=0.1):
#Align text to path and offset
text_name=curve_name+"_text"
bpy.ops.object.text_add(enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
bpy.context.active_object.name = text_name
text_ob=bpy.data.objects[text_name]
text_ob.data.extrude = 0.01 #solidify text
text_ob.rotation_euler.z = pi/2 #justeulerthings rotate 180
text_ob.scale=(0.4,0.4,0.4)#rescale
text_ob.data.size = 0.5 #resize text
spline=bpy.data.objects[curve_name].data.splines[0]#spline defining curve, since all paths are a simple single spline, we can use 0 index
location_x=(spline.bezier_points[1].co.x+spline.bezier_points[0].co.x)/2 #get midpoint on path
text_ob.location.z=0.1 #set locations
text_ob.location.x=location_x
text_ob.data.body=text_body #finally add in the text
text_ob.location.y=yloc
mat = bpy.data.materials.get(object_name) #material name is same as object name by default
text_ob.data.materials.append(mat)#set material to match object
#add a bit of wiggle to the dna in the case of animation
bpy.ops.object.modifier_add(type='WAVE')
text_ob.modifiers["Wave"].use_y = False
text_ob.modifiers["Wave"].height = 0.01
text_ob.modifiers["Wave"].speed = 0.1
#Add to collection and clean from other collections
if text_ob not in list(bpy.data.collections[collection_name].objects): #add collection if name isn't in collections yet
bpy.data.collections[collection_name].objects.link(text_ob) #link curve to new collection
if text_ob in list(bpy.context.scene.collection.objects):
bpy.context.scene.collection.objects.unlink(text_ob) #remove link from scene collection
if text_ob in list(bpy.data.collections["Collection"].objects):
bpy.data.collections["Collection"].objects.unlink(text_ob) #remove link from default collection
def add_segment(
segment_name="gDNA",
dna_color="#808080",
dna_length=50,
import_object_name="DNA_Surface_real",
attach_to=None,
attach_left_or_right=None,
hybridize_to=None,
hybridize_anchor_left_or_right=None,
text_body=None,
file_path="",
yloc=0.1):
if segment_name not in bpy.data.objects: #make sure we aren't trying to make duplicate objects
#dna_color=hex_color_to_blender_rgb(dna_color)
setup_initial_curve(
curve_name=segment_name+"_path",
collection_name=segment_name)
load_in_structure(
collection_name=segment_name,
object_name=segment_name,
curve_name=segment_name+"_path",
dna_length=dna_length,
dna_color=dna_color,
import_object_name=import_object_name,
file_path=file_path)
trim_and_move_curve(
curve_name=segment_name+"_path",
collection_name=segment_name,
object_name=segment_name,
attach_to=attach_to,
attach_left_or_right=attach_left_or_right)
if text_body is not None:
add_text_to_curve(curve_name=segment_name+"_path",
object_name=segment_name,
collection_name=segment_name,
text_body=text_body,
yloc=yloc)
else:
print("Segment name already in scene.")
#using a simple poll to decrease list of dna segments
def choseable_dna_segment(self, object):
return not object.name.endswith("text") and not object.name.endswith("path")
def add_tn5(
file_path="",
collection_name="Tn5_gDNA_left",
color=(0.3, 0.3, 0.8, 1),
attach_to="gDNA",
attach_left_or_right="left"
):
#tn5_loaded_oligo_list=["ME_right","tn5_idx_left","truseq_i7_left"]
inner_path="Object"
scene = bpy.context.scene
attach_to=attach_to.name+"_path" #select the material, but we actually want to just deform it to the path
if collection_name not in bpy.data.collections: #add collection if name isn't in collections yet
bpy.data.collections.new(name=collection_name) #make new collection
if bpy.data.collections[collection_name] not in list(scene.collection.children): #link collection to scene if it isn't in yet
scene.collection.children.link(bpy.data.collections[collection_name]) #link new collection to scene
object_name=collection_name #collection name and object name the same
#make material and add to tn5
mat = bpy.data.materials.new(name=collection_name)
mat.use_nodes = True #use node trees, these can be seen by switching a panel to the shader editor if you want. It will look like the above shader, just not nicely placed.
mat_nodes = mat.node_tree.nodes
mat_links = mat.node_tree.links
mat = bpy.data.materials[object_name] #Get the material you want
bpy.data.materials[object_name].node_tree.nodes["Principled BSDF"].inputs[0].default_value = color
bpy.data.materials[object_name].node_tree.nodes["Principled BSDF"].inputs[1].default_value = 0.2
#tn5 1
import_object_name_1="Tn5_1"
old_set = set(bpy.data.objects[:])
bpy.ops.wm.append(
filepath=os.path.join(file_path,inner_path, import_object_name_1),
directory=os.path.join(file_path, inner_path),
filename=import_object_name_1, use_recursive=True
)
new_set = set(bpy.data.objects[:]) - old_set
obj=list(new_set)[0]
obj.name= object_name+"_1" #change name of object we just made
bpy.context.view_layer.objects.active = obj
bpy.ops.object.modifier_add(type='CURVE')#Add array modifier to dna and assign to path
obj.modifiers["Curve"].object = bpy.data.objects[attach_to]
obj.modifiers["Curve"].deform_axis = 'POS_Z'
#Assign material
obj.select_set(True)
obj.data.materials.clear()#clear material from new dna
mat = bpy.data.materials.get(object_name) #set material properties of collection
obj.data.materials.append(mat) #add material
#tn5 2
import_object_name_1="Tn5_2"
old_set = set(bpy.data.objects[:])
bpy.ops.wm.append(
filepath=os.path.join(file_path,inner_path, import_object_name_1),
directory=os.path.join(file_path,inner_path),
filename=import_object_name_1, use_recursive=True
)
new_set = set(bpy.data.objects[:]) - old_set
obj=list(new_set)[0]
obj.name= object_name+"_2" #change name of object we just made
bpy.context.view_layer.objects.active = obj
bpy.ops.object.modifier_add(type='CURVE')#Add array modifier to dna and assign to path
obj.modifiers["Curve"].object = bpy.data.objects[attach_to]
obj.modifiers["Curve"].deform_axis = 'POS_Z'
#Assign material
obj.select_set(True)
obj.data.materials.clear()#clear material from new dna
mat = bpy.data.materials.get(object_name) #set material properties of collection
obj.data.materials.append(mat) #add material
for i in [object_name+"_1",object_name+"_2"]:#Add to collection and clean from other collections
obj=bpy.data.objects[i]
if obj not in list(bpy.data.collections[collection_name].objects): #add collection if name isn't in collections yet
bpy.data.collections[collection_name].objects.link(obj) #link curve to new collection
if obj in list(bpy.context.scene.collection.objects):
bpy.context.scene.collection.objects.unlink(obj) #remove link from scene collection
if obj in list(bpy.data.collections["Collection"].objects):
bpy.data.collections["Collection"].objects.unlink(obj) #remove link from default collection
#should probably update this to be relative, and use a proper addon name
DNA_PROPS = [
("dna_length", bpy.props.IntProperty(
name="Length of DNA",
description="Length of DNA (bp) of segment to generate.",
default=25,
soft_max=100,
min=1
)),
("segment_name", bpy.props.StringProperty(
name="Segment Name",
description="Name of DNA segment to generate.",
default="gDNA"
)),
("color_set", bpy.props.FloatVectorProperty(
name = "Color Picker",
subtype = "COLOR",
default = (1.0,1.0,1.0,1.0),
size = 4
)),
("dna_type", bpy.props.EnumProperty(
name="DNA/RNA Type",
description="Select DNA or RNA type.",
default="DNA_Surface_real",
items=[("DNA_Surface_real","DNA","DNA Surface"),("RNA_Surface_real","RNA","RNA Surface")]
)),
("text_body", bpy.props.StringProperty(
name="Floating Text",
description="Adds a floating text descriptor next to the segment.",
default="gDNA"
)),
("text_body_position", bpy.props.FloatProperty(
name="Text Position",
subtype = "DISTANCE",
unit = "LENGTH",
description="Adjusts floating text descriptor location",
default=0.1,
min=-1,
max=1
)),
("attach_to", bpy.props.PointerProperty(
name="Attach to:",
type=bpy.types.Object,
poll=choseable_dna_segment,
description="Select DNA segment to attach to.")),
("attach_left_or_right", bpy.props.EnumProperty(
name="Attach Position for Target",
description="Attach to the left (-X) or right (+X) of target molecule?",
default="None",
items=[("None","None","None"),("left","left","left"),("right","right","right")]
)),
("hybridize_to", bpy.props.PointerProperty(
name="Hybridize to:",
type=bpy.types.Object,
poll=choseable_dna_segment,
description="Select DNA segment to hybridize to.")),
("hybridize_anchor_left_or_right", bpy.props.EnumProperty(
name="Anchoring Position for Generated Segment",
description="Anchor hybrid DNA to the left (-X) or right (+X) of target molecule?",
default="None",
items=[("None","None","None"),("left","left","left"),("right","right","right")]
)),
("dna_modifier_color", bpy.props.FloatVectorProperty(
name="New Color",
subtype='COLOR',
default = (1.0,1.0,1.0,1.0),
size = 4))
]
TN5_PROPS = [
("tn5_attach_to", bpy.props.PointerProperty(
name="Attach to:",
type=bpy.types.Object,
poll=choseable_dna_segment,
description="Select DNA segment to attach to.")),
("tn5_attach_left_or_right", bpy.props.EnumProperty(
name="Attach Position for Target",
description="Attach to the left (-X) or right (+X) of target molecule?",
default="left",
items=[("left","left","left"),("right","right","right")]
)),
("tn5_collection_name", bpy.props.StringProperty(
name="Collection Name",
description="Name of Tn5 Collection Name",
default="Tn5_gDNA_left"
)),
("tn5_color_set", bpy.props.FloatVectorProperty(
name = "Color Picker",
subtype = "COLOR",
default = (1.0,1.0,1.0,1.0),
size = 4
)),
]
class Preferences_dna_maker(bpy.types.AddonPreferences):
bl_idname= __name__ #change this to __package__ when module is packaged together
filepath_blendfile: bpy.props.StringProperty(
name="Path to Packaged Blender File",
subtype='FILE_PATH',
)
def draw(self, context):
layout = self.layout
layout.label(text="Please add path to packaged blend file containing PDB structures (dna.blend).")
layout.prop(self, "filepath_blendfile")
class MESH_OT_dna_maker(bpy.types.Operator):
"""Let's make some DNA!"""
bl_idname = "mesh.dna_maker"
bl_label = "DNA Maker"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props=context.scene
add_segment(
file_path=context.preferences.addons['dna_built'].preferences.filepath_blendfile,
segment_name=props.segment_name,
dna_length=props.dna_length,
dna_color=props.color_set,
import_object_name=props.dna_type,
text_body=props.text_body,
yloc=props.text_body_position,
attach_to=props.attach_to,
attach_left_or_right=props.attach_left_or_right,
hybridize_to=props.hybridize_to,
hybridize_anchor_left_or_right=props.hybridize_anchor_left_or_right)
return {'FINISHED'}
class VIEW3D_PT_dna_maker(bpy.types.Panel):
"""Panel for DNA Molecule Builder"""
bl_space_type="VIEW_3D"
bl_region_type="UI"
bl_category="DNA Maker"
bl_label="Add Segments"
def draw(self,context):
layout = self.layout
obj = context.active_object
#Initial default option for messing with gDNA
col = layout.row()
col.operator('mesh.dna_maker',
text="Make DNA",
icon="RNA")
layout.label(text="DNA Layout:")
for prop_name in ["segment_name","dna_length","color_set","dna_type"]:
col = layout.row()
col.prop(context.scene, prop_name)
layout.label(text="Text:")
for prop_name in ["text_body","text_body_position"]:
col = layout.row()
col.prop(context.scene, prop_name)
layout.label(text="Attach to:")
col=layout.row()
col.prop(context.scene,"attach_to")
if context.scene.attach_to != None:
col=layout.row()
col.prop(context.scene,"attach_left_or_right")
col=layout.row()
layout.label(text="Hybridize to:")
col=layout.row()
col.prop(context.scene,"hybridize_to")
if context.scene.hybridize_to != None:
col=layout.row()
col.prop(context.scene,"hybridize_anchor_left_or_right")
class MESH_OT_tn5_addition(bpy.types.Operator):
"""Let's add some proteins!"""
bl_idname = "mesh.tn5_addition"
bl_label = "DNA Maker"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props=context.scene
add_tn5(
file_path=context.preferences.addons['dna_built'].preferences.filepath_blendfile,
collection_name=props.tn5_collection_name,
color=props.tn5_color_set,
attach_to=props.tn5_attach_to,
attach_left_or_right=props.tn5_attach_left_or_right)
return {'FINISHED'}
class VIEW3D_PT_tn5_addition(bpy.types.Panel):
"""Panel for DNA Molecule Builder"""
bl_space_type="VIEW_3D"
bl_region_type="UI"
bl_category="DNA Maker"
bl_label="Add Tn5"
def draw(self,context):
layout = self.layout
obj = context.object
#Modify color of DNA and Text through manipulating the material properties
col = layout.row()
layout.label(text="Select a DNA segment to tagment.")
layout.prop_search(context.scene, "tn5_attach_to", context.scene, "objects", text="DNA")
for prop_name in ["tn5_attach_left_or_right","tn5_collection_name","tn5_color_set"]:
col = layout.row()
col.prop(context.scene, prop_name)
col = layout.row()
if context.scene.tn5_attach_to != None:
col.operator('mesh.tn5_addition',
text="Tagment DNA",
icon="RNA")
CLASSES = [
Preferences_dna_maker,
MESH_OT_dna_maker,
VIEW3D_PT_dna_maker,
MESH_OT_tn5_addition,
VIEW3D_PT_tn5_addition
]
def register():
for cls in CLASSES:
bpy.utils.register_class(cls)
for (prop_name, prop_value) in DNA_PROPS:
setattr(bpy.types.Scene, prop_name, prop_value)
for (prop_name, prop_value) in TN5_PROPS:
setattr(bpy.types.Scene, prop_name, prop_value)
def unregister():
for cls in CLASSES:
bpy.utils.unregister_class(cls)
for (prop_name, prop_value) in DNA_PROPS:
delattr(bpy.types.Scene, prop_name)
for (prop_name, prop_value) in TN5_PROPS:
delattr(bpy.types.Scene, prop_name)
#hybridization looks like it has a weird logic flow or something, attaches correctly, but doesn't rotate or translate
#To Do
#Add proteins as additional function
#Adjust Tn5 angle to correct for DNA
#Add DNA copy for second Tn5