Skip to content

Commit

Permalink
Merge pull request #45 from Griperis/various-modifications
Browse files Browse the repository at this point in the history
Improvements, modifications, bugfixes
  • Loading branch information
Griperis committed Feb 3, 2022
2 parents b3b13a6 + 3a55a34 commit f9e145e
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 127 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ Currently supported features:
- Basic animations from data
- Panel settings (to prevent sidebar cluster)
- Loaded data list
- Label alignment to active camera
- Data inspector
- Label alignment to active camera or active view
- Chart container size
- Example data available in addon

Known issues:
- Charts from larger files (>200 entries) take long time to generate (except surface chart), because of large numbers of manipulations with objects instead of meshes
Expand All @@ -115,4 +117,4 @@ Zdeněk Doležal - Bachelor Thesis

Faculty of information technology BUT

Version 2.0
Version 2.1
99 changes: 17 additions & 82 deletions data_vis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
'author': 'Zdenek Dolezal',
'description': 'Data visualisation addon',
'blender': (2, 80, 0),
'version': (2, 0, 0),
'version': (2, 1, 0),
'location': 'Object -> Add Mesh',
'warning': '',
'category': 'Generic'
Expand All @@ -17,21 +17,20 @@
import bpy
import bpy.utils.previews
import os
import subprocess
import sys

from .operators.bar_chart import OBJECT_OT_BarChart
from .operators.line_chart import OBJECT_OT_LineChart
from .operators.pie_chart import OBJECT_OT_PieChart
from .operators.point_chart import OBJECT_OT_PointChart
from .operators.surface_chart import OBJECT_OT_SurfaceChart
from .operators.bubble_chart import OBJECT_OT_BubbleChart
from .operators.label_align import OBJECT_OT_AlignLabels
from .operators.label_align import DV_AlignLabels
from .properties import DV_AnimationPropertyGroup, DV_AxisPropertyGroup, DV_ColorPropertyGroup, DV_HeaderPropertyGroup, DV_LabelPropertyGroup, DV_LegendPropertyGroup, DV_GeneralPropertyGroup
from .data_manager import DataManager
from .docs import get_example_data_doc, draw_tooltip_button
from .icon_manager import IconManager
from .general import DV_ShowPopup, DV_DataInspect
from .utils import env_utils

icon_manager = IconManager()
data_manager = DataManager()
Expand All @@ -41,63 +40,6 @@
EXAMPLE_DATA_FOLDER = 'example_data'


class OBJECT_OT_InstallModules(bpy.types.Operator):
'''Operator that tries to install scipy and numpy using pip into blender python'''
bl_label = 'Install addon dependencies'
bl_idname = 'object.install_modules'
bl_options = {'REGISTER'}

def execute(self, context):
version = '{}.{}'.format(bpy.app.version[0], bpy.app.version[1])

python_path = os.path.join(os.getcwd(), version, 'python', 'bin', 'python')
try:
self.install(python_path)
except Exception as e:
self.report({'ERROR'}, 'Error ocurred, try to install dependencies manually. \n Exception: {}'.format(str(e)))
return {'FINISHED'}

def install(self, python_path):
import platform

info = ''
bp_pip = -1
bp_res = -1

p_pip = -1
p_res = -1

p3_pip = -1
p3_res = -1
try:
bp_pip = subprocess.check_call([python_path, '-m', 'ensurepip', '--user'])
bp_res = subprocess.check_call([python_path, '-m', 'pip', 'install', '--user', 'scipy'])
except OSError as e:
info = 'Python in blender folder failed: ' + str(e) + '\n'

if bp_pip != 0 or bp_res != 0:
if platform.system() == 'Linux':
try:
p_pip = subprocess.check_call(['python', '-m', 'ensurepip', '--user'])
p_res = subprocess.check_call(['python', '-m', 'pip', 'install', '--user', 'scipy'])
except OSError as e:
info += 'Python in PATH failed: ' + str(e) + '\n'

if p_pip != 0 or p_res != 0:
try:
# python3
p3_pip = subprocess.check_call(['python3', '-m', 'ensurepip', '--user'])
p3_res = subprocess.check_call(['python3', '-m', 'pip', 'install', '--user', 'scipy'])
except OSError as e:
info += 'Python3 in PATH failed: ' + str(e) + '\n'

# if one approach worked
if (bp_pip == 0 and bp_res == 0) or (p_pip == 0 and p_res == 0) or (p3_pip == 0 and p3_res == 0):
self.report({'INFO'}, 'Scipy module should be succesfully installed, restart Blender now please! (Best effort approach)')
else:
raise Exception('Failed to install pip or scipy into blender python:\n' + str(info))


class FILE_OT_DVLoadFile(bpy.types.Operator):
bl_idname = 'ui.dv_load_data'
bl_label = 'Load New File'
Expand All @@ -122,8 +64,14 @@ def execute(self, context):
if ext != '.csv':
self.report({'WARNING'}, 'Only CSV files are supported!')
return {'CANCELLED'}
line_n = data_manager.load_data(self.filepath)

for i, item in enumerate(context.scene.data_list):
if item.filepath == self.filepath:
context.scene.data_list_index = i
self.report({'WARNING'}, f'File {self.filepath} already loaded!')
return {'CANCELLED'}

line_n = data_manager.load_data(self.filepath)

report_type = {'INFO'}
if line_n == 0:
Expand All @@ -132,6 +80,8 @@ def execute(self, context):
item = context.scene.data_list.add()
_, item.name = os.path.split(self.filepath)
item.filepath = self.filepath

context.scene.data_list_index = len(context.scene.data_list) - 1
self.report(report_type, f'File: {self.filepath}, loaded {line_n} lines!')
return {'FINISHED'}

Expand Down Expand Up @@ -234,7 +184,7 @@ def draw_data_list(self, context, layout):

col = layout.column(align=True)
row = col.row(align=True)
row.operator(FILE_OT_DVLoadFile.bl_idname, text='Load File', icon='ADD')
row.operator(FILE_OT_DVLoadFile.bl_idname, text='Load File', icon='ADD').filepath = ''
row.operator(DV_OT_RemoveData.bl_idname, text='Remove', icon='REMOVE')
col = layout.column(align=True)
row = col.row(align=True)
Expand Down Expand Up @@ -272,7 +222,7 @@ def draw(self, context):
row.scale_y = 2

row = layout.row()
row.operator('object.align_labels', icon='CAMERA_DATA')
row.operator(DV_AlignLabels.bl_idname, icon='CAMERA_DATA')
row.scale_y = 1.5

scn = context.scene
Expand Down Expand Up @@ -397,22 +347,6 @@ def get_example_data(self, context):

def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text='Python dependencies', icon='PLUS')
row = box.row()
row.scale_y = 2.0
try:
import scipy
import numpy
row.label(text='Dependencies already installed...')
except ImportError:
row.operator('object.install_modules')
row = box.row()
version = '{}.{}'.format(bpy.app.version[0], bpy.app.version[1])
row.label(text='Or use pip to install scipy into python which Blender uses!')
row = box.row()
row.label(text='Blender has to be restarted after this process!')

box = layout.box()
box.label(text='Customize position of addon panel', icon='TOOL_SETTINGS')
box.prop(self, 'ui_region_type')
Expand Down Expand Up @@ -478,7 +412,6 @@ def chart_ops(self, context):
DV_Preferences,
DV_ShowPopup,
DV_DataInspect,
OBJECT_OT_InstallModules,
DV_LabelPropertyGroup,
DV_ColorPropertyGroup,
DV_AxisPropertyGroup,
Expand All @@ -498,7 +431,7 @@ def chart_ops(self, context):
OBJECT_OT_LineChart,
OBJECT_OT_SurfaceChart,
OBJECT_OT_BubbleChart,
OBJECT_OT_AlignLabels,
DV_AlignLabels,
FILE_OT_DVLoadFile,
DV_AddonPanel,
]
Expand All @@ -515,6 +448,8 @@ def reload():


def register():
for module in ['scipy', 'numpy']:
env_utils.ensure_python_module(module)

icon_manager.load_icons()
for c in classes:
Expand Down
3 changes: 3 additions & 0 deletions data_vis/data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ def get_raw_data(self):
return self.raw_data

def load_data(self, filepath, delimiter=','):
if not os.path.exists(filepath):
return 0

self.default_state()
self.filepath = filepath
try:
Expand Down
3 changes: 2 additions & 1 deletion data_vis/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
'species_2D_anim.csv': 'changing distribution of species in some area, good for animated [bar chart]',
'survey-result_2D.csv': 'result of user-experience questionaire for data-vis, good for [bar chart]',
'time-data_1_2D.csv': 'datapoints in a timeline - combine with "time-data_2_2D.csv" in a [line chart]',
'x+y-small_3D_anim.csv': 'small example of 3D x+y function, good for [bar chart, buibble chart, point chart]',
'x+y-small_3D.csv': 'small example of 3D x+y function, good for [bar chart, bubble chart, point chart]',
'function-fancy_3D_anim.csv': 'animated 3d function, good for [surface chart, bar chart, point chart]',
'tan_1000-val_3D.csv': 'large example of tan function, best suitable for [surface chart]'
}

Expand Down
60 changes: 42 additions & 18 deletions data_vis/operators/label_align.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,66 @@
import bpy
from mathutils import Vector
from mathutils import Vector, Quaternion
from math import radians


class OBJECT_OT_AlignLabels(bpy.types.Operator):
'''Aligns labels to currently active camera '''
bl_idname = 'object.align_labels'
bl_label = 'Align Labels To Camera'
class DV_AlignLabels(bpy.types.Operator):
bl_idname = 'data_vis.align_labels'
bl_label = 'Align Labels'
bl_options = {'REGISTER', 'UNDO'}
bl_description = 'Aligns labels to currently active camera or to the view'

align_header: bpy.props.BoolProperty(
default=True,
name='Align header'
name='Align Header',
description='If true then header is aligned'
)

align_axis_labels: bpy.props.BoolProperty(
default=True,
name='Align axis labels'
name='Align Axis Labels',
description='If true then axis labels are aligned'
)

align_target: bpy.props.EnumProperty(
name='Align Target',
description='Select target to align labels to',
items=[
('view', 'View', 'Align to the scene 3D view location'),
('camera', 'Camera', 'Align to active camera')
]
)

@classmethod
def poll(cls, context):
return bpy.context.object and bpy.context.scene.camera
return context.object is not None and context.scene.camera is not None

def draw(self, context):
layout = self.layout
layout.prop(self, 'align_header')
layout.prop(self, 'align_axis_labels')
layout.prop(self, 'align_target', text='')

def execute(self, context):
axis_count = 0
is_pie = False
for child in bpy.context.object.children:
if 'Axis_Container_AxisDir.X' in child.name:
self.align_labels('x', child)
self.align_labels(context, 'x', child)
axis_count += 1
elif 'Axis_Container_AxisDir.Y' in child.name:
self.align_labels('y', child)
self.align_labels(context, 'y', child)
axis_count += 1
elif 'Axis_Container_AxisDir.Z' in child.name:
self.align_labels('z', child)
self.align_labels(context, 'z', child)
axis_count += 1

if child.name.startswith('TextPie'):
self.align_labels('to', child)
self.align_labels(context, 'to', child)
is_pie = True
if child.name == 'TextHeader':
if not self.align_header:
continue
self.align_labels('to', child)
self.align_labels(context, 'to', child)

if axis_count in [2, 3] or is_pie:
self.report({'INFO'}, 'Labels aligned!')
Expand All @@ -52,8 +69,11 @@ def execute(self, context):
self.report({'WARNING'}, 'Select valid chart container!')
return {'CANCELLED'}

def align_labels(self, obj_type, obj):
cam_vector = self.get_camera_vector()
def invoke(self, context: bpy.types.Context, event):
return context.window_manager.invoke_props_dialog(self)

def align_labels(self, context, obj_type, obj):
cam_vector = self.get_align_target_vector(context)

if obj_type == 'to':
obj.rotation_euler = cam_vector
Expand All @@ -73,6 +93,10 @@ def align_labels(self, obj_type, obj):
else:
child.rotation_euler = cam_vector

def get_camera_vector(self):
camera = bpy.context.scene.camera
return camera.rotation_euler
def get_align_target_vector(self, context: bpy.types.Context):
if self.align_target == 'camera':
return context.scene.camera.rotation_euler
elif self.align_target == 'view':
return Quaternion.to_euler(context.region_data.view_rotation)
else:
raise ValueError('Invalid align target!')
25 changes: 18 additions & 7 deletions data_vis/operators/line_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,22 @@ def bevel_curve_obj(self):
'''Bevels curve object'''
bpy.ops.object.mode_set(mode='EDIT')
opts = self.bevel_settings['rounded'] if self.rounded == '1' else self.bevel_settings['sharp']
bpy.ops.mesh.bevel(
segments=opts['segments'],
offset=opts['offset'],
offset_type='OFFSET',
profile=opts['profile'],
vertex_only=True
)
# vertex_only argument doesn't exists above 2.83.0
if bpy.app.version > (2, 83, 0):
bpy.ops.mesh.bevel(
segments=opts['segments'],
offset=opts['offset'],
offset_type='OFFSET',
profile=opts['profile'],
)
else:
bpy.ops.mesh.bevel(
segments=opts['segments'],
offset=opts['offset'],
offset_type='OFFSET',
profile=opts['profile'],
vertex_only=True
)
bpy.ops.object.mode_set(mode='OBJECT')

def add_bevel_obj(self):
Expand All @@ -245,6 +254,8 @@ def add_bevel_obj(self):
bevel_obj.parent = self.container_object

bpy.ops.object.convert(target='CURVE')
if hasattr(self.curve_obj.data, 'bevel_mode'):
self.curve_obj.data.bevel_mode = 'OBJECT'
self.curve_obj.data.bevel_object = bevel_obj
return bevel_obj

Expand Down
Loading

0 comments on commit f9e145e

Please sign in to comment.