Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions csv_importer/addon.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import bpy
from . import ops, props, ui
from .props import CSVImporterObjectProperties
from bpy.props import PointerProperty
from .props import CSVImporterObjectProperties, CSVExporterSceneProperties
from bpy.props import PointerProperty, EnumProperty
from .ops import ImportCsvPolarsOperator
from .utils import add_current_module_to_path

Expand All @@ -19,10 +19,25 @@ def register():
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
bpy.types.Object.csv = PointerProperty(type=CSVImporterObjectProperties) # type: ignore
bpy.types.Scene.csv_export = PointerProperty(type=CSVExporterSceneProperties) # type: ignore

# Add export type property to window manager for UI
bpy.types.WindowManager.csv_export_type = EnumProperty( # type: ignore
name="Export Type",
description="Type of file to export",
items=[
('CSV', "CSV", "Export as CSV"),
('JSON', "JSON", "Export as JSON"),
('PARQUET', "Parquet", "Export as Parquet")
],
default='CSV'
)


def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
for cls in reversed(CLASSES):
bpy.utils.unregister_class(cls)
del bpy.types.Object.csv # type: ignore
del bpy.types.Scene.csv_export # type: ignore
del bpy.types.WindowManager.csv_export_type # type: ignore
77 changes: 77 additions & 0 deletions csv_importer/exporters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import databpy as db
import polars as pl
import bpy


def from_blender_to_polars_df(blender_object: bpy.types.Object) -> pl.DataFrame:
"""
Convert a Blender mesh object to a basic Polars DataFrame containing mesh attributes.

Args:
blender_object: The Blender object to convert

Returns:
pl.DataFrame: Basic DataFrame containing raw mesh attribute data
"""
# Evaluate the object and get mesh data
evaluated_obj = db.evaluate_object(blender_object)
mesh = evaluated_obj.to_mesh()

# Collect all attribute data
attribute_data = {}
for attr in mesh.attributes:
if attr.name not in {'sharp_face', 'UVMap'} and not attr.name.startswith('.'):
a = db.named_attribute(evaluated_obj, attr.name)
attribute_data[attr.name] = a

# Create and return basic polars DataFrame
return pl.DataFrame(attribute_data)


def from_polars_df_to_csv(df: pl.DataFrame, export_path: str) -> None:
"""
Process a Polars DataFrame and export it to a CSV file.
Handles array expansion, column reordering, and CSV writing.

Args:
df: The Polars DataFrame to process and export
export_path: The file path where the CSV should be saved
"""
# Sort columns so "position" is first
if "position" in df.columns:
column_order = ["position"] + [col for col in df.columns if col != "position"]
df = df.select(column_order)

# Check dtypes and expand array columns
dtypes = df.dtypes
array_columns = []
expanded_df = df.clone()

for i, dtype in enumerate(dtypes):
col_name = df.columns[i]
if str(dtype).startswith('Array'):
array_columns.append(col_name)

# Get the array length from the first non-null value
first_array = expanded_df.select(pl.col(col_name)).item(0, 0)
array_length = len(first_array)

# Expand array into separate columns with indexed names
for j in range(array_length):
expanded_df = expanded_df.with_columns([
pl.col(col_name).arr.get(j).alias(f"{col_name}{j+1}")
])
# Drop the original array column
expanded_df = expanded_df.drop(col_name)

# Reorder columns to place position columns first
position_columns = [col for col in expanded_df.columns if col.startswith('position')]
other_columns = [col for col in expanded_df.columns if not col.startswith('position')]
column_order = position_columns + other_columns
expanded_df = expanded_df.select(column_order)

# Write processed DataFrame to CSV file
expanded_df.write_csv(export_path)



62 changes: 62 additions & 0 deletions csv_importer/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from bpy_extras.io_utils import ImportHelper
from .csv import load_csv
from .parsers import update_obj_from_csv
from .exporters import from_blender_to_polars_df, from_polars_df_to_csv
from pathlib import Path
import csv
import os


# based on the blender docs: https://docs.blender.org/api/current/bpy.types.FileHandler.html#basic-filehandler-for-operator-that-imports-just-one-file
Expand Down Expand Up @@ -131,10 +134,69 @@ def execute(self, context):
self.report({"INFO"}, "Hot reload started")
return {"FINISHED"}

class CSV_OT_ExportData(bpy.types.Operator):
bl_idname = "csv.export_data"
bl_label = "Export Data"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export mesh attribute data to file"

export_type: bpy.props.EnumProperty( # type: ignore
name="Export Type",
description="Type of file to export",
items=[
('CSV', "CSV", "Export as CSV"),
('JSON', "JSON", "Export as JSON"),
('PARQUET', "Parquet", "Export as Parquet")
],
default='CSV'
)

def execute(self, context):
scene = context.scene
export_path = bpy.path.abspath(scene.csv_export.export_path)
export_object = scene.csv_export.export_object

# Check if an object is selected
if export_object is None:
self.report({"WARNING"}, "No object selected for export")
return {"CANCELLED"}

# Print the object name
print(f"Exporting data from object: {export_object.name}")

# Create directory if it doesn't exist
directory = os.path.dirname(export_path)
if directory and not os.path.exists(directory):
try:
os.makedirs(directory)
except OSError as e:
self.report({"ERROR"}, f"Failed to create directory: {e}")
return {"CANCELLED"}

try:
# Convert Blender object to Polars DataFrame
df = from_blender_to_polars_df(export_object)

# Export DataFrame based on selected type
if self.export_type == 'CSV':
from_polars_df_to_csv(df, export_path)
elif self.export_type == 'JSON':
df.write_json(export_path)
else: # PARQUET
df.write_parquet(export_path)

self.report({"INFO"}, f"{self.export_type} file exported to: {export_path} for object: {export_object.name} ({len(df)} rows, {len(df.columns)} columns)")
return {"FINISHED"}

except Exception as e:
self.report({"ERROR"}, f"Failed to export {self.export_type} file: {e}")
return {"CANCELLED"}


CLASSES = (
ImportCsvPolarsOperator,
CSV_FH_import,
CSV_OP_ReloadData,
CSV_OT_ToggleHotReload,
CSV_OT_ExportData,
)
21 changes: 19 additions & 2 deletions csv_importer/props.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from bpy.types import PropertyGroup
from bpy.props import StringProperty, IntProperty, BoolProperty
from bpy.props import StringProperty, IntProperty, BoolProperty, PointerProperty
import bpy


class CSVImporterObjectProperties(PropertyGroup):
Expand All @@ -23,4 +24,20 @@ class CSVImporterObjectProperties(PropertyGroup):
)


CLASSES = (CSVImporterObjectProperties,)
class CSVExporterSceneProperties(PropertyGroup):
export_path: StringProperty( # type: ignore
name="Export Path",
description="Path where the CSV file will be exported",
subtype="FILE_PATH",
default="//exported_data.csv",
)

export_object: PointerProperty( # type: ignore
name="Export Object",
description="Object to export data from",
type=bpy.types.Object,
poll=lambda self, obj: obj.type == 'MESH',
)


CLASSES = (CSVImporterObjectProperties, CSVExporterSceneProperties)
32 changes: 31 additions & 1 deletion csv_importer/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,35 @@ def draw(self, context):
"csv.toggle_hot_reload", text=message, icon=icon, depress=obj.csv.hot_reload
)

class CSV_PT_ExportPanel(bpy.types.Panel):
bl_label = "Export"
bl_idname = "CSV_PT_ExportPanel"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_order = 0
bl_options = {"HEADER_LAYOUT_EXPAND"}

def draw(self, context):
layout = self.layout
scene = context.scene

# Object selector
row = layout.row()
row.prop(scene.csv_export, "export_object", text="Object")

# Export path field
row = layout.row()
row.prop(scene.csv_export, "export_path", text="Export Path")

# Export format buttons
row = layout.row(align=True)
op = row.operator("csv.export_data", text="CSV", icon="FILE")
op.export_type = 'CSV'
op = row.operator("csv.export_data", text="JSON", icon="FILE")
op.export_type = 'JSON'
op = row.operator("csv.export_data", text="Parquet", icon="FILE")
op.export_type = 'PARQUET'


CLASSES = (CSV_PT_ObjectPanel,)
CLASSES = (CSV_PT_ObjectPanel, CSV_PT_ExportPanel)