Add files via upload
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,345 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# 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 2
|
||||
# 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
|
||||
# MERCHANTABILITY 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, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# --------------------------------- DUAL MESH -------------------------------- #
|
||||
# -------------------------------- version 0.3 ------------------------------- #
|
||||
# #
|
||||
# Convert a generic mesh to its dual. With open meshes it can get some wired #
|
||||
# effect on the borders. #
|
||||
# #
|
||||
# (c) Alessandro Zomparelli #
|
||||
# (2017) #
|
||||
# #
|
||||
# http://www.co-de-it.com/ #
|
||||
# #
|
||||
# ############################################################################ #
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
import bmesh
|
||||
from .utils import *
|
||||
|
||||
|
||||
class dual_mesh_tessellated(Operator):
|
||||
bl_idname = "object.dual_mesh_tessellated"
|
||||
bl_label = "Dual Mesh"
|
||||
bl_description = ("Generate a polygonal mesh using Tessellate. (Non-destructive)")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
apply_modifiers : BoolProperty(
|
||||
name="Apply Modifiers",
|
||||
default=True,
|
||||
description="Apply object's modifiers"
|
||||
)
|
||||
|
||||
source_faces : EnumProperty(
|
||||
items=[
|
||||
('QUAD', 'Quad Faces', ''),
|
||||
('TRI', 'Triangles', '')],
|
||||
name="Source Faces",
|
||||
description="Source polygons",
|
||||
default="QUAD",
|
||||
options={'LIBRARY_EDITABLE'}
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
auto_layer_collection()
|
||||
ob0 = context.object
|
||||
name1 = "DualMesh_{}_Component".format(self.source_faces)
|
||||
# Generate component
|
||||
if self.source_faces == 'QUAD':
|
||||
verts = [(0.0, 0.0, 0.0), (0.0, 0.5, 0.0),
|
||||
(0.0, 1.0, 0.0), (0.5, 1.0, 0.0),
|
||||
(1.0, 1.0, 0.0), (1.0, 0.5, 0.0),
|
||||
(1.0, 0.0, 0.0), (0.5, 0.0, 0.0),
|
||||
(1/3, 1/3, 0.0), (2/3, 2/3, 0.0)]
|
||||
edges = [(0,1), (1,2), (2,3), (3,4), (4,5), (5,6), (6,7),
|
||||
(7,0), (1,8), (8,7), (3,9), (9,5), (8,9)]
|
||||
faces = [(7,8,1,0), (8,9,3,2,1), (9,5,4,3), (9,8,7,6,5)]
|
||||
else:
|
||||
verts = [(0.0,0.0,0.0), (0.5,0.0,0.0), (1.0,0.0,0.0), (0.0,1.0,0.0), (0.5,1.0,0.0), (1.0,1.0,0.0)]
|
||||
edges = [(0,1), (1,2), (2,5), (5,4), (4,3), (3,0), (1,4)]
|
||||
faces = [(0,1,4,3), (1,2,5,4)]
|
||||
|
||||
# check pre-existing component
|
||||
try:
|
||||
_verts = [0]*len(verts)*3
|
||||
__verts = [c for co in verts for c in co]
|
||||
ob1 = bpy.data.objects[name1]
|
||||
ob1.data.vertices.foreach_get("co",_verts)
|
||||
for a, b in zip(_verts, __verts):
|
||||
if abs(a-b) > 0.0001:
|
||||
raise ValueError
|
||||
except:
|
||||
me = bpy.data.meshes.new("Dual-Mesh") # add a new mesh
|
||||
me.from_pydata(verts, edges, faces)
|
||||
me.update(calc_edges=True, calc_edges_loose=True)
|
||||
if self.source_faces == 'QUAD': n_seams = 8
|
||||
else: n_seams = 6
|
||||
for i in range(n_seams): me.edges[i].use_seam = True
|
||||
ob1 = bpy.data.objects.new(name1, me)
|
||||
context.collection.objects.link(ob1)
|
||||
# fix visualization issue
|
||||
context.view_layer.objects.active = ob1
|
||||
ob1.select_set(True)
|
||||
bpy.ops.object.editmode_toggle()
|
||||
bpy.ops.object.editmode_toggle()
|
||||
ob1.select_set(False)
|
||||
# hide component
|
||||
ob1.hide_select = True
|
||||
ob1.hide_render = True
|
||||
ob1.hide_viewport = True
|
||||
ob = convert_object_to_mesh(ob0,False,False)
|
||||
ob.name = 'DualMesh'
|
||||
#ob = bpy.data.objects.new("DualMesh", convert_object_to_mesh(ob0,False,False))
|
||||
#context.collection.objects.link(ob)
|
||||
#context.view_layer.objects.active = ob
|
||||
#ob.select_set(True)
|
||||
ob.tissue_tessellate.component = ob1
|
||||
ob.tissue_tessellate.generator = ob0
|
||||
ob.tissue_tessellate.gen_modifiers = self.apply_modifiers
|
||||
ob.tissue_tessellate.merge = True
|
||||
ob.tissue_tessellate.bool_dissolve_seams = True
|
||||
if self.source_faces == 'TRI': ob.tissue_tessellate.fill_mode = 'FAN'
|
||||
bpy.ops.object.update_tessellate()
|
||||
ob.location = ob0.location
|
||||
ob.matrix_world = ob0.matrix_world
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
class dual_mesh(Operator):
|
||||
bl_idname = "object.dual_mesh"
|
||||
bl_label = "Convert to Dual Mesh"
|
||||
bl_description = ("Convert a generic mesh into a polygonal mesh. (Destructive)")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
quad_method : EnumProperty(
|
||||
items=[('BEAUTY', 'Beauty',
|
||||
'Split the quads in nice triangles, slower method'),
|
||||
('FIXED', 'Fixed',
|
||||
'Split the quads on the 1st and 3rd vertices'),
|
||||
('FIXED_ALTERNATE', 'Fixed Alternate',
|
||||
'Split the quads on the 2nd and 4th vertices'),
|
||||
('SHORTEST_DIAGONAL', 'Shortest Diagonal',
|
||||
'Split the quads based on the distance between the vertices')
|
||||
],
|
||||
name="Quad Method",
|
||||
description="Method for splitting the quads into triangles",
|
||||
default="FIXED",
|
||||
options={'LIBRARY_EDITABLE'}
|
||||
)
|
||||
polygon_method : EnumProperty(
|
||||
items=[
|
||||
('BEAUTY', 'Beauty', 'Arrange the new triangles evenly'),
|
||||
('CLIP', 'Clip',
|
||||
'Split the polygons with an ear clipping algorithm')],
|
||||
name="Polygon Method",
|
||||
description="Method for splitting the polygons into triangles",
|
||||
default="BEAUTY",
|
||||
options={'LIBRARY_EDITABLE'}
|
||||
)
|
||||
preserve_borders : BoolProperty(
|
||||
name="Preserve Borders",
|
||||
default=True,
|
||||
description="Preserve original borders"
|
||||
)
|
||||
apply_modifiers : BoolProperty(
|
||||
name="Apply Modifiers",
|
||||
default=True,
|
||||
description="Apply object's modifiers"
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
mode = context.mode
|
||||
if mode == 'EDIT_MESH':
|
||||
mode = 'EDIT'
|
||||
act = context.active_object
|
||||
if mode != 'OBJECT':
|
||||
sel = [act]
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
else:
|
||||
sel = context.selected_objects
|
||||
doneMeshes = []
|
||||
|
||||
for ob0 in sel:
|
||||
if ob0.type != 'MESH':
|
||||
continue
|
||||
if ob0.data.name in doneMeshes:
|
||||
continue
|
||||
ob = ob0
|
||||
mesh_name = ob0.data.name
|
||||
|
||||
# store linked objects
|
||||
clones = []
|
||||
n_users = ob0.data.users
|
||||
count = 0
|
||||
for o in bpy.data.objects:
|
||||
if o.type != 'MESH':
|
||||
continue
|
||||
if o.data.name == mesh_name:
|
||||
count += 1
|
||||
clones.append(o)
|
||||
if count == n_users:
|
||||
break
|
||||
|
||||
if self.apply_modifiers:
|
||||
bpy.ops.object.convert(target='MESH')
|
||||
ob.data = ob.data.copy()
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
ob.select_set(True)
|
||||
context.view_layer.objects.active = ob0
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# prevent borders erosion
|
||||
bpy.ops.mesh.select_mode(
|
||||
use_extend=False, use_expand=False, type='EDGE'
|
||||
)
|
||||
bpy.ops.mesh.select_non_manifold(
|
||||
extend=False, use_wire=False, use_boundary=True,
|
||||
use_multi_face=False, use_non_contiguous=False,
|
||||
use_verts=False
|
||||
)
|
||||
bpy.ops.mesh.extrude_region_move(
|
||||
MESH_OT_extrude_region={"mirror": False},
|
||||
TRANSFORM_OT_translate={"value": (0, 0, 0)}
|
||||
)
|
||||
|
||||
bpy.ops.mesh.select_mode(
|
||||
use_extend=False, use_expand=False, type='VERT',
|
||||
action='TOGGLE'
|
||||
)
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.quads_convert_to_tris(
|
||||
quad_method=self.quad_method, ngon_method=self.polygon_method
|
||||
)
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.modifier_add(type='SUBSURF')
|
||||
ob.modifiers[-1].name = "dual_mesh_subsurf"
|
||||
while True:
|
||||
bpy.ops.object.modifier_move_up(modifier="dual_mesh_subsurf")
|
||||
if ob.modifiers[0].name == "dual_mesh_subsurf":
|
||||
break
|
||||
|
||||
bpy.ops.object.modifier_apply(
|
||||
apply_as='DATA', modifier='dual_mesh_subsurf'
|
||||
)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
|
||||
verts = ob.data.vertices
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
verts[-1].select = True
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_more(use_face_step=False)
|
||||
|
||||
bpy.ops.mesh.select_similar(
|
||||
type='EDGE', compare='EQUAL', threshold=0.01)
|
||||
bpy.ops.mesh.select_all(action='INVERT')
|
||||
|
||||
bpy.ops.mesh.dissolve_verts()
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
|
||||
bpy.ops.mesh.select_non_manifold(
|
||||
extend=False, use_wire=False, use_boundary=True,
|
||||
use_multi_face=False, use_non_contiguous=False, use_verts=False)
|
||||
bpy.ops.mesh.select_more()
|
||||
|
||||
# find boundaries
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bound_v = [v.index for v in ob.data.vertices if v.select]
|
||||
bound_e = [e.index for e in ob.data.edges if e.select]
|
||||
bound_p = [p.index for p in ob.data.polygons if p.select]
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# select quad faces
|
||||
context.tool_settings.mesh_select_mode = (False, False, True)
|
||||
bpy.ops.mesh.select_face_by_sides(number=4, extend=False)
|
||||
|
||||
# deselect boundaries
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
for i in bound_v:
|
||||
context.active_object.data.vertices[i].select = False
|
||||
for i in bound_e:
|
||||
context.active_object.data.edges[i].select = False
|
||||
for i in bound_p:
|
||||
context.active_object.data.polygons[i].select = False
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
context.tool_settings.mesh_select_mode = (False, False, True)
|
||||
bpy.ops.mesh.edge_face_add()
|
||||
context.tool_settings.mesh_select_mode = (True, False, False)
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
|
||||
# delete boundaries
|
||||
bpy.ops.mesh.select_non_manifold(
|
||||
extend=False, use_wire=True, use_boundary=True,
|
||||
use_multi_face=False, use_non_contiguous=False, use_verts=True
|
||||
)
|
||||
bpy.ops.mesh.delete(type='VERT')
|
||||
|
||||
# remove middle vertices
|
||||
bm = bmesh.from_edit_mesh(ob.data)
|
||||
for v in bm.verts:
|
||||
if len(v.link_edges) == 2 and len(v.link_faces) < 3:
|
||||
v.select = True
|
||||
|
||||
# dissolve
|
||||
bpy.ops.mesh.dissolve_verts()
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
|
||||
# remove border faces
|
||||
if not self.preserve_borders:
|
||||
bpy.ops.mesh.select_non_manifold(
|
||||
extend=False, use_wire=False, use_boundary=True,
|
||||
use_multi_face=False, use_non_contiguous=False, use_verts=False
|
||||
)
|
||||
bpy.ops.mesh.select_more()
|
||||
bpy.ops.mesh.delete(type='FACE')
|
||||
|
||||
# clean wires
|
||||
bpy.ops.mesh.select_non_manifold(
|
||||
extend=False, use_wire=True, use_boundary=False,
|
||||
use_multi_face=False, use_non_contiguous=False, use_verts=False
|
||||
)
|
||||
bpy.ops.mesh.delete(type='EDGE')
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
ob0.data.name = mesh_name
|
||||
doneMeshes.append(mesh_name)
|
||||
|
||||
for o in clones:
|
||||
o.data = ob.data
|
||||
|
||||
for o in sel:
|
||||
o.select_set(True)
|
||||
|
||||
context.view_layer.objects.active = act
|
||||
bpy.ops.object.mode_set(mode=mode)
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,488 @@
|
||||
import bpy, os
|
||||
import numpy as np
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
from math import pi
|
||||
from bpy.types import (
|
||||
Operator,
|
||||
Panel,
|
||||
PropertyGroup,
|
||||
)
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
PointerProperty
|
||||
)
|
||||
from .utils import *
|
||||
|
||||
def change_speed_mode(self, context):
|
||||
props = context.scene.tissue_gcode
|
||||
if props.previous_speed_mode != props.speed_mode:
|
||||
if props.speed_mode == 'SPEED':
|
||||
props.speed = props.feed/60
|
||||
props.speed_vertical = props.feed_vertical/60
|
||||
props.speed_horizontal = props.feed_horizontal/60
|
||||
else:
|
||||
props.feed = props.speed*60
|
||||
props.feed_vertical = props.speed_vertical*60
|
||||
props.feed_horizontal = props.speed_horizontal*60
|
||||
props.previous_speed_mode == props.speed_mode
|
||||
return
|
||||
|
||||
class tissue_gcode_prop(PropertyGroup):
|
||||
last_e : FloatProperty(name="Pull", default=5.0, min=0, soft_max=10)
|
||||
path_length : FloatProperty(name="Pull", default=5.0, min=0, soft_max=10)
|
||||
|
||||
folder : StringProperty(
|
||||
name="File", default="", subtype='FILE_PATH',
|
||||
description = 'Destination folder.\nIf missing, the file folder will be used'
|
||||
)
|
||||
pull : FloatProperty(
|
||||
name="Pull", default=5.0, min=0, soft_max=10,
|
||||
description='Pull material before lift'
|
||||
)
|
||||
push : FloatProperty(
|
||||
name="Push", default=5.0, min=0, soft_max=10,
|
||||
description='Push material before start extruding'
|
||||
)
|
||||
dz : FloatProperty(
|
||||
name="dz", default=2.0, min=0, soft_max=20,
|
||||
description='Z movement for lifting the nozzle before travel'
|
||||
)
|
||||
flow_mult : FloatProperty(
|
||||
name="Flow Mult", default=1.0, min=0, soft_max=3,
|
||||
description = 'Flow multiplier.\nUse a single value or a list of values for changing it during the printing path'
|
||||
)
|
||||
feed : IntProperty(
|
||||
name="Feed Rate (F)", default=3600, min=0, soft_max=20000,
|
||||
description='Printing speed'
|
||||
)
|
||||
feed_horizontal : IntProperty(
|
||||
name="Feed Horizontal", default=7200, min=0, soft_max=20000,
|
||||
description='Travel speed'
|
||||
)
|
||||
feed_vertical : IntProperty(
|
||||
name="Feed Vertical", default=3600, min=0, soft_max=20000,
|
||||
description='Lift movements speed'
|
||||
)
|
||||
|
||||
speed : IntProperty(
|
||||
name="Speed", default=60, min=0, soft_max=100,
|
||||
description='Printing speed'
|
||||
)
|
||||
speed_horizontal : IntProperty(
|
||||
name="Travel", default=120, min=0, soft_max=200,
|
||||
description='Travel speed'
|
||||
)
|
||||
speed_vertical : IntProperty(
|
||||
name="Z-Lift", default=60, min=0, soft_max=200,
|
||||
description='Lift movements speed'
|
||||
)
|
||||
|
||||
esteps : FloatProperty(
|
||||
name="E Steps/Unit", default=5, min=0, soft_max=100)
|
||||
start_code : StringProperty(
|
||||
name="Start", default='', description = 'Text block for starting code'
|
||||
)
|
||||
end_code : StringProperty(
|
||||
name="End", default='', description = 'Text block for ending code'
|
||||
)
|
||||
auto_sort_layers : BoolProperty(
|
||||
name="Auto Sort Layers", default=True,
|
||||
description = 'Sort layers according to the Z of the median point'
|
||||
)
|
||||
auto_sort_points : BoolProperty(
|
||||
name="Auto Sort Points", default=False,
|
||||
description = 'Shift layer points trying to automatically reduce needed travel movements'
|
||||
)
|
||||
close_all : BoolProperty(
|
||||
name="Close Shapes", default=False,
|
||||
description = 'Repeat the starting point at the end of the vertices list for each layer'
|
||||
)
|
||||
nozzle : FloatProperty(
|
||||
name="Nozzle", default=0.4, min=0, soft_max=10,
|
||||
description='Nozzle diameter'
|
||||
)
|
||||
layer_height : FloatProperty(
|
||||
name="Layer Height", default=0.1, min=0, soft_max=10,
|
||||
description = 'Average layer height, needed for a correct extrusion'
|
||||
)
|
||||
filament : FloatProperty(
|
||||
name="Filament (\u03A6)", default=1.75, min=0, soft_max=120,
|
||||
description='Filament (or material container) diameter'
|
||||
)
|
||||
|
||||
gcode_mode : EnumProperty(items=[
|
||||
("CONT", "Continuous", ""),
|
||||
("RETR", "Retraction", "")
|
||||
], default='CONT', name="Mode",
|
||||
description = 'If retraction is used, then each separated list of vertices\nwill be considered as a different layer'
|
||||
)
|
||||
speed_mode : EnumProperty(items=[
|
||||
("SPEED", "Speed (mm/s)", ""),
|
||||
("FEED", "Feed (mm/min)", "")
|
||||
], default='SPEED', name="Speed Mode",
|
||||
description = 'Speed control mode',
|
||||
update = change_speed_mode
|
||||
)
|
||||
previous_speed_mode : StringProperty(
|
||||
name="previous_speed_mode", default='', description = ''
|
||||
)
|
||||
retraction_mode : EnumProperty(items=[
|
||||
("FIRMWARE", "Firmware", ""),
|
||||
("GCODE", "Gcode", "")
|
||||
], default='GCODE', name="Retraction Mode",
|
||||
description = 'If firmware retraction is used, then the retraction parameters will be controlled by the printer'
|
||||
)
|
||||
animate : BoolProperty(
|
||||
name="Animate", default=False,
|
||||
description = 'Show print progression according to current frame'
|
||||
)
|
||||
|
||||
|
||||
class TISSUE_PT_gcode_exporter(Panel):
|
||||
bl_category = "Tissue Gcode"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
#bl_space_type = 'PROPERTIES'
|
||||
#bl_region_type = 'WINDOW'
|
||||
#bl_context = "data"
|
||||
bl_label = "Tissue Gcode Export"
|
||||
#bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
try: return context.object.type in ('CURVE','MESH')
|
||||
except: return False
|
||||
|
||||
def draw(self, context):
|
||||
props = context.scene.tissue_gcode
|
||||
|
||||
#addon = context.user_preferences.addons.get(sverchok.__name__)
|
||||
#over_sized_buttons = addon.preferences.over_sized_buttons
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
row = col.row()
|
||||
row.prop(props, 'folder', toggle=True, text='')
|
||||
col = layout.column(align=True)
|
||||
row = col.row()
|
||||
row.prop(props, 'gcode_mode', expand=True, toggle=True)
|
||||
#col = layout.column(align=True)
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Extrusion:", icon='MOD_FLUIDSIM')
|
||||
#col.prop(self, 'esteps')
|
||||
col.prop(props, 'filament')
|
||||
col.prop(props, 'nozzle')
|
||||
col.prop(props, 'layer_height')
|
||||
col.separator()
|
||||
col.label(text="Speed (Feed Rate F):", icon='DRIVER')
|
||||
col.prop(props, 'speed_mode', text='')
|
||||
speed_prefix = 'feed' if props.speed_mode == 'FEED' else 'speed'
|
||||
col.prop(props, speed_prefix, text='Print')
|
||||
if props.gcode_mode == 'RETR':
|
||||
col.prop(props, speed_prefix + '_vertical', text='Z Lift')
|
||||
col.prop(props, speed_prefix + '_horizontal', text='Travel')
|
||||
col.separator()
|
||||
if props.gcode_mode == 'RETR':
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Retraction Mode:", icon='NOCURVE')
|
||||
row = col.row()
|
||||
row.prop(props, 'retraction_mode', expand=True, toggle=True)
|
||||
if props.retraction_mode == 'GCODE':
|
||||
col.separator()
|
||||
col.label(text="Retraction:", icon='PREFERENCES')
|
||||
col.prop(props, 'pull', text='Retraction')
|
||||
col.prop(props, 'dz', text='Z Hop')
|
||||
col.prop(props, 'push', text='Preload')
|
||||
col.separator()
|
||||
#col.label(text="Layers options:", icon='ALIGN_JUSTIFY')
|
||||
col.separator()
|
||||
col.prop(props, 'auto_sort_layers', text="Sort Layers (Z)")
|
||||
col.prop(props, 'auto_sort_points', text="Sort Points (XY)")
|
||||
#col.prop(props, 'close_all')
|
||||
col.separator()
|
||||
col.label(text='Custom Code:', icon='TEXT')
|
||||
col.prop_search(props, 'start_code', bpy.data, 'texts')
|
||||
col.prop_search(props, 'end_code', bpy.data, 'texts')
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 2.0
|
||||
row.operator('scene.tissue_gcode_export')
|
||||
#col.separator()
|
||||
#col.prop(props, 'animate', icon='TIME')
|
||||
|
||||
|
||||
class tissue_gcode_export(Operator):
|
||||
bl_idname = "scene.tissue_gcode_export"
|
||||
bl_label = "Export Gcode"
|
||||
bl_description = ("Export selected curve object as Gcode file")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
try:
|
||||
return context.object.type in ('CURVE', 'MESH')
|
||||
except:
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
props = scene.tissue_gcode
|
||||
# manage data
|
||||
if props.speed_mode == 'SPEED':
|
||||
props.feed = props.speed*60
|
||||
props.feed_vertical = props.speed_vertical*60
|
||||
props.feed_horizontal = props.speed_horizontal*60
|
||||
feed = props.feed
|
||||
feed_v = props.feed_vertical
|
||||
feed_h = props.feed_horizontal
|
||||
layer = props.layer_height
|
||||
flow_mult = props.flow_mult
|
||||
#if context.object.type != 'CURVE':
|
||||
# self.report({'ERROR'}, 'Please select a Curve object')
|
||||
# return {'CANCELLED'}
|
||||
ob = context.object
|
||||
matr = ob.matrix_world
|
||||
if ob.type == 'MESH':
|
||||
dg = context.evaluated_depsgraph_get()
|
||||
mesh = ob.evaluated_get(dg).data
|
||||
edges = [list(e.vertices) for e in mesh.edges]
|
||||
verts = [v.co for v in mesh.vertices]
|
||||
ordered_verts = find_curves(edges, len(mesh.vertices))
|
||||
ob = curve_from_pydata(verts, ordered_verts, name='__temp_curve__', merge_distance=0.1, set_active=False)
|
||||
|
||||
vertices = [[matr @ p.co.xyz for p in s.points] for s in ob.data.splines]
|
||||
cyclic_u = [s.use_cyclic_u for s in ob.data.splines]
|
||||
|
||||
if ob.name == '__temp_curve__': bpy.data.objects.remove(ob)
|
||||
|
||||
if len(vertices) == 1: props.gcode_mode = 'CONT'
|
||||
export = True
|
||||
|
||||
# open file
|
||||
if(export):
|
||||
if props.folder == '':
|
||||
folder = '//' + os.path.splitext(bpy.path.basename(bpy.context.blend_data.filepath))[0]
|
||||
else:
|
||||
folder = props.folder
|
||||
if '.gcode' not in folder: folder += '.gcode'
|
||||
path = bpy.path.abspath(folder)
|
||||
file = open(path, 'w')
|
||||
try:
|
||||
for line in bpy.data.texts[props.start_code].lines:
|
||||
file.write(line.body + '\n')
|
||||
except:
|
||||
pass
|
||||
|
||||
#if props.gcode_mode == 'RETR':
|
||||
|
||||
# sort layers (Z)
|
||||
if props.auto_sort_layers:
|
||||
sorted_verts = []
|
||||
for curve in vertices:
|
||||
# mean z
|
||||
listz = [v[2] for v in curve]
|
||||
meanz = np.mean(listz)
|
||||
# store curve and meanz
|
||||
sorted_verts.append((curve, meanz))
|
||||
vertices = [data[0] for data in sorted(sorted_verts, key=lambda height: height[1])]
|
||||
|
||||
# sort vertices (XY)
|
||||
if props.auto_sort_points:
|
||||
# curves median point
|
||||
median_points = [np.mean(verts,axis=0) for verts in vertices]
|
||||
|
||||
# chose starting point for each curve
|
||||
for j, curve in enumerate(vertices):
|
||||
# for closed curves finds the best starting point
|
||||
if cyclic_u[j]:
|
||||
# create kd tree
|
||||
kd = mathutils.kdtree.KDTree(len(curve))
|
||||
for i, v in enumerate(curve):
|
||||
kd.insert(v, i)
|
||||
kd.balance()
|
||||
|
||||
if props.gcode_mode == 'RETR':
|
||||
if j==0:
|
||||
# close to next two curves median point
|
||||
co_find = np.mean(median_points[j+1:j+3],axis=0)
|
||||
elif j < len(vertices)-1:
|
||||
co_find = np.mean([median_points[j-1],median_points[j+1]],axis=0)
|
||||
else:
|
||||
co_find = np.mean(median_points[j-2:j],axis=0)
|
||||
#flow_mult[j] = flow_mult[j][index:]+flow_mult[j][:index]
|
||||
#layer[j] = layer[j][index:]+layer[j][:index]
|
||||
else:
|
||||
if j==0:
|
||||
# close to next two curves median point
|
||||
co_find = np.mean(median_points[j+1:j+3],axis=0)
|
||||
else:
|
||||
co_find = vertices[j-1][-1]
|
||||
co, index, dist = kd.find(co_find)
|
||||
vertices[j] = vertices[j][index:]+vertices[j][:index+1]
|
||||
else:
|
||||
if j > 0:
|
||||
p0 = curve[0]
|
||||
p1 = curve[-1]
|
||||
last = vertices[j-1][-1]
|
||||
d0 = (last-p0).length
|
||||
d1 = (last-p1).length
|
||||
if d1 < d0: vertices[j].reverse()
|
||||
|
||||
|
||||
|
||||
'''
|
||||
# close shapes
|
||||
if props.close_all:
|
||||
for i in range(len(vertices)):
|
||||
vertices[i].append(vertices[i][0])
|
||||
#flow_mult[i].append(flow_mult[i][0])
|
||||
#layer[i].append(layer[i][0])
|
||||
'''
|
||||
# calc bounding box
|
||||
min_corner = np.min(vertices[0],axis=0)
|
||||
max_corner = np.max(vertices[0],axis=0)
|
||||
for i in range(1,len(vertices)):
|
||||
eval_points = vertices[i] + [min_corner]
|
||||
min_corner = np.min(eval_points,axis=0)
|
||||
eval_points = vertices[i] + [max_corner]
|
||||
max_corner = np.max(eval_points,axis=0)
|
||||
|
||||
# initialize variables
|
||||
e = 0
|
||||
last_vert = Vector((0,0,0))
|
||||
maxz = 0
|
||||
path_length = 0
|
||||
travel_length = 0
|
||||
|
||||
printed_verts = []
|
||||
printed_edges = []
|
||||
travel_verts = []
|
||||
travel_edges = []
|
||||
|
||||
# write movements
|
||||
for i in range(len(vertices)):
|
||||
curve = vertices[i]
|
||||
first_id = len(printed_verts)
|
||||
for j in range(len(curve)):
|
||||
v = curve[j]
|
||||
v_flow_mult = flow_mult#[i][j]
|
||||
v_layer = layer#[i][j]
|
||||
|
||||
# record max z
|
||||
maxz = np.max((maxz,v[2]))
|
||||
#maxz = max(maxz,v[2])
|
||||
|
||||
# first point of the gcode
|
||||
if i == j == 0:
|
||||
printed_verts.append(v)
|
||||
if(export):
|
||||
file.write('G92 E0 \n')
|
||||
params = v[:3] + (feed,)
|
||||
to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
|
||||
file.write(to_write)
|
||||
else:
|
||||
# start after retraction
|
||||
if j == 0 and props.gcode_mode == 'RETR':
|
||||
if(export):
|
||||
params = v[:2] + (maxz+props.dz,) + (feed_h,)
|
||||
to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
|
||||
file.write(to_write)
|
||||
params = v[:3] + (feed_v,)
|
||||
to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
|
||||
file.write(to_write)
|
||||
to_write = 'G1 F{:.0f}\n'.format(feed)
|
||||
file.write(to_write)
|
||||
if props.retraction_mode == 'GCODE':
|
||||
e += props.push
|
||||
file.write( 'G1 E' + format(e, '.4f') + '\n')
|
||||
else:
|
||||
file.write('G11\n')
|
||||
printed_verts.append((v[0], v[1], maxz+props.dz))
|
||||
travel_edges.append((len(printed_verts)-1, len(printed_verts)-2))
|
||||
travel_length += (Vector(printed_verts[-1])-Vector(printed_verts[-2])).length
|
||||
printed_verts.append(v)
|
||||
travel_edges.append((len(printed_verts)-1, len(printed_verts)-2))
|
||||
travel_length += maxz+props.dz - v[2]
|
||||
# regular extrusion
|
||||
else:
|
||||
printed_verts.append(v)
|
||||
v1 = Vector(v)
|
||||
v0 = Vector(curve[j-1])
|
||||
dist = (v1-v0).length
|
||||
area = v_layer * props.nozzle + pi*(v_layer/2)**2 # rectangle + circle
|
||||
cylinder = pi*(props.filament/2)**2
|
||||
flow = area / cylinder * (0 if j == 0 else 1)
|
||||
e += dist * v_flow_mult * flow
|
||||
params = v[:3] + (e,)
|
||||
if(export):
|
||||
to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params)
|
||||
file.write(to_write)
|
||||
path_length += dist
|
||||
printed_edges.append([len(printed_verts)-1, len(printed_verts)-2])
|
||||
if props.gcode_mode == 'RETR':
|
||||
v0 = Vector(curve[-1])
|
||||
if props.close_all and False:
|
||||
#printed_verts.append(v0)
|
||||
printed_edges.append([len(printed_verts)-1, first_id])
|
||||
|
||||
v1 = Vector(curve[0])
|
||||
dist = (v0-v1).length
|
||||
area = v_layer * props.nozzle + pi*(v_layer/2)**2 # rectangle + circle
|
||||
cylinder = pi*(props.filament/2)**2
|
||||
flow = area / cylinder
|
||||
e += dist * v_flow_mult * flow
|
||||
params = v1[:3] + (e,)
|
||||
if(export):
|
||||
to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params)
|
||||
file.write(to_write)
|
||||
path_length += dist
|
||||
v0 = v1
|
||||
if i < len(vertices)-1:
|
||||
if(export):
|
||||
if props.retraction_mode == 'GCODE':
|
||||
e -= props.pull
|
||||
file.write('G0 E' + format(e, '.4f') + '\n')
|
||||
else:
|
||||
file.write('G10\n')
|
||||
params = v0[:2] + (maxz+props.dz,) + (feed_v,)
|
||||
to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params)
|
||||
file.write(to_write)
|
||||
printed_verts.append(v0.to_tuple())
|
||||
printed_verts.append((v0.x, v0.y, maxz+props.dz))
|
||||
travel_edges.append((len(printed_verts)-1, len(printed_verts)-2))
|
||||
travel_length += maxz+props.dz - v0.z
|
||||
if(export):
|
||||
# end code
|
||||
try:
|
||||
for line in bpy.data.texts[props.end_code].lines:
|
||||
file.write(line.body + '\n')
|
||||
except:
|
||||
pass
|
||||
file.close()
|
||||
print("Saved gcode to " + path)
|
||||
bb = list(min_corner) + list(max_corner)
|
||||
info = 'Bounding Box:\n'
|
||||
info += '\tmin\tX: {0:.1f}\tY: {1:.1f}\tZ: {2:.1f}\n'.format(*bb)
|
||||
info += '\tmax\tX: {3:.1f}\tY: {4:.1f}\tZ: {5:.1f}\n'.format(*bb)
|
||||
info += 'Extruded Filament: ' + format(e, '.2f') + '\n'
|
||||
info += 'Extruded Volume: ' + format(e*pi*(props.filament/2)**2, '.2f') + '\n'
|
||||
info += 'Printed Path Length: ' + format(path_length, '.2f') + '\n'
|
||||
info += 'Travel Length: ' + format(travel_length, '.2f')
|
||||
'''
|
||||
# animate
|
||||
if scene.animate:
|
||||
scene = bpy.context.scene
|
||||
try:
|
||||
param = (scene.frame_current - scene.frame_start)/(scene.frame_end - scene.frame_start)
|
||||
except:
|
||||
param = 1
|
||||
last_vert = max(int(param*len(printed_verts)),1)
|
||||
printed_verts = printed_verts[:last_vert]
|
||||
printed_edges = [e for e in printed_edges if e[0] < last_vert and e[1] < last_vert]
|
||||
travel_edges = [e for e in travel_edges if e[0] < last_vert and e[1] < last_vert]
|
||||
'''
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,477 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# 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 2
|
||||
# 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
|
||||
# MERCHANTABILITY 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, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
# --------------------------- LATTICE ALONG SURFACE -------------------------- #
|
||||
# -------------------------------- version 0.3 ------------------------------- #
|
||||
# #
|
||||
# Automatically generate and assign a lattice that follows the active surface. #
|
||||
# #
|
||||
# (c) Alessandro Zomparelli #
|
||||
# (2017) #
|
||||
# #
|
||||
# http://www.co-de-it.com/ #
|
||||
# #
|
||||
# ############################################################################ #
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
from bpy.types import Operator
|
||||
from bpy.props import (BoolProperty, StringProperty, FloatProperty)
|
||||
from mathutils import Vector
|
||||
|
||||
from .utils import *
|
||||
|
||||
|
||||
def not_in(element, grid):
|
||||
output = True
|
||||
for loop in grid:
|
||||
if element in loop:
|
||||
output = False
|
||||
break
|
||||
return output
|
||||
|
||||
|
||||
def grid_from_mesh(mesh, swap_uv):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
verts_grid = []
|
||||
edges_grid = []
|
||||
faces_grid = []
|
||||
|
||||
running_grid = True
|
||||
while running_grid:
|
||||
verts_loop = []
|
||||
edges_loop = []
|
||||
faces_loop = []
|
||||
|
||||
# storing first point
|
||||
verts_candidates = []
|
||||
if len(faces_grid) == 0:
|
||||
# for first loop check all vertices
|
||||
verts_candidates = bm.verts
|
||||
else:
|
||||
# for other loops start form the vertices of the first face
|
||||
# the last loop, skipping already used vertices
|
||||
verts_candidates = [v for v in bm.faces[faces_grid[-1][0]].verts if not_in(v.index, verts_grid)]
|
||||
|
||||
# check for last loop
|
||||
is_last = False
|
||||
for vert in verts_candidates:
|
||||
if len(vert.link_faces) == 1: # check if corner vertex
|
||||
vert.select = True
|
||||
verts_loop.append(vert.index)
|
||||
is_last = True
|
||||
break
|
||||
|
||||
if not is_last:
|
||||
for vert in verts_candidates:
|
||||
new_link_faces = [f for f in vert.link_faces if not_in(f.index, faces_grid)]
|
||||
if len(new_link_faces) < 2: # check if corner vertex
|
||||
vert.select = True
|
||||
verts_loop.append(vert.index)
|
||||
break
|
||||
|
||||
running_loop = len(verts_loop) > 0
|
||||
|
||||
while running_loop:
|
||||
bm.verts.ensure_lookup_table()
|
||||
id = verts_loop[-1]
|
||||
link_edges = bm.verts[id].link_edges
|
||||
# storing second point
|
||||
if len(verts_loop) == 1: # only one vertex stored in the loop
|
||||
if len(faces_grid) == 0: # first loop #
|
||||
edge = link_edges[swap_uv] # chose direction
|
||||
for vert in edge.verts:
|
||||
if vert.index != id:
|
||||
vert.select = True
|
||||
verts_loop.append(vert.index) # new vertex
|
||||
edges_loop.append(edge.index) # chosen edge
|
||||
faces_loop.append(edge.link_faces[0].index) # only one face
|
||||
# edge.link_faces[0].select = True
|
||||
else: # other loops #
|
||||
# start from the edges of the first face of the last loop
|
||||
for edge in bm.faces[faces_grid[-1][0]].edges:
|
||||
# chose an edge starting from the first vertex that is not returning back
|
||||
if bm.verts[verts_loop[0]] in edge.verts and \
|
||||
bm.verts[verts_grid[-1][0]] not in edge.verts:
|
||||
for vert in edge.verts:
|
||||
if vert.index != id:
|
||||
vert.select = True
|
||||
verts_loop.append(vert.index)
|
||||
edges_loop.append(edge.index)
|
||||
|
||||
for face in edge.link_faces:
|
||||
if not_in(face.index, faces_grid):
|
||||
faces_loop.append(face.index)
|
||||
# continuing the loop
|
||||
else:
|
||||
for edge in link_edges:
|
||||
for vert in edge.verts:
|
||||
store_data = False
|
||||
if not_in(vert.index, verts_grid) and vert.index not in verts_loop:
|
||||
if len(faces_loop) > 0:
|
||||
bm.faces.ensure_lookup_table()
|
||||
if vert not in bm.faces[faces_loop[-1]].verts:
|
||||
store_data = True
|
||||
else:
|
||||
store_data = True
|
||||
if store_data:
|
||||
vert.select = True
|
||||
verts_loop.append(vert.index)
|
||||
edges_loop.append(edge.index)
|
||||
for face in edge.link_faces:
|
||||
if not_in(face.index, faces_grid):
|
||||
faces_loop.append(face.index)
|
||||
break
|
||||
# ending condition
|
||||
if verts_loop[-1] == id or verts_loop[-1] == verts_loop[0]:
|
||||
running_loop = False
|
||||
|
||||
verts_grid.append(verts_loop)
|
||||
edges_grid.append(edges_loop)
|
||||
faces_grid.append(faces_loop)
|
||||
|
||||
if len(faces_loop) == 0:
|
||||
running_grid = False
|
||||
|
||||
return verts_grid, edges_grid, faces_grid
|
||||
|
||||
|
||||
class lattice_along_surface(Operator):
|
||||
bl_idname = "object.lattice_along_surface"
|
||||
bl_label = "Lattice along Surface"
|
||||
bl_description = ("Automatically add a Lattice modifier to the selected "
|
||||
"object, adapting it to the active one.\nThe active "
|
||||
"object must be a rectangular grid compatible with the "
|
||||
"Lattice's topology")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
set_parent : BoolProperty(
|
||||
name="Set Parent",
|
||||
default=True,
|
||||
description="Automatically set the Lattice as parent"
|
||||
)
|
||||
flipNormals : BoolProperty(
|
||||
name="Flip Normals",
|
||||
default=False,
|
||||
description="Flip normals direction"
|
||||
)
|
||||
swapUV : BoolProperty(
|
||||
name="Swap UV",
|
||||
default=False,
|
||||
description="Flip grid's U and V"
|
||||
)
|
||||
flipU : BoolProperty(
|
||||
name="Flip U",
|
||||
default=False,
|
||||
description="Flip grid's U")
|
||||
|
||||
flipV : BoolProperty(
|
||||
name="Flip V",
|
||||
default=False,
|
||||
description="Flip grid's V"
|
||||
)
|
||||
flipW : BoolProperty(
|
||||
name="Flip W",
|
||||
default=False,
|
||||
description="Flip grid's W"
|
||||
)
|
||||
use_groups : BoolProperty(
|
||||
name="Vertex Group",
|
||||
default=False,
|
||||
description="Use active Vertex Group for lattice's thickness"
|
||||
)
|
||||
high_quality_lattice : BoolProperty(
|
||||
name="High quality",
|
||||
default=True,
|
||||
description="Increase the the subdivisions in normal direction for a "
|
||||
"more correct result"
|
||||
)
|
||||
hide_lattice : BoolProperty(
|
||||
name="Hide Lattice",
|
||||
default=True,
|
||||
description="Automatically hide the Lattice object"
|
||||
)
|
||||
scale_x : FloatProperty(
|
||||
name="Scale X",
|
||||
default=1,
|
||||
min=0.001,
|
||||
max=1,
|
||||
description="Object scale"
|
||||
)
|
||||
scale_y : FloatProperty(
|
||||
name="Scale Y", default=1,
|
||||
min=0.001,
|
||||
max=1,
|
||||
description="Object scale"
|
||||
)
|
||||
scale_z : FloatProperty(
|
||||
name="Scale Z",
|
||||
default=1,
|
||||
min=0.001,
|
||||
max=1,
|
||||
description="Object scale"
|
||||
)
|
||||
thickness : FloatProperty(
|
||||
name="Thickness",
|
||||
default=1,
|
||||
soft_min=0,
|
||||
soft_max=5,
|
||||
description="Lattice thickness"
|
||||
)
|
||||
displace : FloatProperty(
|
||||
name="Displace",
|
||||
default=0,
|
||||
soft_min=-1,
|
||||
soft_max=1,
|
||||
description="Lattice displace"
|
||||
)
|
||||
grid_object = ""
|
||||
source_object = ""
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
try: return bpy.context.object.mode == 'OBJECT'
|
||||
except: return False
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Thickness:")
|
||||
col.prop(
|
||||
self, "thickness", text="Thickness", icon='NONE', expand=False,
|
||||
slider=True, toggle=False, icon_only=False, event=False,
|
||||
full_event=False, emboss=True, index=-1
|
||||
)
|
||||
col.prop(
|
||||
self, "displace", text="Offset", icon='NONE', expand=False,
|
||||
slider=True, toggle=False, icon_only=False, event=False,
|
||||
full_event=False, emboss=True, index=-1
|
||||
)
|
||||
row = col.row()
|
||||
row.prop(self, "use_groups")
|
||||
col.separator()
|
||||
col.label(text="Scale:")
|
||||
col.prop(
|
||||
self, "scale_x", text="U", icon='NONE', expand=False,
|
||||
slider=True, toggle=False, icon_only=False, event=False,
|
||||
full_event=False, emboss=True, index=-1
|
||||
)
|
||||
col.prop(
|
||||
self, "scale_y", text="V", icon='NONE', expand=False,
|
||||
slider=True, toggle=False, icon_only=False, event=False,
|
||||
full_event=False, emboss=True, index=-1
|
||||
)
|
||||
col.separator()
|
||||
col.label(text="Flip:")
|
||||
row = col.row()
|
||||
row.prop(self, "flipU", text="U")
|
||||
row.prop(self, "flipV", text="V")
|
||||
row.prop(self, "flipW", text="W")
|
||||
col.prop(self, "swapUV")
|
||||
col.prop(self, "flipNormals")
|
||||
col.separator()
|
||||
col.label(text="Lattice Options:")
|
||||
col.prop(self, "high_quality_lattice")
|
||||
col.prop(self, "hide_lattice")
|
||||
col.prop(self, "set_parent")
|
||||
|
||||
def execute(self, context):
|
||||
if self.source_object == self.grid_object == "" or True:
|
||||
if len(bpy.context.selected_objects) != 2:
|
||||
self.report({'ERROR'}, "Please, select two objects")
|
||||
return {'CANCELLED'}
|
||||
grid_obj = bpy.context.object
|
||||
if grid_obj.type not in ('MESH', 'CURVE', 'SURFACE'):
|
||||
self.report({'ERROR'}, "The surface object is not valid. Only Mesh,"
|
||||
"Curve and Surface objects are allowed.")
|
||||
return {'CANCELLED'}
|
||||
obj = None
|
||||
for o in bpy.context.selected_objects:
|
||||
if o.name != grid_obj.name and o.type in \
|
||||
('MESH', 'CURVE', 'SURFACE', 'FONT'):
|
||||
obj = o
|
||||
o.select_set(False)
|
||||
break
|
||||
try:
|
||||
obj_dim = obj.dimensions
|
||||
obj_me = simple_to_mesh(obj)#obj.to_mesh(bpy.context.depsgraph, apply_modifiers=True)
|
||||
except:
|
||||
self.report({'ERROR'}, "The object to deform is not valid. Only "
|
||||
"Mesh, Curve, Surface and Font objects are allowed.")
|
||||
return {'CANCELLED'}
|
||||
self.grid_object = grid_obj.name
|
||||
self.source_object = obj.name
|
||||
else:
|
||||
grid_obj = bpy.data.objects[self.grid_object]
|
||||
obj = bpy.data.objects[self.source_object]
|
||||
obj_me = simple_to_mesh(obj)# obj.to_mesh(bpy.context.depsgraph, apply_modifiers=True)
|
||||
for o in bpy.context.selected_objects: o.select_set(False)
|
||||
grid_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = grid_obj
|
||||
|
||||
temp_grid_obj = grid_obj.copy()
|
||||
temp_grid_obj.data = simple_to_mesh(grid_obj)
|
||||
grid_mesh = temp_grid_obj.data
|
||||
for v in grid_mesh.vertices:
|
||||
v.co = grid_obj.matrix_world @ v.co
|
||||
grid_mesh.calc_normals()
|
||||
|
||||
if len(grid_mesh.polygons) > 64 * 64:
|
||||
bpy.data.objects.remove(temp_grid_obj)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# CREATING LATTICE
|
||||
min = Vector((0, 0, 0))
|
||||
max = Vector((0, 0, 0))
|
||||
first = True
|
||||
for v in obj_me.vertices:
|
||||
v0 = v.co.copy()
|
||||
vert = obj.matrix_world @ v0
|
||||
if vert[0] < min[0] or first:
|
||||
min[0] = vert[0]
|
||||
if vert[1] < min[1] or first:
|
||||
min[1] = vert[1]
|
||||
if vert[2] < min[2] or first:
|
||||
min[2] = vert[2]
|
||||
if vert[0] > max[0] or first:
|
||||
max[0] = vert[0]
|
||||
if vert[1] > max[1] or first:
|
||||
max[1] = vert[1]
|
||||
if vert[2] > max[2] or first:
|
||||
max[2] = vert[2]
|
||||
first = False
|
||||
|
||||
bb = max - min
|
||||
lattice_loc = (max + min) / 2
|
||||
bpy.ops.object.add(type='LATTICE')
|
||||
lattice = bpy.context.active_object
|
||||
lattice.location = lattice_loc
|
||||
lattice.scale = Vector((bb.x / self.scale_x, bb.y / self.scale_y,
|
||||
bb.z / self.scale_z))
|
||||
|
||||
if bb.x == 0:
|
||||
lattice.scale.x = 1
|
||||
if bb.y == 0:
|
||||
lattice.scale.y = 1
|
||||
if bb.z == 0:
|
||||
lattice.scale.z = 1
|
||||
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.modifier_add(type='LATTICE')
|
||||
obj.modifiers[-1].object = lattice
|
||||
|
||||
# set as parent
|
||||
if self.set_parent:
|
||||
obj.select_set(True)
|
||||
lattice.select_set(True)
|
||||
bpy.context.view_layer.objects.active = lattice
|
||||
bpy.ops.object.parent_set(type='LATTICE')
|
||||
|
||||
# reading grid structure
|
||||
verts_grid, edges_grid, faces_grid = grid_from_mesh(
|
||||
grid_mesh,
|
||||
swap_uv=self.swapUV
|
||||
)
|
||||
nu = len(verts_grid)
|
||||
nv = len(verts_grid[0])
|
||||
nw = 2
|
||||
scale_normal = self.thickness
|
||||
|
||||
try:
|
||||
lattice.data.points_u = nu
|
||||
lattice.data.points_v = nv
|
||||
lattice.data.points_w = nw
|
||||
for i in range(nu):
|
||||
for j in range(nv):
|
||||
for w in range(nw):
|
||||
if self.use_groups:
|
||||
try:
|
||||
displace = temp_grid_obj.vertex_groups.active.weight(
|
||||
verts_grid[i][j]) * scale_normal * bb.z
|
||||
except:
|
||||
displace = 0#scale_normal * bb.z
|
||||
else:
|
||||
displace = scale_normal * bb.z
|
||||
target_point = (grid_mesh.vertices[verts_grid[i][j]].co +
|
||||
grid_mesh.vertices[verts_grid[i][j]].normal *
|
||||
(w + self.displace / 2 - 0.5) * displace) - lattice.location
|
||||
if self.flipW:
|
||||
w = 1 - w
|
||||
if self.flipU:
|
||||
i = nu - i - 1
|
||||
if self.flipV:
|
||||
j = nv - j - 1
|
||||
|
||||
lattice.data.points[i + j * nu + w * nu * nv].co_deform.x = \
|
||||
target_point.x / bpy.data.objects[lattice.name].scale.x
|
||||
lattice.data.points[i + j * nu + w * nu * nv].co_deform.y = \
|
||||
target_point.y / bpy.data.objects[lattice.name].scale.y
|
||||
lattice.data.points[i + j * nu + w * nu * nv].co_deform.z = \
|
||||
target_point.z / bpy.data.objects[lattice.name].scale.z
|
||||
|
||||
except:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
temp_grid_obj.select_set(True)
|
||||
lattice.select_set(True)
|
||||
obj.select_set(False)
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
bpy.ops.object.modifier_remove(modifier=obj.modifiers[-1].name)
|
||||
if nu > 64 or nv > 64:
|
||||
self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64")
|
||||
return {'CANCELLED'}
|
||||
else:
|
||||
self.report({'ERROR'}, "The grid mesh is not correct")
|
||||
return {'CANCELLED'}
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
#grid_obj.select_set(True)
|
||||
#lattice.select_set(False)
|
||||
obj.select_set(False)
|
||||
#bpy.ops.object.delete(use_global=False)
|
||||
bpy.context.view_layer.objects.active = lattice
|
||||
lattice.select_set(True)
|
||||
|
||||
if self.high_quality_lattice:
|
||||
bpy.context.object.data.points_w = 8
|
||||
else:
|
||||
bpy.context.object.data.use_outside = True
|
||||
|
||||
if self.hide_lattice:
|
||||
bpy.ops.object.hide_view_set(unselected=False)
|
||||
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
lattice.select_set(False)
|
||||
|
||||
if self.flipNormals:
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.flip_normals()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
except:
|
||||
pass
|
||||
bpy.data.meshes.remove(grid_mesh)
|
||||
bpy.data.meshes.remove(obj_me)
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,54 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# 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 2
|
||||
# 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
|
||||
# MERCHANTABILITY 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, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import numpy as np
|
||||
try:
|
||||
from numba import jit
|
||||
print("Tissue: Numba module loaded succesfully")
|
||||
@jit
|
||||
def numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, diff_a, diff_b, f, k, dt, time_steps):
|
||||
arr = np.arange(n_edges)*2
|
||||
id0 = edge_verts[arr] # first vertex indices for each edge
|
||||
id1 = edge_verts[arr+1] # second vertex indices for each edge
|
||||
for i in range(time_steps):
|
||||
lap_a = np.zeros(n_verts)
|
||||
lap_b = np.zeros(n_verts)
|
||||
lap_a0 = a[id1] - a[id0] # laplacian increment for first vertex of each edge
|
||||
lap_b0 = b[id1] - b[id0] # laplacian increment for first vertex of each edge
|
||||
|
||||
for i, j, la0, lb0 in zip(id0,id1,lap_a0,lap_b0):
|
||||
lap_a[i] += la0
|
||||
lap_b[i] += lb0
|
||||
lap_a[j] -= la0
|
||||
lap_b[j] -= lb0
|
||||
ab2 = a*b**2
|
||||
#a += eval("(diff_a*lap_a - ab2 + f*(1-a))*dt")
|
||||
#b += eval("(diff_b*lap_b + ab2 - (k+f)*b)*dt")
|
||||
a += (diff_a*lap_a - ab2 + f*(1-a))*dt
|
||||
b += (diff_b*lap_b + ab2 - (k+f)*b)*dt
|
||||
return a, b
|
||||
|
||||
@jit
|
||||
def numba_lerp2(v00, v10, v01, v11, vx, vy):
|
||||
co0 = v00 + (v10 - v00) * vx
|
||||
co1 = v01 + (v11 - v01) * vx
|
||||
co2 = co0 + (co1 - co0) * vy
|
||||
return co2
|
||||
except:
|
||||
print("Tissue: Numba not installed")
|
||||
pass
|
||||
@@ -0,0 +1,40 @@
|
||||
# Tissue
|
||||

|
||||
Tissue - Blender's add-on for computational design by Co-de-iT
|
||||
http://www.co-de-it.com/wordpress/code/blender-tissue
|
||||
|
||||
Tissue is already shipped with both Blender 2.79b and Blender 2.80. However both versions can be updated manually, for more updated features and more stability.
|
||||
|
||||
### Blender 2.80
|
||||
|
||||
Tissue v0.3.31 for Blender 2.80 (latest stable release): https://github.com/alessandro-zomparelli/tissue/releases/tag/v0-3-31
|
||||
|
||||
Development branch (most updated version): https://github.com/alessandro-zomparelli/tissue/tree/b280-dev
|
||||
|
||||
### Blender 2.79
|
||||
|
||||
Tissue v0.3.4 for Blender 2.79b (latest stable release): https://github.com/alessandro-zomparelli/tissue/releases/tag/v0-3-4
|
||||
|
||||
Development branch (most updated version): https://github.com/alessandro-zomparelli/tissue/tree/dev1
|
||||
|
||||
|
||||
### Installation:
|
||||
|
||||
1. Start Blender. Open User Preferences, the addons tab
|
||||
2. Search for Tissue add-on and remove existing version
|
||||
3. Click "install from file" and point Blender at the downloaded zip ("Install..." for Blender 2.80)
|
||||
4. Activate Tissue add-on from user preferences
|
||||
5. Save user preferences if you want to have it on at startup. (This could be not necessary for Blender 2.80 if "Auto-Save Preferences" id on)
|
||||
|
||||
### Documentation
|
||||
|
||||
Tissue documentation for Blender 2.80: https://github.com/alessandro-zomparelli/tissue/wiki
|
||||
|
||||
|
||||
### Contribute
|
||||
Please help me keeping Tissue stable and updated, report any issue here: https://github.com/alessandro-zomparelli/tissue/issues
|
||||
|
||||
Tissue is free and open-source. I really think that this is the power of Blender and I wanted to give my small contribution to it.
|
||||
If you like my work and you want to help to continue the development of Tissue, please consider to make a small donation. Any small contribution is really appreciated, thanks! :-D
|
||||
|
||||
Alessandro
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,462 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# 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 2
|
||||
# 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
|
||||
# MERCHANTABILITY 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, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
import bpy
|
||||
import threading
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
from multiprocessing import Process, Pool
|
||||
from mathutils import Vector
|
||||
try: from .numba_functions import numba_lerp2
|
||||
except: pass
|
||||
|
||||
weight = []
|
||||
n_threads = multiprocessing.cpu_count()
|
||||
|
||||
class ThreadVertexGroup(threading.Thread):
|
||||
def __init__ ( self, id, vertex_group, n_verts):
|
||||
self.id = id
|
||||
self.vertex_group = vertex_group
|
||||
self.n_verts = n_verts
|
||||
threading.Thread.__init__ ( self )
|
||||
|
||||
def run (self):
|
||||
global weight
|
||||
global n_threads
|
||||
verts = np.arange(int(self.n_verts/8))*8 + self.id
|
||||
for v in verts:
|
||||
try:
|
||||
weight[v] = self.vertex_group.weight(v)
|
||||
except:
|
||||
pass
|
||||
|
||||
def thread_read_weight(_weight, vertex_group):
|
||||
global weight
|
||||
global n_threads
|
||||
print(n_threads)
|
||||
weight = _weight
|
||||
n_verts = len(weight)
|
||||
threads = [ThreadVertexGroup(i, vertex_group, n_verts) for i in range(n_threads)]
|
||||
for t in threads: t.start()
|
||||
for t in threads: t.join()
|
||||
return weight
|
||||
|
||||
def process_read_weight(id, vertex_group, n_verts):
|
||||
global weight
|
||||
global n_threads
|
||||
verts = np.arange(int(self.n_verts/8))*8 + self.id
|
||||
for v in verts:
|
||||
try:
|
||||
weight[v] = self.vertex_group.weight(v)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def read_weight(_weight, vertex_group):
|
||||
global weight
|
||||
global n_threads
|
||||
print(n_threads)
|
||||
weight = _weight
|
||||
n_verts = len(weight)
|
||||
n_cores = multiprocessing.cpu_count()
|
||||
pool = Pool(processes=n_cores)
|
||||
multiple_results = [pool.apply_async(process_read_weight, (i, vertex_group, n_verts)) for i in range(n_cores)]
|
||||
#processes = [Process(target=process_read_weight, args=(i, vertex_group, n_verts)) for i in range(n_threads)]
|
||||
#for t in processes: t.start()
|
||||
#for t in processes: t.join()
|
||||
return weight
|
||||
|
||||
#Recursivly transverse layer_collection for a particular name
|
||||
def recurLayerCollection(layerColl, collName):
|
||||
found = None
|
||||
if (layerColl.name == collName):
|
||||
return layerColl
|
||||
for layer in layerColl.children:
|
||||
found = recurLayerCollection(layer, collName)
|
||||
if found:
|
||||
return found
|
||||
|
||||
def auto_layer_collection():
|
||||
# automatically change active layer collection
|
||||
layer = bpy.context.view_layer.active_layer_collection
|
||||
layer_collection = bpy.context.view_layer.layer_collection
|
||||
if layer.hide_viewport or layer.collection.hide_viewport:
|
||||
collections = bpy.context.object.users_collection
|
||||
for c in collections:
|
||||
lc = recurLayerCollection(layer_collection, c.name)
|
||||
if not c.hide_viewport and not lc.hide_viewport:
|
||||
bpy.context.view_layer.active_layer_collection = lc
|
||||
|
||||
def lerp(a, b, t):
|
||||
return a + (b - a) * t
|
||||
|
||||
def _lerp2(v1, v2, v3, v4, v):
|
||||
v12 = v1.lerp(v2,v.x) # + (v2 - v1) * v.x
|
||||
v34 = v3.lerp(v4,v.x) # + (v4 - v3) * v.x
|
||||
return v12.lerp(v34, v.y)# + (v34 - v12) * v.y
|
||||
|
||||
def lerp2(v1, v2, v3, v4, v):
|
||||
v12 = v1 + (v2 - v1) * v.x
|
||||
v34 = v3 + (v4 - v3) * v.x
|
||||
return v12 + (v34 - v12) * v.y
|
||||
|
||||
def lerp3(v1, v2, v3, v4, v):
|
||||
loc = lerp2(v1.co, v2.co, v3.co, v4.co, v)
|
||||
nor = lerp2(v1.normal, v2.normal, v3.normal, v4.normal, v)
|
||||
nor.normalize()
|
||||
return loc + nor * v.z
|
||||
|
||||
def np_lerp2(v00, v10, v01, v11, vx, vy):
|
||||
#try:
|
||||
# co2 = numba_lerp2(v00, v10, v01, v11, vx, vy)
|
||||
#except:
|
||||
co0 = v00 + (v10 - v00) * vx
|
||||
co1 = v01 + (v11 - v01) * vx
|
||||
co2 = co0 + (co1 - co0) * vy
|
||||
return co2
|
||||
|
||||
|
||||
# Prevent Blender Crashes with handlers
|
||||
def set_animatable_fix_handler(self, context):
|
||||
old_handlers = []
|
||||
blender_handlers = bpy.app.handlers.render_init
|
||||
for h in blender_handlers:
|
||||
if "turn_off_animatable" in str(h):
|
||||
old_handlers.append(h)
|
||||
for h in old_handlers: blender_handlers.remove(h)
|
||||
################ blender_handlers.append(turn_off_animatable)
|
||||
return
|
||||
|
||||
def turn_off_animatable(scene):
|
||||
for o in bpy.data.objects:
|
||||
o.tissue_tessellate.bool_run = False
|
||||
o.reaction_diffusion_settings.run = False
|
||||
#except: pass
|
||||
return
|
||||
|
||||
### OBJECTS ###
|
||||
|
||||
def convert_object_to_mesh(ob, apply_modifiers=True, preserve_status=True):
|
||||
try: ob.name
|
||||
except: return None
|
||||
if ob.type != 'MESH':
|
||||
if not apply_modifiers:
|
||||
mod_visibility = [m.show_viewport for m in ob.modifiers]
|
||||
for m in ob.modifiers: m.show_viewport = False
|
||||
#ob.modifiers.update()
|
||||
#dg = bpy.context.evaluated_depsgraph_get()
|
||||
#ob_eval = ob.evaluated_get(dg)
|
||||
#me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)
|
||||
me = simple_to_mesh(ob)
|
||||
new_ob = bpy.data.objects.new(ob.data.name, me)
|
||||
new_ob.location, new_ob.matrix_world = ob.location, ob.matrix_world
|
||||
if not apply_modifiers:
|
||||
for m,vis in zip(ob.modifiers,mod_visibility): m.show_viewport = vis
|
||||
else:
|
||||
if apply_modifiers:
|
||||
new_ob = ob.copy()
|
||||
new_me = simple_to_mesh(ob)
|
||||
new_ob.modifiers.clear()
|
||||
new_ob.data = new_me
|
||||
else:
|
||||
new_ob = ob.copy()
|
||||
new_ob.data = ob.data.copy()
|
||||
new_ob.modifiers.clear()
|
||||
bpy.context.collection.objects.link(new_ob)
|
||||
if preserve_status:
|
||||
new_ob.select_set(False)
|
||||
else:
|
||||
for o in bpy.context.view_layer.objects: o.select_set(False)
|
||||
new_ob.select_set(True)
|
||||
bpy.context.view_layer.objects.active = new_ob
|
||||
return new_ob
|
||||
|
||||
def simple_to_mesh(ob):
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
ob_eval = ob.evaluated_get(dg)
|
||||
me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)
|
||||
me.calc_normals()
|
||||
return me
|
||||
|
||||
def join_objects(objects, link_to_scene=True, make_active=False):
|
||||
C = bpy.context
|
||||
bm = bmesh.new()
|
||||
|
||||
materials = {}
|
||||
faces_materials = []
|
||||
dg = C.evaluated_depsgraph_get()
|
||||
for o in objects:
|
||||
bm.from_object(o, dg)
|
||||
# add object's material to the dictionary
|
||||
for m in o.data.materials:
|
||||
if m not in materials: materials[m] = len(materials)
|
||||
for f in o.data.polygons:
|
||||
index = f.material_index
|
||||
mat = o.material_slots[index].material
|
||||
new_index = materials[mat]
|
||||
faces_materials.append(new_index)
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
# assign new indexes
|
||||
for index, f in zip(faces_materials, bm.faces): f.material_index = index
|
||||
# create object
|
||||
me = bpy.data.meshes.new('joined')
|
||||
bm.to_mesh(me)
|
||||
me.update()
|
||||
ob = bpy.data.objects.new('joined', me)
|
||||
if link_to_scene: C.collection.objects.link(ob)
|
||||
# make active
|
||||
if make_active:
|
||||
for o in C.view_layer.objects: o.select_set(False)
|
||||
ob.select_set(True)
|
||||
C.view_layer.objects.active = ob
|
||||
# add materials
|
||||
for m in materials.keys(): ob.data.materials.append(m)
|
||||
return ob
|
||||
|
||||
### MESH FUNCTIONS
|
||||
|
||||
def get_vertices_numpy(mesh):
|
||||
n_verts = len(mesh.vertices)
|
||||
verts = [0]*n_verts*3
|
||||
mesh.vertices.foreach_get('co', verts)
|
||||
verts = np.array(verts).reshape((n_verts,3))
|
||||
return verts
|
||||
|
||||
def get_vertices_and_normals_numpy(mesh):
|
||||
n_verts = len(mesh.vertices)
|
||||
verts = [0]*n_verts*3
|
||||
normals = [0]*n_verts*3
|
||||
mesh.vertices.foreach_get('co', verts)
|
||||
mesh.vertices.foreach_get('normal', normals)
|
||||
verts = np.array(verts).reshape((n_verts,3))
|
||||
normals = np.array(normals).reshape((n_verts,3))
|
||||
return verts, normals
|
||||
|
||||
def get_edges_numpy(mesh):
|
||||
n_edges = len(mesh.edges)
|
||||
edges = [0]*n_edges*2
|
||||
mesh.edges.foreach_get('vertices', edges)
|
||||
edges = np.array(edges).reshape((n_edges,2)).astype('int')
|
||||
return edges
|
||||
|
||||
def get_edges_id_numpy(mesh):
|
||||
n_edges = len(mesh.edges)
|
||||
edges = [0]*n_edges*2
|
||||
mesh.edges.foreach_get('vertices', edges)
|
||||
edges = np.array(edges).reshape((n_edges,2))
|
||||
indexes = np.arange(n_edges).reshape((n_edges,1))
|
||||
edges = np.concatenate((edges,indexes), axis=1)
|
||||
return edges
|
||||
|
||||
def get_vertices(mesh):
|
||||
n_verts = len(mesh.vertices)
|
||||
verts = [0]*n_verts*3
|
||||
mesh.vertices.foreach_get('co', verts)
|
||||
verts = np.array(verts).reshape((n_verts,3))
|
||||
verts = [Vector(v) for v in verts]
|
||||
return verts
|
||||
|
||||
def get_faces(mesh):
|
||||
faces = [[v for v in f.vertices] for f in mesh.polygons]
|
||||
return faces
|
||||
|
||||
def get_faces_numpy(mesh):
|
||||
faces = [[v for v in f.vertices] for f in mesh.polygons]
|
||||
return np.array(faces)
|
||||
|
||||
def get_faces_edges_numpy(mesh):
|
||||
faces = [v.edge_keys for f in mesh.polygons]
|
||||
return np.array(faces)
|
||||
|
||||
#try:
|
||||
#from numba import jit, njit
|
||||
#from numba.typed import List
|
||||
'''
|
||||
@jit
|
||||
def find_curves(edges, n_verts):
|
||||
#verts_dict = {key:[] for key in range(n_verts)}
|
||||
verts_dict = {}
|
||||
for key in range(n_verts): verts_dict[key] = []
|
||||
for e in edges:
|
||||
verts_dict[e[0]].append(e[1])
|
||||
verts_dict[e[1]].append(e[0])
|
||||
curves = []#List()
|
||||
loop1 = True
|
||||
while loop1:
|
||||
if len(verts_dict) == 0:
|
||||
loop1 = False
|
||||
continue
|
||||
# next starting point
|
||||
v = list(verts_dict.keys())[0]
|
||||
# neighbors
|
||||
v01 = verts_dict[v]
|
||||
if len(v01) == 0:
|
||||
verts_dict.pop(v)
|
||||
continue
|
||||
curve = []#List()
|
||||
curve.append(v) # add starting point
|
||||
curve.append(v01[0]) # add neighbors
|
||||
verts_dict.pop(v)
|
||||
loop2 = True
|
||||
while loop2:
|
||||
last_point = curve[-1]
|
||||
#if last_point not in verts_dict: break
|
||||
v01 = verts_dict[last_point]
|
||||
# curve end
|
||||
if len(v01) == 1:
|
||||
verts_dict.pop(last_point)
|
||||
loop2 = False
|
||||
continue
|
||||
if v01[0] == curve[-2]:
|
||||
curve.append(v01[1])
|
||||
verts_dict.pop(last_point)
|
||||
elif v01[1] == curve[-2]:
|
||||
curve.append(v01[0])
|
||||
verts_dict.pop(last_point)
|
||||
else:
|
||||
loop2 = False
|
||||
continue
|
||||
if curve[0] == curve[-1]:
|
||||
loop2 = False
|
||||
continue
|
||||
curves.append(curve)
|
||||
return curves
|
||||
'''
|
||||
def find_curves(edges, n_verts):
|
||||
verts_dict = {key:[] for key in range(n_verts)}
|
||||
for e in edges:
|
||||
verts_dict[e[0]].append(e[1])
|
||||
verts_dict[e[1]].append(e[0])
|
||||
curves = []
|
||||
while True:
|
||||
if len(verts_dict) == 0: break
|
||||
# next starting point
|
||||
v = list(verts_dict.keys())[0]
|
||||
# neighbors
|
||||
v01 = verts_dict[v]
|
||||
if len(v01) == 0:
|
||||
verts_dict.pop(v)
|
||||
continue
|
||||
curve = []
|
||||
if len(v01) > 1: curve.append(v01[1]) # add neighbors
|
||||
curve.append(v) # add starting point
|
||||
curve.append(v01[0]) # add neighbors
|
||||
verts_dict.pop(v)
|
||||
# start building curve
|
||||
while True:
|
||||
#last_point = curve[-1]
|
||||
#if last_point not in verts_dict: break
|
||||
|
||||
# try to change direction if needed
|
||||
if curve[-1] in verts_dict: pass
|
||||
elif curve[0] in verts_dict: curve.reverse()
|
||||
else: break
|
||||
|
||||
# neighbors points
|
||||
last_point = curve[-1]
|
||||
v01 = verts_dict[last_point]
|
||||
|
||||
# curve end
|
||||
if len(v01) == 1:
|
||||
verts_dict.pop(last_point)
|
||||
if curve[0] in verts_dict: continue
|
||||
else: break
|
||||
|
||||
# chose next point
|
||||
new_point = None
|
||||
if v01[0] == curve[-2]: new_point = v01[1]
|
||||
elif v01[1] == curve[-2]: new_point = v01[0]
|
||||
#else: break
|
||||
|
||||
#if new_point != curve[1]:
|
||||
curve.append(new_point)
|
||||
verts_dict.pop(last_point)
|
||||
if curve[0] == curve[-1]:
|
||||
verts_dict.pop(new_point)
|
||||
break
|
||||
curves.append(curve)
|
||||
return curves
|
||||
|
||||
def curve_from_points(points, name='Curve'):
|
||||
curve = bpy.data.curves.new(name,'CURVE')
|
||||
for c in points:
|
||||
s = curve.splines.new('POLY')
|
||||
s.points.add(len(c))
|
||||
for i,p in enumerate(c): s.points[i].co = p.xyz + [1]
|
||||
ob_curve = bpy.data.objects.new(name,curve)
|
||||
return ob_curve
|
||||
|
||||
def curve_from_pydata(points, indexes, name='Curve', skip_open=False, merge_distance=1, set_active=True):
|
||||
curve = bpy.data.curves.new(name,'CURVE')
|
||||
curve.dimensions = '3D'
|
||||
for c in indexes:
|
||||
# cleanup
|
||||
pts = np.array([points[i] for i in c])
|
||||
if merge_distance > 0:
|
||||
pts1 = np.roll(pts,1,axis=0)
|
||||
dist = np.linalg.norm(pts1-pts, axis=1)
|
||||
count = 0
|
||||
n = len(dist)
|
||||
mask = np.ones(n).astype('bool')
|
||||
for i in range(n):
|
||||
count += dist[i]
|
||||
if count > merge_distance: count = 0
|
||||
else: mask[i] = False
|
||||
pts = pts[mask]
|
||||
|
||||
bool_cyclic = c[0] == c[-1]
|
||||
if skip_open and not bool_cyclic: continue
|
||||
s = curve.splines.new('POLY')
|
||||
n_pts = len(pts)
|
||||
s.points.add(n_pts-1)
|
||||
w = np.ones(n_pts).reshape((n_pts,1))
|
||||
co = np.concatenate((pts,w),axis=1).reshape((n_pts*4))
|
||||
s.points.foreach_set('co',co)
|
||||
s.use_cyclic_u = bool_cyclic
|
||||
ob_curve = bpy.data.objects.new(name,curve)
|
||||
bpy.context.collection.objects.link(ob_curve)
|
||||
if set_active:
|
||||
bpy.context.view_layer.objects.active = ob_curve
|
||||
return ob_curve
|
||||
|
||||
def curve_from_vertices(indexes, verts, name='Curve'):
|
||||
curve = bpy.data.curves.new(name,'CURVE')
|
||||
for c in indexes:
|
||||
s = curve.splines.new('POLY')
|
||||
s.points.add(len(c))
|
||||
for i,p in enumerate(c): s.points[i].co = verts[p].co.xyz + [1]
|
||||
ob_curve = bpy.data.objects.new(name,curve)
|
||||
return ob_curve
|
||||
|
||||
### WEIGHT FUNCTIONS ###
|
||||
|
||||
def get_weight(vertex_group, n_verts):
|
||||
weight = [0]*n_verts
|
||||
for i in range(n_verts):
|
||||
try: weight[i] = vertex_group.weight(i)
|
||||
except: pass
|
||||
return weight
|
||||
|
||||
def get_weight_numpy(vertex_group, n_verts):
|
||||
weight = [0]*n_verts
|
||||
for i in range(n_verts):
|
||||
try: weight[i] = vertex_group.weight(i)
|
||||
except: pass
|
||||
return np.array(weight)
|
||||
@@ -0,0 +1,178 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# 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 2
|
||||
# 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
|
||||
# MERCHANTABILITY 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, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# --------------------------------- UV to MESH ------------------------------- #
|
||||
# -------------------------------- version 0.1.1 ----------------------------- #
|
||||
# #
|
||||
# Create a new Mesh based on active UV #
|
||||
# #
|
||||
# (c) Alessandro Zomparelli #
|
||||
# (2017) #
|
||||
# #
|
||||
# http://www.co-de-it.com/ #
|
||||
# #
|
||||
# ############################################################################ #
|
||||
|
||||
import bpy
|
||||
import math
|
||||
from bpy.types import Operator
|
||||
from bpy.props import BoolProperty
|
||||
from mathutils import Vector
|
||||
from .utils import *
|
||||
|
||||
|
||||
class uv_to_mesh(Operator):
|
||||
bl_idname = "object.uv_to_mesh"
|
||||
bl_label = "UV to Mesh"
|
||||
bl_description = ("Create a new Mesh based on active UV")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
apply_modifiers : BoolProperty(
|
||||
name="Apply Modifiers",
|
||||
default=True,
|
||||
description="Apply object's modifiers"
|
||||
)
|
||||
vertex_groups : BoolProperty(
|
||||
name="Keep Vertex Groups",
|
||||
default=True,
|
||||
description="Transfer all the Vertex Groups"
|
||||
)
|
||||
materials : BoolProperty(
|
||||
name="Keep Materials",
|
||||
default=True,
|
||||
description="Transfer all the Materials"
|
||||
)
|
||||
auto_scale : BoolProperty(
|
||||
name="Resize",
|
||||
default=True,
|
||||
description="Scale the new object in order to preserve the average surface area"
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
ob0 = bpy.context.object
|
||||
for o in bpy.context.view_layer.objects: o.select_set(False)
|
||||
ob0.select_set(True)
|
||||
|
||||
#if self.apply_modifiers:
|
||||
# bpy.ops.object.duplicate_move()
|
||||
# bpy.ops.object.convert(target='MESH')
|
||||
|
||||
# me0 = ob0.to_mesh(bpy.context.depsgraph, apply_modifiers=self.apply_modifiers)
|
||||
#if self.apply_modifiers: me0 = simple_to_mesh(ob0)
|
||||
#else: me0 = ob0.data.copy()
|
||||
name0 = ob0.name
|
||||
ob0 = convert_object_to_mesh(ob0, apply_modifiers=self.apply_modifiers, preserve_status=False)
|
||||
me0 = ob0.data
|
||||
area = 0
|
||||
|
||||
verts = []
|
||||
faces = []
|
||||
face_materials = []
|
||||
for face in me0.polygons:
|
||||
area += face.area
|
||||
uv_face = []
|
||||
store = False
|
||||
try:
|
||||
for loop in face.loop_indices:
|
||||
uv = me0.uv_layers.active.data[loop].uv
|
||||
if uv.x != 0 and uv.y != 0:
|
||||
store = True
|
||||
new_vert = Vector((uv.x, uv.y, 0))
|
||||
verts.append(new_vert)
|
||||
uv_face.append(loop)
|
||||
if store:
|
||||
faces.append(uv_face)
|
||||
face_materials.append(face.material_index)
|
||||
except:
|
||||
self.report({'ERROR'}, "Missing UV Map")
|
||||
|
||||
return {'CANCELLED'}
|
||||
|
||||
name = name0 + '_UV'
|
||||
# Create mesh and object
|
||||
me = bpy.data.meshes.new(name + 'Mesh')
|
||||
ob = bpy.data.objects.new(name, me)
|
||||
|
||||
# Link object to scene and make active
|
||||
scn = bpy.context.scene
|
||||
bpy.context.collection.objects.link(ob)
|
||||
bpy.context.view_layer.objects.active = ob
|
||||
ob.select_set(True)
|
||||
|
||||
# Create mesh from given verts, faces.
|
||||
me.from_pydata(verts, [], faces)
|
||||
# Update mesh with new data
|
||||
me.update()
|
||||
if self.auto_scale:
|
||||
new_area = 0
|
||||
for p in me.polygons:
|
||||
new_area += p.area
|
||||
if new_area == 0:
|
||||
self.report({'ERROR'}, "Impossible to generate mesh from UV")
|
||||
bpy.data.objects.remove(ob0)
|
||||
|
||||
return {'CANCELLED'}
|
||||
|
||||
# VERTEX GROUPS
|
||||
if self.vertex_groups:
|
||||
for group in ob0.vertex_groups:
|
||||
index = group.index
|
||||
ob.vertex_groups.new(name=group.name)
|
||||
for p in me0.polygons:
|
||||
for vert, loop in zip(p.vertices, p.loop_indices):
|
||||
try:
|
||||
ob.vertex_groups[index].add([loop], group.weight(vert), 'REPLACE')
|
||||
except:
|
||||
pass
|
||||
|
||||
ob0.select_set(False)
|
||||
if self.auto_scale:
|
||||
scaleFactor = math.pow(area / new_area, 1 / 2)
|
||||
ob.scale = Vector((scaleFactor, scaleFactor, scaleFactor))
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
||||
bpy.ops.mesh.remove_doubles(threshold=1e-06)
|
||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
|
||||
# MATERIALS
|
||||
if self.materials:
|
||||
try:
|
||||
# assign old material
|
||||
uv_materials = [slot.material for slot in ob0.material_slots]
|
||||
for i in range(len(uv_materials)):
|
||||
bpy.ops.object.material_slot_add()
|
||||
bpy.context.object.material_slots[i].material = uv_materials[i]
|
||||
for i in range(len(ob.data.polygons)):
|
||||
ob.data.polygons[i].material_index = face_materials[i]
|
||||
except:
|
||||
pass
|
||||
'''
|
||||
if self.apply_modifiers:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
ob.select_set(False)
|
||||
ob0.select_set(True)
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
ob.select_set(True)
|
||||
bpy.context.view_layer.objects.active = ob
|
||||
'''
|
||||
|
||||
bpy.data.objects.remove(ob0)
|
||||
bpy.data.meshes.remove(me0)
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,151 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# 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 2
|
||||
# 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
|
||||
# MERCHANTABILITY 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, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# --------------------------------- TISSUE ----------------------------------- #
|
||||
# ------------------------------- version 0.3 -------------------------------- #
|
||||
# #
|
||||
# Creates duplicates of selected mesh to active morphing the shape according #
|
||||
# to target faces. #
|
||||
# #
|
||||
# Alessandro Zomparelli #
|
||||
# (2017) #
|
||||
# #
|
||||
# http://www.co-de-it.com/ #
|
||||
# http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Mesh/Tissue #
|
||||
# #
|
||||
# ############################################################################ #
|
||||
|
||||
bl_info = {
|
||||
"name": "Tissue",
|
||||
"author": "Alessandro Zomparelli (Co-de-iT)",
|
||||
"version": (0, 3, 34),
|
||||
"blender": (2, 80, 0),
|
||||
"location": "",
|
||||
"description": "Tools for Computational Design",
|
||||
"warning": "",
|
||||
"wiki_url": "https://github.com/alessandro-zomparelli/tissue/wiki",
|
||||
"tracker_url": "https://github.com/alessandro-zomparelli/tissue/issues",
|
||||
"category": "Mesh"}
|
||||
|
||||
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
importlib.reload(tessellate_numpy)
|
||||
importlib.reload(colors_groups_exchanger)
|
||||
importlib.reload(dual_mesh)
|
||||
importlib.reload(lattice)
|
||||
importlib.reload(uv_to_mesh)
|
||||
importlib.reload(utils)
|
||||
importlib.reload(gcode_export)
|
||||
|
||||
else:
|
||||
from . import tessellate_numpy
|
||||
from . import colors_groups_exchanger
|
||||
from . import dual_mesh
|
||||
from . import lattice
|
||||
from . import uv_to_mesh
|
||||
from . import utils
|
||||
from . import gcode_export
|
||||
|
||||
import bpy
|
||||
from bpy.props import PointerProperty, CollectionProperty, BoolProperty
|
||||
|
||||
classes = (
|
||||
tessellate_numpy.tissue_tessellate_prop,
|
||||
tessellate_numpy.tissue_tessellate,
|
||||
tessellate_numpy.tissue_update_tessellate,
|
||||
tessellate_numpy.tissue_refresh_tessellate,
|
||||
tessellate_numpy.TISSUE_PT_tessellate,
|
||||
tessellate_numpy.tissue_rotate_face_left,
|
||||
tessellate_numpy.tissue_rotate_face_right,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_object,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_frame,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_thickness,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_coordinates,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_rotation,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_options,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_selective,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_morphing,
|
||||
tessellate_numpy.TISSUE_PT_tessellate_iterations,
|
||||
|
||||
colors_groups_exchanger.face_area_to_vertex_groups,
|
||||
colors_groups_exchanger.vertex_colors_to_vertex_groups,
|
||||
colors_groups_exchanger.vertex_group_to_vertex_colors,
|
||||
colors_groups_exchanger.TISSUE_PT_weight,
|
||||
colors_groups_exchanger.TISSUE_PT_color,
|
||||
colors_groups_exchanger.weight_contour_curves,
|
||||
colors_groups_exchanger.tissue_weight_contour_curves_pattern,
|
||||
colors_groups_exchanger.weight_contour_mask,
|
||||
colors_groups_exchanger.weight_contour_displace,
|
||||
colors_groups_exchanger.harmonic_weight,
|
||||
colors_groups_exchanger.edges_deformation,
|
||||
colors_groups_exchanger.edges_bending,
|
||||
colors_groups_exchanger.weight_laplacian,
|
||||
colors_groups_exchanger.reaction_diffusion,
|
||||
colors_groups_exchanger.start_reaction_diffusion,
|
||||
colors_groups_exchanger.TISSUE_PT_reaction_diffusion,
|
||||
colors_groups_exchanger.reset_reaction_diffusion_weight,
|
||||
colors_groups_exchanger.formula_prop,
|
||||
colors_groups_exchanger.reaction_diffusion_prop,
|
||||
colors_groups_exchanger.weight_formula,
|
||||
colors_groups_exchanger.curvature_to_vertex_groups,
|
||||
colors_groups_exchanger.weight_formula_wiki,
|
||||
colors_groups_exchanger.tissue_weight_distance,
|
||||
|
||||
dual_mesh.dual_mesh,
|
||||
dual_mesh.dual_mesh_tessellated,
|
||||
|
||||
lattice.lattice_along_surface,
|
||||
|
||||
uv_to_mesh.uv_to_mesh,
|
||||
gcode_export.TISSUE_PT_gcode_exporter,
|
||||
gcode_export.tissue_gcode_prop,
|
||||
gcode_export.tissue_gcode_export
|
||||
)
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
#bpy.utils.register_module(__name__)
|
||||
bpy.types.Object.tissue_tessellate = PointerProperty(
|
||||
type=tessellate_numpy.tissue_tessellate_prop
|
||||
)
|
||||
bpy.types.Scene.tissue_gcode = PointerProperty(
|
||||
type=gcode_export.tissue_gcode_prop
|
||||
)
|
||||
bpy.types.Object.formula_settings = CollectionProperty(
|
||||
type=colors_groups_exchanger.formula_prop
|
||||
)
|
||||
bpy.types.Object.reaction_diffusion_settings = PointerProperty(
|
||||
type=colors_groups_exchanger.reaction_diffusion_prop
|
||||
)
|
||||
# colors_groups_exchanger
|
||||
bpy.app.handlers.frame_change_post.append(colors_groups_exchanger.reaction_diffusion_def)
|
||||
#bpy.app.handlers.frame_change_post.append(tessellate_numpy.anim_tessellate)
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
del bpy.types.Object.tissue_tessellate
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user