diff --git a/README.md b/README.md index 5db3da9..9ef4e8a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,47 @@ -# PlotRock -3D Graphing Addon for Blender +# PlotRock - 3D Graphing Addon for Blender + +Plotrock is a [Blender](http://www.blender.org) plugin that allows users to create line graphs, +using data from their tables and spreadsheets. ## Features * Import files from Excel, LibreOffice, Google Sheets, or other similar spreadsheet programs. -* Ability to edit CSV within Blender and regenerate plot. +* Ability to edit .csv file within Blender and regenerate plot. +* Handy sidemenu that allows plot customization. * Utilize all Blender tools such as materials, lighting, and animated cameras. +https://user-images.githubusercontent.com/43371347/129456528-502482cb-86b4-4a55-a419-97ae086831ed.mp4 + + +## Install +1. Find the latest [release](https://github.com/theTaikun/plotrock/releases). +2. Download the `Source Code` zip. +3. In Blender, navigate to `Edit` => `Preferences` => `Add-Ons`. +4. Click `Install` and select the dowloaded .zip. +5. Now that the addon is installed, click its checkbox to enable it. + +## Usage +_PlotRock side menu can be accessed by pressing the N key in the 3D Vieport._ + +1. Attain data, either from exporting spreadsheet data to .csv, or procuring a .csv file from elsewhere. +2. Import data either from `File`=>`Import`=>`Import CSV for plotting` or from the PlotRock side menu. + 1. On the right-hand side of the import menu, double check import settings. By default, imports file with headers, and comma seperated. +3. To edit the data: + 1. Open a Text Editor window, and select the imported file. + 2. Edit the plot data as needed + 3. Click `Update Plot` from the PlotRock side menu. +4. To customize the shape of the plot, select it, and adjust the sliders in the RockPlot menu. +5. Finalize by adding camera(s) and lighting, to taste. + ## Limitations +RockPlot is still a work in progress, +and as such, +has some limitations. + * Only handles files formatted as CSV. -* ~~CSVs must actually use commas. Other deliminators not yet supported.~~ +* ~~CSVs must actually use commas. Other deliminators not yet supported.~~ Can use either comma or semicolon delimiters. +* Data within CSV has some requirements: + * 2D data, meaning two column. Must be only one Y value for each X value. + * Both X and Y values must be a number. * Files must have the extension `.csv` or `.txt`. -* ~~Headers not yet supported.~~ +* Only one plot per axis. * Currently only creates line graphs. diff --git a/__init__.py b/__init__.py index a4ccb6f..190dbc8 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,12 @@ bl_info = { "name": "PlotRock", + "description": "Create 3D plots from CSV data", + "author": "Isaac Phillips (theTaikun)", "version": (0,0,1), - "blender": (2, 80, 0), + "version_code": 1, # not used by blender, but keeping track here + "blender": (2, 92, 0), + "location": "File > Import > Import CSV for plotting", + "tracker_url": "https://github.com/theTaikun/plotrock/issues/new", "category": "Object", } diff --git a/plot.py b/plot.py index 9e9c577..57f19b8 100644 --- a/plot.py +++ b/plot.py @@ -18,6 +18,14 @@ def convertData(csv_textdata, entry_delimiter=",", has_headers=True): pos_list = [list(map(float, x)) for x in string_list] # convert list of strings to list of floats return pos_list, headers +# findRoot function courtesy of MMDTools addon +def findRoot(obj): + if obj: + if obj.plotrock_type == 'ROOT': + return obj + return findRoot(obj.parent) + return None + class NewPlot: """" @@ -78,7 +86,15 @@ def create_curve(self, coords_list): spline = self.spline spline.points.add(len(coords_list) -1 ) for i, val in enumerate(coords_list): - spline.points[i].co = (val + [2.0] + [1.0]) + spline.points[i].co = (val + [0.0] + [1.0]) + + # Size of Empty same for all axis => set as max of x and y value + max_x = max(coords_list)[0] # Max of nested list checks first val + max_y = max(coords_list, key=lambda x: x[1])[1] # Funct to check max by second val, and return that val + self.root.empty_display_size = max(max_x, max_y) # compares the 2 maxes + + self.root.plotrock_settings.max_x = max_x + self.root.plotrock_settings.max_y = max_y self.crv.plotrock_csv = self.csv_textdata def create_obj(self): @@ -86,16 +102,125 @@ def create_obj(self): self.root = bpy.data.objects.new("rockplot_root", None) self.root.empty_display_type = "ARROWS" + self.root.plotrock_type = "ROOT" crv = bpy.data.curves.new('crv', 'CURVE') crv.dimensions = '2D' + crv.extrude = 0.5 + crv.bevel_depth = 0.05 crv.plotrock_type="plot" spline = crv.splines.new(type='POLY') + spline.use_smooth = False self.crv = crv self.spline = spline self.obj = bpy.data.objects.new('object_name', crv) + self.obj.location[2] = 0.5 self.obj.parent = self.root bpy.data.scenes[0].collection.objects.link(self.obj) bpy.data.scenes[0].collection.objects.link(self.root) + self.grid = self.create_grid() + self.grid.parent = self.root + bpy.data.scenes[0].collection.objects.link(self.grid) + + + def create_grid(self): + gridGeo = bpy.data.node_groups.new("gridNodeTree", "GeometryNodeTree") + gridMesh = bpy.data.meshes.new("gridMesh") + gridObj = bpy.data.objects.new("gridObj", gridMesh ) + geoModifier = gridObj.modifiers.new("gridGeoNodes", "NODES") + geoModifier.node_group = gridGeo + wireModifier = gridObj.modifiers.new("gridWire", "WIREFRAME") + wireModifier.thickness = 0.125 + + nodes = gridGeo.nodes + + gridGeo.inputs.new("NodeSocketGeometry", "Geometry") + gridGeo.outputs.new("NodeSocketGeometry", "Geometry") + input_node = nodes.new("NodeGroupInput") + input_node.location.x = -700 - input_node.width + input_node.location.y = -100 + output_node = nodes.new("NodeGroupOutput") + output_node.is_active_output = True + output_node.location.x = 200 + + + group_input_size= gridGeo.inputs.new("NodeSocketVector", "Size") + group_input_size.default_value=[10,10,0] + # TODO: allow both driver and user modifiable + group_input_size.description = "Custom Grid Size. TEMPORARILY DISABLED" + + node = nodes.new("NodeReroute") + node.name = "SIZE_SPLITTER" + node.location.x = -600 + + node=nodes.new("FunctionNodeInputVector") + node.name = "DRIVER_SIZE" + node.location.x = -700 - node.width + node.location.y = 100 + fcurve = node.driver_add("vector") + var = fcurve[0].driver.variables.new() + fcurve[0].driver.expression = "var" + var.targets[0].id = self.root + var.targets[0].data_path = "plotrock_settings.max_x" + var = fcurve[1].driver.variables.new() + fcurve[1].driver.expression = "var" + var.targets[0].id = self.root + var.targets[0].data_path = "plotrock_settings.max_y" + + node = nodes.new("GeometryNodeMeshGrid") + node.location.x = -100 - node.width + node.location.y = 200 + node.name = "MESH_GRID" + + node = nodes.new("GeometryNodeTransform") + node.name = "XLATE_XFORM" + + node = nodes.new("ShaderNodeVectorMath") + node.name = "DIV_BY_TWO" + node.location.x = -50 - node.width + node.location.y = -100 + node.operation = "DIVIDE" + node.inputs[1].default_value=[2,2,1] + + node = nodes.new("ShaderNodeVectorMath") + node.name = "ADD_GRID_LINE" + node.location.x = -450 - node.width + node.location.y = 200 + node.operation = "ADD" + node.inputs[1].default_value=[1,1,0] + + node = nodes.new("ShaderNodeSeparateXYZ") + node.name = "SepXYZ_size" + node.location.x = -275 - node.width + node.location.y = 250 + + node = nodes.new("ShaderNodeSeparateXYZ") + node.name = "SepXYZ_verts" + node.location.x = -275 - node.width + node.location.y = 100 + + # Choose between user-editable modifier input, and automatic driver + #size_input = input_node.outputs[1] + size_input = nodes["DRIVER_SIZE"].outputs[0] + + + gridGeo.links.new(nodes["SIZE_SPLITTER"].inputs[0], size_input) + gridGeo.links.new(output_node.inputs[0], nodes["XLATE_XFORM"].outputs[0]) + + gridGeo.links.new(nodes["XLATE_XFORM"].inputs["Geometry"], nodes["MESH_GRID"].outputs[0]) + gridGeo.links.new(nodes["XLATE_XFORM"].inputs[1], nodes["DIV_BY_TWO"].outputs[0]) + gridGeo.links.new(nodes["SepXYZ_size"].inputs[0], nodes["SIZE_SPLITTER"].outputs[0]) + gridGeo.links.new(nodes["DIV_BY_TWO"].inputs[0], nodes["SIZE_SPLITTER"].outputs[0]) + gridGeo.links.new(nodes["ADD_GRID_LINE"].inputs[0], nodes["SIZE_SPLITTER"].outputs[0]) + gridGeo.links.new(nodes["MESH_GRID"].inputs[0], nodes["SepXYZ_size"].outputs[0]) + gridGeo.links.new(nodes["MESH_GRID"].inputs[1], nodes["SepXYZ_size"].outputs[1]) + + gridGeo.links.new(nodes["MESH_GRID"].inputs[2], nodes["SepXYZ_verts"].outputs[0]) + gridGeo.links.new(nodes["MESH_GRID"].inputs[3], nodes["SepXYZ_verts"].outputs[1]) + + gridGeo.links.new(nodes["SepXYZ_verts"].inputs[0], nodes["ADD_GRID_LINE"].outputs[0]) + + return gridObj + class UpdatePlot(bpy.types.Operator): bl_idname = "plotrock.update_plot" @@ -122,7 +247,17 @@ def update_curve(self): spline = self.spline for i, val in enumerate(self.pos_list): print("updating point {}".format(i)) - spline.points[i].co= (val + [2.0] + [1.0]) + spline.points[i].co= (val + [0.0] + [1.0]) + + def update_axis(self): + coords_list = self.pos_list + # Size of Empty same for all axis => set as max of x and y value + max_x = max(coords_list)[0] # Max of nested list checks first val + max_y = max(coords_list, key=lambda x: x[1])[1] # Funct to check max by second val, and return that val + self.root.empty_display_size = max(max_x, max_y) # compares the 2 maxes + + self.root.plotrock_settings.max_x = max_x + self.root.plotrock_settings.max_y = max_y def execute(self, context): print("updating") @@ -132,10 +267,12 @@ def execute(self, context): self.csv_textdata = self.crv.plotrock_csv self.delimiter = self.csv_textdata['delimiter'] self.has_headers = self.csv_textdata['has_headers'] + self.root = findRoot(self.obj) self.pos_list, self.headers = convertData(self.csv_textdata, self.delimiter, self.has_headers) self.update_curve() + self.update_axis() return {"FINISHED"} diff --git a/properties.py b/properties.py index 7e7db36..804daa1 100644 --- a/properties.py +++ b/properties.py @@ -2,21 +2,34 @@ type_property= bpy.props.EnumProperty( name="Type", - description="PlotRock Component Type", + # Warnings meant for end user, not to alter within UI + description="PlotRock Component Type (DO NOT CHANGE)", default = "NONE", items=( - ("plot", "Plot", "", 1), - ("test_prop", "Test Property", "", 2), - ("NONE", "None", "", 3) + ("plot", "Plot", "DO NOT CHANGE", 1), + ("ROOT", "Root", "DO NOT CHANGE", 2), + ("NONE", "None", "DO NOT CHANGE", 3) ) ) +class RootSettings(bpy.types.PropertyGroup): + max_x: bpy.props.FloatProperty() + max_y: bpy.props.FloatProperty() + csv_file = bpy.props.PointerProperty(type=bpy.types.Text) def register(): + bpy.utils.register_class(RootSettings) + bpy.types.Curve.plotrock_type = type_property + bpy.types.Object.plotrock_type = type_property bpy.types.Curve.plotrock_csv = csv_file + bpy.types.Object.plotrock_settings = bpy.props.PointerProperty(type=RootSettings) def unregister(): del bpy.types.Curve.plotrock_type + del bpy.types.Object.plotrock_type del bpy.types.Curve.plotrock_csv + del bpy.types.Object.plotrock_settings + + bpy.utils.unregister_class(RootSettings) diff --git a/sample-data.csv b/sample-data.csv new file mode 100644 index 0000000..c0d4d6f --- /dev/null +++ b/sample-data.csv @@ -0,0 +1,5 @@ +1,1 +2,2.7 +3,3 +4,3 +5,6 diff --git a/sample-data_headers.csv b/sample-data_headers.csv new file mode 100644 index 0000000..d716d40 --- /dev/null +++ b/sample-data_headers.csv @@ -0,0 +1,6 @@ +day,value +1,1 +2,2.7 +3,3 +4,3 +5,6 diff --git a/ui_panel.py b/ui_panel.py index 00a1d52..3b5f9cd 100644 --- a/ui_panel.py +++ b/ui_panel.py @@ -1,10 +1,9 @@ import bpy -class LayoutDemoPanel(bpy.types.Panel): - """Creates a Panel in the scene context of the properties editor""" - bl_label = "RockPlot Settings" - bl_idname = "OBJECT_PT_layout" +class OperatorPanel(bpy.types.Panel): + bl_label = "Operator" + bl_idname = "OBJECT_PT_plotrock_layout" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_context = "" @@ -75,12 +74,50 @@ def draw(self, context): row.scale_y = 3.0 row.operator("plotrock.update_plot") + +class PlotPanel(bpy.types.Panel): + bl_label = "Plot Settings" + bl_idname = "OBJECT_PT_plotrock_plot" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_context = "" + bl_category = "RockPlot" + + def draw(self, context): + layout = self.layout + obj = context.active_object + + if(obj is not None and obj.type == "CURVE" and obj.data.plotrock_type == "plot"): + split = layout.split() + col=split.column() + col.label(text="Line Shape") + + # Line Depth + col=split.column() + col.prop(obj.data, 'extrude', text="Depth") + + # Line Width + col.prop(obj.data, 'bevel_depth', text='Width') + + row = layout.row() + row.prop(obj.data.splines[0], 'use_smooth') + + row = layout.row() + row.prop(obj, 'location', index=2, text="Z-Position") + else: + layout.label(text="Select a Plot") + return + + + def register(): - bpy.utils.register_class(LayoutDemoPanel) + bpy.utils.register_class(OperatorPanel) + bpy.utils.register_class(PlotPanel) def unregister(): - bpy.utils.unregister_class(LayoutDemoPanel) + bpy.utils.unregister_class(OperatorPanel) + bpy.utils.unregister_class(PlotPanel) if __name__ == "__main__":