2025-12-01
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# 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, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
reloadable_modules = [
|
||||
'flip_fluid_aabb',
|
||||
'flip_fluid_cache',
|
||||
'flip_fluid_material_library'
|
||||
'flip_fluid_map',
|
||||
'flip_fluid_geometry_export_object',
|
||||
'flip_fluid_geometry_database',
|
||||
'flip_fluid_geometry_exporter',
|
||||
'flip_fluid_preset_stack',
|
||||
]
|
||||
for module_name in reloadable_modules:
|
||||
if module_name in locals():
|
||||
importlib.reload(locals()[module_name])
|
||||
|
||||
import bpy
|
||||
|
||||
from . import (
|
||||
flip_fluid_aabb,
|
||||
flip_fluid_cache,
|
||||
flip_fluid_material_library,
|
||||
flip_fluid_map,
|
||||
flip_fluid_geometry_export_object,
|
||||
flip_fluid_geometry_database,
|
||||
flip_fluid_geometry_exporter,
|
||||
flip_fluid_preset_stack,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
flip_fluid_cache.register()
|
||||
flip_fluid_material_library.register()
|
||||
flip_fluid_preset_stack.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
flip_fluid_cache.unregister()
|
||||
flip_fluid_material_library.unregister()
|
||||
flip_fluid_preset_stack.unregister()
|
||||
@@ -0,0 +1,104 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# 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, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ..utils import version_compatibility_utils as vcu
|
||||
|
||||
class AABB(object):
|
||||
def __init__(self, x=0, y=0, z=0, xdim=0, ydim=0, zdim=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.z = z
|
||||
self.xdim = xdim
|
||||
self.ydim = ydim
|
||||
self.zdim = zdim
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_blender_object(cls, obj):
|
||||
if obj.type == 'MESH':
|
||||
vertices = obj.data.vertices
|
||||
elif obj.type == 'CURVE':
|
||||
spline = obj.data.splines[0]
|
||||
if spline.type == 'BEZIER':
|
||||
vertices = spline.bezier_points
|
||||
elif spline.type == 'NURBS':
|
||||
vertices = spline.points
|
||||
elif obj.type == 'EMPTY':
|
||||
position = obj.matrix_world.translation
|
||||
return cls(position[0], position[1], position[2], 0.0, 0.0, 0.0)
|
||||
|
||||
xmin, ymin, zmin = float('inf'), float('inf'), float('inf')
|
||||
xmax, ymax, zmax = -float('inf'), -float('inf'), -float('inf')
|
||||
for mv in vertices:
|
||||
v = vcu.element_multiply(obj.matrix_world, mv.co)
|
||||
xmin = min(v.x, xmin)
|
||||
ymin = min(v.y, ymin)
|
||||
zmin = min(v.z, zmin)
|
||||
xmax = max(v.x, xmax)
|
||||
ymax = max(v.y, ymax)
|
||||
zmax = max(v.z, zmax)
|
||||
xdim, ydim, zdim = xmax - xmin, ymax - ymin, zmax - zmin
|
||||
|
||||
return cls(xmin, ymin, zmin, xdim, ydim, zdim)
|
||||
|
||||
|
||||
def is_empty(self, tol = 1e-6):
|
||||
return self.xdim * self.ydim * self.zdim < tol
|
||||
|
||||
|
||||
def contains(self, bbox):
|
||||
return (bbox.x > self.x and bbox.y > self.y and bbox.z > self.z and
|
||||
bbox.x + bbox.xdim < self.x + self.xdim and
|
||||
bbox.y + bbox.ydim < self.y + self.ydim and
|
||||
bbox.z + bbox.zdim < self.z + self.zdim)
|
||||
|
||||
|
||||
def contains_point(self, p):
|
||||
return (p[0] >= self.x and p[0] < self.x + self.xdim and
|
||||
p[1] >= self.y and p[1] < self.y + self.ydim and
|
||||
p[2] >= self.z and p[2] < self.z + self.zdim)
|
||||
|
||||
|
||||
def expand(self, amount):
|
||||
hw = 0.5 * amount
|
||||
self.x -= hw
|
||||
self.y -= hw
|
||||
self.z -= hw
|
||||
self.xdim += amount
|
||||
self.ydim += amount
|
||||
self.zdim += amount
|
||||
|
||||
return AABB(self.x - hw, self.y - hw, self.z - hw,
|
||||
self.xdim + hw, self.ydim + hw, self.zdim + hw)
|
||||
|
||||
|
||||
def intersection(self, bbox):
|
||||
xmin = max(self.x, bbox.x)
|
||||
ymin = max(self.y, bbox.y)
|
||||
zmin = max(self.z, bbox.z)
|
||||
xmax = min(self.x + self.xdim, bbox.x + bbox.xdim)
|
||||
ymax = min(self.y + self.ydim, bbox.y + bbox.ydim)
|
||||
zmax = min(self.z + self.zdim, bbox.z + bbox.zdim)
|
||||
xdim = max(0.0, xmax - xmin)
|
||||
ydim = max(0.0, ymax - ymin)
|
||||
zdim = max(0.0, zmax - zmin)
|
||||
|
||||
return AABB(xmin, ymin, zmin, xdim, ydim, zdim)
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
return {'x': self.x, 'y': self.y, 'z': self.z,
|
||||
'xdim': self.xdim, 'ydim': self.ydim, 'zdim': self.zdim}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+413
@@ -0,0 +1,413 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# 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, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, enum
|
||||
from mathutils import Vector, Matrix, Quaternion, Euler, Color
|
||||
from ..ffengine import TriangleMesh
|
||||
from ..utils import export_utils
|
||||
from ..utils import version_compatibility_utils as vcu
|
||||
from ..utils import cache_utils
|
||||
|
||||
|
||||
###########################################################################
|
||||
### Geometry Helpers
|
||||
###########################################################################
|
||||
|
||||
|
||||
def find_fcurve(id_data, path, index=0):
|
||||
anim_data = id_data.animation_data
|
||||
for fcurve in anim_data.action.fcurves:
|
||||
if fcurve.data_path == path and fcurve.array_index == index:
|
||||
return fcurve
|
||||
|
||||
|
||||
def get_vector3_at_frame(obj, path, frame_id):
|
||||
xcurve = find_fcurve(obj, path, 0)
|
||||
ycurve = find_fcurve(obj, path, 1)
|
||||
zcurve = find_fcurve(obj, path, 2)
|
||||
x = xcurve.evaluate(frame_id) if xcurve else getattr(obj, path)[0]
|
||||
y = ycurve.evaluate(frame_id) if ycurve else getattr(obj, path)[1]
|
||||
z = zcurve.evaluate(frame_id) if zcurve else getattr(obj, path)[2]
|
||||
return (x, y, z)
|
||||
|
||||
|
||||
def get_vector4_at_frame(obj, path, frame_id):
|
||||
wcurve = find_fcurve(obj, path, 0)
|
||||
xcurve = find_fcurve(obj, path, 1)
|
||||
ycurve = find_fcurve(obj, path, 2)
|
||||
zcurve = find_fcurve(obj, path, 3)
|
||||
w = wcurve.evaluate(frame_id) if wcurve else getattr(obj, path)[0]
|
||||
x = xcurve.evaluate(frame_id) if xcurve else getattr(obj, path)[1]
|
||||
y = ycurve.evaluate(frame_id) if ycurve else getattr(obj, path)[2]
|
||||
z = zcurve.evaluate(frame_id) if zcurve else getattr(obj, path)[3]
|
||||
return (w, x, y, z)
|
||||
|
||||
|
||||
def get_rotation_mode_at_frame(obj, frame_id):
|
||||
mode_curve = find_fcurve(obj, "rotation_mode")
|
||||
mode = mode_curve.evaluate(frame_id) if mode_curve else getattr(obj, "rotation_mode")
|
||||
if isinstance(mode, float):
|
||||
# Handles case where rotation mode is keyframed - enum returns a float instead of a string
|
||||
enum = int(mode)
|
||||
if mode == 0:
|
||||
mode = 'QUATERNION'
|
||||
elif mode == 1:
|
||||
mode = 'XYZ'
|
||||
elif mode == 2:
|
||||
mode = 'XZY'
|
||||
elif mode == 3:
|
||||
mode = 'YXZ'
|
||||
elif mode == 4:
|
||||
mode = 'YZX'
|
||||
elif mode == 5:
|
||||
mode = 'ZXY'
|
||||
elif mode == 6:
|
||||
mode = 'ZYX'
|
||||
elif mode == 7:
|
||||
mode = 'AXIS_ANGLE'
|
||||
return mode
|
||||
|
||||
|
||||
def get_matrix_world_at_frame(obj, frame_id):
|
||||
rotation_mode = get_rotation_mode_at_frame(obj, frame_id)
|
||||
if rotation_mode == 'AXIS_ANGLE':
|
||||
axis_angle = get_vector4_at_frame(obj, "rotation_axis_angle", frame_id)
|
||||
angle = axis_angle[0]
|
||||
axis = Vector((axis_angle[1], axis_angle[2], axis_angle[3]))
|
||||
rotation_matrix = Matrix.Rotation(angle, 4, axis)
|
||||
elif rotation_mode == 'QUATERNION':
|
||||
rotation_quat = get_vector4_at_frame(obj, "rotation_quaternion", frame_id)
|
||||
quaternion = Quaternion(rotation_quat)
|
||||
rotation_matrix = quaternion.to_euler().to_matrix().to_4x4()
|
||||
else:
|
||||
rotation = get_vector3_at_frame(obj, "rotation_euler", frame_id)
|
||||
euler_rotation = Euler(rotation, rotation_mode)
|
||||
rotation_matrix = euler_rotation.to_matrix().to_4x4()
|
||||
|
||||
location = get_vector3_at_frame(obj, "location", frame_id)
|
||||
location_matrix = Matrix.Translation(location).to_4x4()
|
||||
|
||||
scale = get_vector3_at_frame(obj, "scale", frame_id)
|
||||
scale_matrix = Matrix.Identity(4)
|
||||
scale_matrix[0][0] = scale[0]
|
||||
scale_matrix[1][1] = scale[1]
|
||||
scale_matrix[2][2] = scale[2]
|
||||
|
||||
return vcu.element_multiply(vcu.element_multiply(location_matrix, rotation_matrix), scale_matrix)
|
||||
|
||||
|
||||
def get_mesh_centroid(obj, apply_transforms=True):
|
||||
if apply_transforms:
|
||||
tmesh = vcu.object_to_triangle_mesh(obj, obj.matrix_world)
|
||||
else:
|
||||
tmesh = vcu.object_to_triangle_mesh(obj)
|
||||
num_vertices = len(tmesh.vertices) // 3
|
||||
if num_vertices == 0:
|
||||
return (0.0, 0.0, 0.0)
|
||||
|
||||
xacc, yacc, zacc = 0.0, 0.0, 0.0
|
||||
for vidx in range(0, num_vertices):
|
||||
xacc += tmesh.vertices[3 * vidx + 0]
|
||||
yacc += tmesh.vertices[3 * vidx + 1]
|
||||
zacc += tmesh.vertices[3 * vidx + 2]
|
||||
|
||||
xacc /= num_vertices
|
||||
yacc /= num_vertices
|
||||
zacc /= num_vertices
|
||||
|
||||
return (xacc, yacc, zacc)
|
||||
|
||||
|
||||
# Method adapted from: https://blender.stackexchange.com/a/93441
|
||||
def curve_to_triangle_mesh(bl_curve_object, apply_transforms=True):
|
||||
if not apply_transforms:
|
||||
orig_location = Vector(bl_curve_object.location)
|
||||
if bl_curve_object.rotation_mode == 'QUATERNION':
|
||||
orig_rotation = Vector(bl_curve_object.rotation_quaternion)
|
||||
bl_curve_object.rotation_quaternion = (0, 0, 0, 0)
|
||||
elif bl_curve_object.rotation_mode == 'AXIS_ANGLE':
|
||||
orig_rotation = Vector(bl_curve_object.rotation_axis_angle)
|
||||
bl_curve_object.rotation_axis_angle = (0, 0, 0, 0)
|
||||
else:
|
||||
orig_rotation = Vector(bl_curve_object.rotation_euler)
|
||||
bl_curve_object.rotation_euler = (0, 0, 0)
|
||||
orig_scale = Vector(bl_curve_object.scale)
|
||||
|
||||
bl_curve_object.location = (0, 0, 0)
|
||||
bl_curve_object.scale = (1, 1, 1)
|
||||
|
||||
instance_obj = bpy.data.objects.new("Empty", None)
|
||||
vcu.link_object_to_master_scene(instance_obj)
|
||||
|
||||
follow_path_constraint = instance_obj.constraints.new(type='FOLLOW_PATH')
|
||||
follow_path_constraint.target = bl_curve_object
|
||||
follow_path_constraint.use_fixed_location = True
|
||||
|
||||
spline = bl_curve_object.data.splines[0]
|
||||
if spline.type == 'BEZIER':
|
||||
num_curve_points = len(spline.bezier_points)
|
||||
elif spline.type == 'NURBS' or spline.type == 'POLY':
|
||||
num_curve_points = len(spline.points)
|
||||
|
||||
extra_vertex = 0 if spline.use_cyclic_u else 1
|
||||
resolution = spline.resolution_u + 1
|
||||
num_vertices = (num_curve_points + extra_vertex) * resolution
|
||||
|
||||
instances = [instance_obj]
|
||||
for i in range(1, num_vertices + extra_vertex):
|
||||
temp_instance = instance_obj.copy()
|
||||
temp_instance_constraint = temp_instance.constraints[0]
|
||||
temp_instance_constraint.offset_factor = i / num_vertices
|
||||
vcu.link_object_to_master_scene(temp_instance)
|
||||
instances.append(temp_instance)
|
||||
|
||||
vcu.depsgraph_update()
|
||||
|
||||
tmesh = TriangleMesh()
|
||||
for instance in instances:
|
||||
instance_constraint = instance.constraints[0]
|
||||
vertex = instance.matrix_world.translation
|
||||
tmesh.vertices.append(vertex[0])
|
||||
tmesh.vertices.append(vertex[1])
|
||||
tmesh.vertices.append(vertex[2])
|
||||
instance.constraints.remove(instance_constraint)
|
||||
vcu.delete_object(instance)
|
||||
|
||||
if not apply_transforms:
|
||||
bl_curve_object.location = orig_location
|
||||
if bl_curve_object.rotation_mode == 'QUATERNION':
|
||||
bl_curve_object.rotation_quaternion = orig_rotation
|
||||
elif bl_curve_object.rotation_mode == 'AXIS_ANGLE':
|
||||
bl_curve_object.rotation_axis_angle = orig_rotation
|
||||
else:
|
||||
bl_curve_object.rotation_euler = orig_rotation
|
||||
bl_curve_object.scale = orig_scale
|
||||
|
||||
return tmesh
|
||||
|
||||
|
||||
###########################################################################
|
||||
### Geometry Export Object
|
||||
###########################################################################
|
||||
|
||||
|
||||
class MotionExportType(enum.Enum):
|
||||
STATIC = 0
|
||||
KEYFRAMED = 1
|
||||
ANIMATED = 2
|
||||
|
||||
|
||||
class GeometryExportType(enum.Enum):
|
||||
MESH = 0
|
||||
VERTICES = 1
|
||||
CENTROID = 2
|
||||
AXIS = 3
|
||||
CURVE = 4
|
||||
|
||||
|
||||
class GeometryExportObject():
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.name_slug = cache_utils.string_to_cache_slug(name)
|
||||
self.motion_export_type = self._initialize_motion_export_type()
|
||||
self.geometry_export_types = []
|
||||
self.skip_reexport = False
|
||||
self.disable_changing_topology_warning = False
|
||||
self.frame_start = 0
|
||||
self.frame_end = 0
|
||||
self.exported_frames = {}
|
||||
self._object_id = -1
|
||||
|
||||
|
||||
def get_blender_object(self):
|
||||
return bpy.data.objects[self.name]
|
||||
|
||||
|
||||
def is_static(self):
|
||||
return self.motion_export_type == MotionExportType.STATIC
|
||||
|
||||
|
||||
def is_keyframed(self):
|
||||
return self.motion_export_type == MotionExportType.KEYFRAMED
|
||||
|
||||
|
||||
def is_animated(self):
|
||||
return self.motion_export_type == MotionExportType.ANIMATED
|
||||
|
||||
|
||||
def is_dynamic(self):
|
||||
return self.is_keyframed() or self.is_animated()
|
||||
|
||||
|
||||
def motion_export_type_to_string(self):
|
||||
if self.is_static():
|
||||
return 'STATIC'
|
||||
elif self.is_keyframed():
|
||||
return 'KEYFRAMED'
|
||||
elif self.is_animated():
|
||||
return 'ANIMATED'
|
||||
|
||||
|
||||
def is_exporting_mesh(self):
|
||||
return GeometryExportType.MESH in self.geometry_export_types
|
||||
|
||||
|
||||
def is_exporting_vertices(self):
|
||||
return GeometryExportType.VERTICES in self.geometry_export_types
|
||||
|
||||
|
||||
def is_exporting_centroid(self):
|
||||
return GeometryExportType.CENTROID in self.geometry_export_types
|
||||
|
||||
|
||||
def is_exporting_axis(self):
|
||||
return GeometryExportType.AXIS in self.geometry_export_types
|
||||
|
||||
|
||||
def is_exporting_curve(self):
|
||||
return GeometryExportType.CURVE in self.geometry_export_types
|
||||
|
||||
|
||||
def set_export_frame_range(self, frame_start, frame_end):
|
||||
if self.is_static():
|
||||
raise Exception("Frame range must only be set for dynamic export objects.")
|
||||
self.frame_start = frame_start
|
||||
self.frame_end = frame_end
|
||||
|
||||
|
||||
def set_motion_export_type(self, motion_export_type_enum):
|
||||
if not isinstance(motion_export_type_enum, MotionExportType):
|
||||
raise TypeError("Value must MotionExportType enum.")
|
||||
self.motion_export_type = motion_export_type_enum
|
||||
|
||||
|
||||
def add_geometry_export_type(self, geometry_export_type):
|
||||
if not isinstance(geometry_export_type, GeometryExportType):
|
||||
raise TypeError("Value must GeometryExportType enum.")
|
||||
if geometry_export_type not in self.geometry_export_types:
|
||||
self.geometry_export_types.append(geometry_export_type)
|
||||
|
||||
|
||||
def clear_geometry_export_types(self):
|
||||
self.geometry_export_types = []
|
||||
|
||||
|
||||
def merge(self, other_export_object):
|
||||
if not isinstance(other_export_object, GeometryExportObject):
|
||||
raise TypeError("Value must GeometryExportObject enum.")
|
||||
if self.name != other_export_object.name:
|
||||
raise Exception("Export objects must have same name to be merged.")
|
||||
|
||||
if other_export_object.motion_export_type.value > self.motion_export_type.value:
|
||||
self.set_motion_export_type(other_export_object.motion_export_type)
|
||||
self.frame_start = other_export_object.frame_start
|
||||
self.frame_end = other_export_object.frame_end
|
||||
|
||||
for geometry_type in other_export_object.geometry_export_types:
|
||||
self.add_geometry_export_type(geometry_type)
|
||||
|
||||
|
||||
def set_object_id(self, database_object_id):
|
||||
self._database_object_id = database_object_id
|
||||
|
||||
|
||||
def get_object_id(self):
|
||||
return self._database_object_id
|
||||
|
||||
|
||||
def exported_frame_exists(self, geometry_export_type, frame_id):
|
||||
if not geometry_export_type in self.geometry_export_types:
|
||||
return False
|
||||
if not geometry_export_type in self.exported_frames:
|
||||
return False
|
||||
if not frame_id in self.exported_frames[geometry_export_type]:
|
||||
return False
|
||||
return self.exported_frames[geometry_export_type][frame_id]
|
||||
|
||||
|
||||
def get_mesh_bobj(self, apply_transforms=True):
|
||||
bl_object = self.get_blender_object()
|
||||
if apply_transforms:
|
||||
tmesh = vcu.object_to_triangle_mesh(bl_object, bl_object.matrix_world)
|
||||
else:
|
||||
tmesh = vcu.object_to_triangle_mesh(bl_object)
|
||||
return tmesh.to_bobj()
|
||||
|
||||
|
||||
def get_centroid(self, apply_transforms=True):
|
||||
bl_object = self.get_blender_object()
|
||||
if bl_object.type == 'MESH':
|
||||
return get_mesh_centroid(bl_object, apply_transforms=apply_transforms)
|
||||
else:
|
||||
vect = bl_object.matrix_world.translation
|
||||
return (vect.x, vect.y, vect.z)
|
||||
|
||||
|
||||
def get_centroid_at_frame(self, frame_id):
|
||||
bl_object = self.get_blender_object()
|
||||
matrix_world = self.get_matrix_world_at_frame(frame_id)
|
||||
if bl_object.type == 'MESH':
|
||||
c = get_mesh_centroid(bl_object, apply_transforms=False)
|
||||
centroid = Vector((c[0], c[1], c[2]))
|
||||
vect = vcu.element_multiply(matrix_world, centroid)
|
||||
else:
|
||||
vect = matrix_world.translation
|
||||
return (vect.x, vect.y, vect.z)
|
||||
|
||||
|
||||
def get_local_axis(self):
|
||||
bl_object = self.get_blender_object()
|
||||
mat = bl_object.matrix_world
|
||||
X = Vector((mat[0][0], mat[1][0], mat[2][0])).normalized()
|
||||
Y = Vector((mat[0][1], mat[1][1], mat[2][1])).normalized()
|
||||
Z = Vector((mat[0][2], mat[1][2], mat[2][2])).normalized()
|
||||
return (X.x, X.y, X.z), (Y.x, Y.y, Y.z), (Z.x, Z.y, Z.z)
|
||||
|
||||
|
||||
def get_local_axis_at_frame(self, frame_id):
|
||||
bl_object = self.get_blender_object()
|
||||
mat = self.get_matrix_world_at_frame(frame_id)
|
||||
X = Vector((mat[0][0], mat[1][0], mat[2][0])).normalized()
|
||||
Y = Vector((mat[0][1], mat[1][1], mat[2][1])).normalized()
|
||||
Z = Vector((mat[0][2], mat[1][2], mat[2][2])).normalized()
|
||||
return (X.x, X.y, X.z), (Y.x, Y.y, Y.z), (Z.x, Z.y, Z.z)
|
||||
|
||||
|
||||
def get_matrix_world_at_frame(self, frame_id):
|
||||
bl_object = self.get_blender_object()
|
||||
return get_matrix_world_at_frame(bl_object, frame_id)
|
||||
|
||||
|
||||
def get_curve_bobj(self, apply_transforms=True):
|
||||
bl_object = self.get_blender_object()
|
||||
tmesh = curve_to_triangle_mesh(bl_object, apply_transforms)
|
||||
return tmesh.to_bobj()
|
||||
|
||||
|
||||
def get_bobj_vertex_triangle_count(self, bobj_data):
|
||||
tmesh = TriangleMesh.from_bobj(bobj_data)
|
||||
return len(tmesh.vertices) // 3, len(tmesh.triangles) // 3
|
||||
|
||||
|
||||
def _initialize_motion_export_type(self):
|
||||
obj = self.get_blender_object()
|
||||
props = obj.flip_fluid.get_property_group()
|
||||
if hasattr(props, 'export_animated_mesh') and props.export_animated_mesh:
|
||||
return MotionExportType.ANIMATED
|
||||
if export_utils.is_object_keyframe_animated(obj):
|
||||
return MotionExportType.KEYFRAMED
|
||||
return MotionExportType.STATIC
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# 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, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, os, copy
|
||||
from datetime import datetime
|
||||
|
||||
from . import flip_fluid_cache
|
||||
from .flip_fluid_geometry_export_object import GeometryExportObject, MotionExportType, GeometryExportType
|
||||
from .flip_fluid_geometry_database import GeometryDatabase
|
||||
from ..utils import version_compatibility_utils as vcu
|
||||
from ..utils import cache_utils
|
||||
|
||||
|
||||
class WorkQueueItem():
|
||||
def __init__(self):
|
||||
self.geometry_export_object = None
|
||||
self.geometry_export_type = None
|
||||
self.frame = 0
|
||||
self.apply_transforms = True
|
||||
|
||||
|
||||
class GeometryExportManager():
|
||||
def __init__(self, export_directory):
|
||||
self.geometry_export_objects = []
|
||||
|
||||
self._database_filename = "export_data.sqlite3"
|
||||
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
self._database_filepath = dprops.cache.get_geometry_database_abspath(export_directory, self._database_filename)
|
||||
self._is_linked_geometry_database = dprops.cache.is_linked_geometry_directory()
|
||||
|
||||
self._geometry_database = GeometryDatabase(self._database_filepath, clear_database=False)
|
||||
|
||||
self._geometry_export_objects_dict = {}
|
||||
self._is_initialized = False
|
||||
|
||||
self._work_queue = []
|
||||
self._static_queue_size = 0
|
||||
self._keyframed_queue_size = 0
|
||||
self._animated_queue_size = 0
|
||||
self._num_static_processed = 0
|
||||
self._num_keyframed_processed = 0
|
||||
self._num_animated_processed = 0
|
||||
|
||||
self._export_stage_string = ""
|
||||
self._export_stage_progress = 0.0
|
||||
self._is_error = False
|
||||
self._error_message = ""
|
||||
|
||||
|
||||
def add_geometry_export_object(self, export_object):
|
||||
if export_object.name_slug in self._geometry_export_objects_dict:
|
||||
existing_object = self._geometry_export_objects_dict[export_object.name_slug]
|
||||
existing_object.merge(export_object)
|
||||
else:
|
||||
self.geometry_export_objects.append(export_object)
|
||||
self._geometry_export_objects_dict[export_object.name_slug] = export_object
|
||||
|
||||
|
||||
def get_geometry_export_object(self, name):
|
||||
slug = cache_utils.string_to_cache_slug(name)
|
||||
if slug in self._geometry_export_objects_dict:
|
||||
return self._geometry_export_objects_dict[slug]
|
||||
return None
|
||||
|
||||
|
||||
def get_geometry_export_objects_by_export_type(self, export_type):
|
||||
if isinstance(export_type, MotionExportType):
|
||||
return [obj for obj in self.geometry_export_objects if obj.motion_export_type == export_type]
|
||||
elif isinstance(export_type, GeometryExportType):
|
||||
return [obj for obj in self.geometry_export_objects if export_type in obj.geometry_export_types]
|
||||
else:
|
||||
raise TypeError("Value must MotionExportType or GeometryExportType enum.")
|
||||
|
||||
|
||||
def initialize(self):
|
||||
if self._is_initialized:
|
||||
raise Exception("GeometryExportManager already initialized.")
|
||||
|
||||
for obj in self.geometry_export_objects:
|
||||
obj.geometry_export_types.sort(key=lambda x: x.value)
|
||||
|
||||
self._geometry_database.open()
|
||||
try:
|
||||
self._delete_geometry_export_objects_from_database()
|
||||
self._add_geometry_export_objects_to_database()
|
||||
self._initialize_geometry_export_object_ids()
|
||||
self._clean_unused_objects_from_database()
|
||||
self._initialize_frame_ranges()
|
||||
self._initialize_work_queues()
|
||||
self._geometry_database.commit()
|
||||
except Exception as e:
|
||||
self._geometry_database.close()
|
||||
raise e
|
||||
self._geometry_database.close()
|
||||
|
||||
self._is_initialized = True
|
||||
|
||||
|
||||
def update_export(self, step_time):
|
||||
if not self._work_queue:
|
||||
return True
|
||||
|
||||
self._geometry_database.open()
|
||||
self._geometry_database.begin()
|
||||
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
while self._work_queue:
|
||||
work_item = self._work_queue.pop()
|
||||
self._process_work_item(work_item)
|
||||
if self._is_error:
|
||||
self._geometry_database.close()
|
||||
return True
|
||||
if self._get_elapsed_time(start_time) >= step_time:
|
||||
break
|
||||
|
||||
self._set_export_state(work_item)
|
||||
except Exception as e:
|
||||
self._geometry_database.close()
|
||||
raise e
|
||||
|
||||
self._geometry_database.commit()
|
||||
self._geometry_database.close()
|
||||
|
||||
filesize = self._geometry_database.get_filesize()
|
||||
num_processed = (self._total_queue_size - len(self._work_queue))
|
||||
output_str = "Exporting... " + str(num_processed) + " / " + str(self._total_queue_size) + " objects "
|
||||
output_str += "\t(Database size: " + str(filesize) + ")"
|
||||
print(output_str)
|
||||
|
||||
return not self._work_queue
|
||||
|
||||
|
||||
def get_export_progress(self):
|
||||
return self._export_stage_progress
|
||||
|
||||
|
||||
def get_export_stage(self):
|
||||
return self._export_stage_string
|
||||
|
||||
|
||||
def is_error(self):
|
||||
return self._is_error
|
||||
|
||||
|
||||
def get_error_message(self):
|
||||
return self._error_message
|
||||
|
||||
|
||||
def _initialize_work_queues(self):
|
||||
static_queue = self._generate_static_work_queue()
|
||||
keyframed_queue = self._generate_keyframed_work_queue()
|
||||
keyframed_static_basis_queue = self._generate_keyframed_static_basis_work_queue()
|
||||
animated_queue = self._generate_animated_work_queue()
|
||||
total_queue = animated_queue + keyframed_queue + keyframed_static_basis_queue + static_queue
|
||||
|
||||
self._work_queue = total_queue
|
||||
self._static_queue_size = len(static_queue + keyframed_static_basis_queue)
|
||||
self._keyframed_queue_size = len(keyframed_queue)
|
||||
self._animated_queue_size = len(animated_queue)
|
||||
self._total_queue_size = len(total_queue)
|
||||
|
||||
|
||||
def _generate_static_work_queue(self):
|
||||
static_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.STATIC)
|
||||
|
||||
work_queue = []
|
||||
for obj in static_objects:
|
||||
for geotype in obj.geometry_export_types:
|
||||
if obj.skip_reexport:
|
||||
export_data_exists = self._geometry_database.static_geometry_exists(obj.get_object_id(), geotype)
|
||||
if export_data_exists:
|
||||
continue
|
||||
|
||||
w = WorkQueueItem()
|
||||
w.geometry_export_object = obj
|
||||
w.geometry_export_type = geotype
|
||||
work_queue.append(w)
|
||||
|
||||
work_queue.reverse()
|
||||
return work_queue
|
||||
|
||||
|
||||
# Keyframed objects also need to export a static version of the geometry to apply
|
||||
# transforms upon
|
||||
def _generate_keyframed_static_basis_work_queue(self):
|
||||
keyframed_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.KEYFRAMED)
|
||||
|
||||
work_queue = []
|
||||
for obj in keyframed_objects:
|
||||
obj_copy = copy.copy(obj)
|
||||
obj_copy.motion_export_type = MotionExportType.STATIC
|
||||
for geotype in obj_copy.geometry_export_types:
|
||||
if obj_copy.skip_reexport:
|
||||
export_data_exists = self._geometry_database.static_geometry_exists(obj_copy.get_object_id(), geotype)
|
||||
if export_data_exists:
|
||||
continue
|
||||
|
||||
w = WorkQueueItem()
|
||||
w.geometry_export_object = obj_copy
|
||||
w.geometry_export_type = geotype
|
||||
w.apply_transforms = False
|
||||
work_queue.append(w)
|
||||
|
||||
work_queue.reverse()
|
||||
return work_queue
|
||||
|
||||
|
||||
def _generate_dynamic_work_queue_for_frame(self, export_objects, frameno):
|
||||
work_queue = []
|
||||
for obj in export_objects:
|
||||
for geotype in obj.geometry_export_types:
|
||||
if obj.skip_reexport and obj.exported_frame_exists(geotype, frameno):
|
||||
continue
|
||||
|
||||
w = WorkQueueItem()
|
||||
w.geometry_export_object = obj
|
||||
w.geometry_export_type = geotype
|
||||
w.frame = frameno
|
||||
work_queue.append(w)
|
||||
|
||||
return work_queue
|
||||
|
||||
|
||||
def _get_min_max_frame_range(self, export_object_list):
|
||||
min_obj = min(export_object_list, key=lambda x: x.frame_start)
|
||||
max_obj = max(export_object_list, key=lambda x: x.frame_end)
|
||||
return min_obj.frame_start, max_obj.frame_end
|
||||
|
||||
|
||||
def _generate_keyframed_work_queue(self):
|
||||
work_queue = []
|
||||
keyframed_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.KEYFRAMED)
|
||||
if not keyframed_objects:
|
||||
return work_queue
|
||||
|
||||
frame_start, frame_end = self._get_min_max_frame_range(keyframed_objects)
|
||||
for frameno in range(frame_start, frame_end + 1):
|
||||
work_queue += self._generate_dynamic_work_queue_for_frame(keyframed_objects, frameno)
|
||||
|
||||
work_queue.reverse()
|
||||
return work_queue
|
||||
|
||||
|
||||
def _generate_animated_work_queue(self):
|
||||
work_queue = []
|
||||
animated_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.ANIMATED)
|
||||
if not animated_objects:
|
||||
return work_queue
|
||||
|
||||
frame_start, frame_end = self._get_min_max_frame_range(animated_objects)
|
||||
for frameno in range(frame_start, frame_end + 1):
|
||||
work_queue += self._generate_dynamic_work_queue_for_frame(animated_objects, frameno)
|
||||
|
||||
work_queue.reverse()
|
||||
return work_queue
|
||||
|
||||
|
||||
def _get_elapsed_time(self, start_time):
|
||||
dt = datetime.now() - start_time
|
||||
return dt.days * 86400 + dt.seconds + dt.microseconds / 1000000.0
|
||||
|
||||
|
||||
def _set_export_state(self, work_item):
|
||||
motion_type = work_item.geometry_export_object.motion_export_type
|
||||
if motion_type == MotionExportType.STATIC:
|
||||
self._export_stage_string = "STATIC"
|
||||
self._export_stage_progress = self._num_static_processed / self._static_queue_size
|
||||
elif motion_type == MotionExportType.KEYFRAMED:
|
||||
self._export_stage_string = "KEYFRAMED"
|
||||
self._export_stage_progress = self._num_keyframed_processed / self._keyframed_queue_size
|
||||
elif motion_type == MotionExportType.ANIMATED:
|
||||
self._export_stage_string = "ANIMATED"
|
||||
self._export_stage_progress = self._num_animated_processed / self._animated_queue_size
|
||||
|
||||
|
||||
def _set_error(self, errmsg):
|
||||
self._is_error = True
|
||||
self._error_message = errmsg
|
||||
|
||||
|
||||
###########################################################################
|
||||
### Database Operations
|
||||
###########################################################################
|
||||
|
||||
|
||||
def _delete_geometry_export_objects_from_database(self):
|
||||
for obj in self.geometry_export_objects:
|
||||
if not obj.skip_reexport and self._geometry_database.object_exists(obj):
|
||||
self._geometry_database.delete_object_by_slug(obj.name_slug)
|
||||
|
||||
|
||||
def _add_geometry_export_objects_to_database(self):
|
||||
for obj in self.geometry_export_objects:
|
||||
if not self._geometry_database.object_exists(obj):
|
||||
self._geometry_database.add_object(obj)
|
||||
|
||||
|
||||
def _initialize_geometry_export_object_ids(self):
|
||||
for obj in self.geometry_export_objects:
|
||||
obj.set_object_id(self._geometry_database.get_object_id(obj))
|
||||
|
||||
|
||||
def _clean_unused_objects_from_database(self):
|
||||
if self._is_linked_geometry_database:
|
||||
return
|
||||
|
||||
all_objects = self._geometry_database.get_all_objects()
|
||||
database_slugs = [row[2] for row in all_objects]
|
||||
current_slugs = [obj.name_slug for obj in self.geometry_export_objects]
|
||||
for slug in database_slugs:
|
||||
if slug not in current_slugs:
|
||||
self._geometry_database.delete_object_by_slug(slug)
|
||||
|
||||
|
||||
def _initialize_geometry_export_object_frame_range(self, obj):
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
frame_start, frame_end = dprops.simulation.get_frame_range()
|
||||
obj.set_export_frame_range(frame_start, frame_end)
|
||||
if not obj.skip_reexport:
|
||||
return
|
||||
|
||||
ident = obj.get_object_id()
|
||||
mtype = obj.motion_export_type
|
||||
frame_data_dict = {}
|
||||
for geotype in obj.geometry_export_types:
|
||||
frame_data_dict[geotype] = {}
|
||||
frame_list = self._geometry_database.get_dynamic_geometry_exported_frames(ident, mtype, geotype)
|
||||
|
||||
for frame_id in range(frame_start, frame_end + 1):
|
||||
frame_data_dict[geotype][frame_id] = False
|
||||
for frame_id in frame_list:
|
||||
frame_data_dict[geotype][frame_id] = True
|
||||
|
||||
obj.exported_frames = frame_data_dict
|
||||
|
||||
|
||||
def _initialize_frame_ranges(self):
|
||||
for obj in self.geometry_export_objects:
|
||||
if not obj.is_dynamic():
|
||||
continue
|
||||
self._initialize_geometry_export_object_frame_range(obj)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###########################################################################
|
||||
### Process Geometry
|
||||
###########################################################################
|
||||
|
||||
|
||||
def _update_blender_frame(self, frameno):
|
||||
if bpy.context.scene.frame_current == frameno:
|
||||
return
|
||||
flip_fluid_cache.DISABLE_MESH_CACHE_LOAD = True
|
||||
bpy.context.scene.frame_set(frameno)
|
||||
flip_fluid_cache.DISABLE_MESH_CACHE_LOAD = False
|
||||
|
||||
|
||||
def _process_mesh_object(self, work_item):
|
||||
motion_type = work_item.geometry_export_object.motion_export_type
|
||||
if motion_type == MotionExportType.STATIC:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
bobj_data = work_item.geometry_export_object.get_mesh_bobj(apply_transforms=work_item.apply_transforms)
|
||||
self._geometry_database.add_mesh_static(object_id, bobj_data)
|
||||
|
||||
elif motion_type == MotionExportType.KEYFRAMED:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
frame_id = work_item.frame
|
||||
matrix_world = work_item.geometry_export_object.get_matrix_world_at_frame(frame_id)
|
||||
self._geometry_database.add_mesh_keyframed(object_id, frame_id, matrix_world)
|
||||
|
||||
elif motion_type == MotionExportType.ANIMATED:
|
||||
frame_id = work_item.frame
|
||||
self._update_blender_frame(frame_id)
|
||||
name_slug = work_item.geometry_export_object.name_slug
|
||||
disable_warning = work_item.geometry_export_object.disable_changing_topology_warning
|
||||
bobj_data = work_item.geometry_export_object.get_mesh_bobj()
|
||||
|
||||
if not disable_warning:
|
||||
current_frame_bytes = len(bobj_data)
|
||||
previous_frame_bytes = self._geometry_database.get_mesh_animated_blob_length(name_slug, frame_id - 1)
|
||||
if previous_frame_bytes is not None and current_frame_bytes != previous_frame_bytes:
|
||||
current_bobj_data = bobj_data
|
||||
previous_bobj_data = previous_frame_bytes = self._geometry_database.get_mesh_animated(name_slug, frame_id - 1)
|
||||
current_vcount, current_tcount = work_item.geometry_export_object.get_bobj_vertex_triangle_count(current_bobj_data)
|
||||
previous_vcount, previous_tcount = work_item.geometry_export_object.get_bobj_vertex_triangle_count(previous_bobj_data)
|
||||
|
||||
bl_object = bpy.data.objects.get(work_item.geometry_export_object.name)
|
||||
error_reason = "Unknown"
|
||||
if bl_object is not None:
|
||||
if bl_object.flip_fluid.is_obstacle():
|
||||
error_reason = "Obstacle object require mesh velocities to be computed for fluid interaction."
|
||||
elif bl_object.flip_fluid.is_inflow():
|
||||
error_reason = "Inflow object 'Add Object Velocity to Inflow' option require mesh velocities to be computed for this feature."
|
||||
elif bl_object.flip_fluid.is_fluid():
|
||||
error_reason = "Fluid object 'Add Object Velocity to Fluid' option require mesh velocities to be computed for this feature."
|
||||
|
||||
errmsg = ("Warning: unable to export animated mesh '" + work_item.geometry_export_object.name +
|
||||
"'. Animated meshes must have the same number of " +
|
||||
"vertices/triangles for each frame and must not change topology\nif the mesh velocity is required to be computed correctly." +
|
||||
"\nError Reason: " + error_reason +
|
||||
"\n\nFrame " + str(frame_id - 1) + ": " + str(previous_vcount) + " vertices, " + str(previous_tcount) + " triangles"
|
||||
"\nFrame " + str(frame_id) + ": " + str(current_vcount)) + " vertices, " + str(current_tcount) + " triangles"
|
||||
|
||||
errmsg += ("\n\nDisable this warning in the Advanced Settings panel. Warning: " +
|
||||
"mesh velocity data will not be computed for meshes with changing topology.")
|
||||
self._set_error(errmsg)
|
||||
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
self._geometry_database.add_mesh_animated(object_id, frame_id, bobj_data)
|
||||
|
||||
|
||||
def _process_centroid_object(self, work_item):
|
||||
motion_type = work_item.geometry_export_object.motion_export_type
|
||||
if motion_type == MotionExportType.STATIC:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
centroid = work_item.geometry_export_object.get_centroid(apply_transforms=work_item.apply_transforms)
|
||||
self._geometry_database.add_centroid_static(object_id, centroid)
|
||||
|
||||
elif motion_type == MotionExportType.KEYFRAMED:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
frame_id = work_item.frame
|
||||
centroid = work_item.geometry_export_object.get_centroid_at_frame(frame_id)
|
||||
self._geometry_database.add_centroid_keyframed(object_id, frame_id, centroid)
|
||||
|
||||
elif motion_type == MotionExportType.ANIMATED:
|
||||
frame_id = work_item.frame
|
||||
self._update_blender_frame(frame_id)
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
centroid = work_item.geometry_export_object.get_centroid()
|
||||
self._geometry_database.add_centroid_animated(object_id, frame_id, centroid)
|
||||
|
||||
|
||||
def _process_axis_object(self, work_item):
|
||||
motion_type = work_item.geometry_export_object.motion_export_type
|
||||
if motion_type == MotionExportType.STATIC:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
local_x, local_y, local_z = work_item.geometry_export_object.get_local_axis()
|
||||
self._geometry_database.add_axis_static(object_id, local_x, local_y, local_z)
|
||||
|
||||
elif motion_type == MotionExportType.KEYFRAMED:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
frame_id = work_item.frame
|
||||
local_x, local_y, local_z = work_item.geometry_export_object.get_local_axis_at_frame(frame_id)
|
||||
self._geometry_database.add_axis_keyframed(object_id, frame_id, local_x, local_y, local_z)
|
||||
|
||||
elif motion_type == MotionExportType.ANIMATED:
|
||||
frame_id = work_item.frame
|
||||
self._update_blender_frame(frame_id)
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
local_x, local_y, local_z = work_item.geometry_export_object.get_local_axis()
|
||||
self._geometry_database.add_axis_animated(object_id, frame_id, local_x, local_y, local_z)
|
||||
|
||||
|
||||
def _process_curve_object(self, work_item):
|
||||
motion_type = work_item.geometry_export_object.motion_export_type
|
||||
if motion_type == MotionExportType.STATIC:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
bobj_data = work_item.geometry_export_object.get_curve_bobj(apply_transforms=work_item.apply_transforms)
|
||||
self._geometry_database.add_curve_static(object_id, bobj_data)
|
||||
|
||||
elif motion_type == MotionExportType.KEYFRAMED:
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
frame_id = work_item.frame
|
||||
matrix_world = work_item.geometry_export_object.get_matrix_world_at_frame(frame_id)
|
||||
self._geometry_database.add_curve_keyframed(object_id, frame_id, matrix_world)
|
||||
|
||||
elif motion_type == MotionExportType.ANIMATED:
|
||||
frame_id = work_item.frame
|
||||
self._update_blender_frame(frame_id)
|
||||
object_id = work_item.geometry_export_object.get_object_id()
|
||||
bobj_data = work_item.geometry_export_object.get_curve_bobj()
|
||||
self._geometry_database.add_curve_animated(object_id, frame_id, bobj_data)
|
||||
|
||||
|
||||
def _process_work_item(self, work_item):
|
||||
export_type = work_item.geometry_export_type
|
||||
if export_type == GeometryExportType.MESH:
|
||||
self._process_mesh_object(work_item)
|
||||
elif export_type == GeometryExportType.VERTICES:
|
||||
pass
|
||||
elif export_type == GeometryExportType.CENTROID:
|
||||
self._process_centroid_object(work_item)
|
||||
elif export_type == GeometryExportType.AXIS:
|
||||
self._process_axis_object(work_item)
|
||||
elif export_type == GeometryExportType.CURVE:
|
||||
self._process_curve_object(work_item)
|
||||
|
||||
motion_type = work_item.geometry_export_object.motion_export_type
|
||||
if motion_type == MotionExportType.STATIC:
|
||||
self._num_static_processed += 1
|
||||
elif motion_type == MotionExportType.KEYFRAMED:
|
||||
self._num_keyframed_processed += 1
|
||||
elif motion_type == MotionExportType.ANIMATED:
|
||||
self._num_animated_processed += 1
|
||||
@@ -0,0 +1,55 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# 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, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Map(dict):
|
||||
def __init__(self, dict_data):
|
||||
super(Map, self).__init__(dict_data)
|
||||
for k, v in dict_data.items():
|
||||
if isinstance(v, dict):
|
||||
self[k] = Map(v)
|
||||
elif isinstance(v, list):
|
||||
new_list = []
|
||||
for list_element in v:
|
||||
if isinstance(list_element, dict):
|
||||
new_list.append(Map(list_element))
|
||||
else:
|
||||
new_list.append(list_element)
|
||||
self[k] = new_list
|
||||
|
||||
else:
|
||||
self[k] = v
|
||||
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return self.get(attr)
|
||||
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self.__setitem__(key, value)
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super(Map, self).__setitem__(key, value)
|
||||
self.__dict__.update({key: value})
|
||||
|
||||
|
||||
def __delattr__(self, item):
|
||||
self.__delitem__(item)
|
||||
|
||||
|
||||
def __delitem__(self, key):
|
||||
super(Map, self).__delitem__(key)
|
||||
del self.__dict__[key]
|
||||
@@ -0,0 +1,341 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# 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, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, os, glob, hashlib, bpy.utils.previews
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
EnumProperty,
|
||||
StringProperty,
|
||||
PointerProperty,
|
||||
CollectionProperty
|
||||
)
|
||||
|
||||
from .. import types
|
||||
from ..utils import version_compatibility_utils as vcu
|
||||
|
||||
|
||||
class FLIPFluidMaterialLibraryMaterial(bpy.types.PropertyGroup):
|
||||
conv = vcu.convert_attribute_to_28
|
||||
name = StringProperty(default=""); exec(conv("name"))
|
||||
description = StringProperty(default=""); exec(conv("description"))
|
||||
path = StringProperty(default=""); exec(conv("path"))
|
||||
type = EnumProperty(items=types.material_types, default="MATERIAL_TYPE_SURFACE"); exec(conv("type"))
|
||||
icon_id = IntProperty(default=-1); exec(conv("icon_id"))
|
||||
imported_icon_id = IntProperty(default=-1); exec(conv("imported_icon_id"))
|
||||
|
||||
|
||||
def get_ui_enum(self):
|
||||
library = bpy.context.scene.flip_fluid_material_library
|
||||
is_imported, imported_name = library.is_material_imported(self.name)
|
||||
if is_imported:
|
||||
display_name = imported_name
|
||||
icon_id = self.imported_icon_id
|
||||
else:
|
||||
display_name = self.name
|
||||
icon_id = self.icon_id
|
||||
return (self.name, display_name, self.description, icon_id, self._get_hash())
|
||||
|
||||
|
||||
def _get_hash(self):
|
||||
# Append text when generating library material hash so that different
|
||||
# hash values are created between library and non-library materials with
|
||||
# the same name
|
||||
hash_string = self.name + "flip_fluid_material_library"
|
||||
return int(hashlib.sha1(hash_string.encode('utf-8')).hexdigest(), 16) % int(1e6)
|
||||
|
||||
|
||||
class FLIPFluidMaterialLibrary(bpy.types.PropertyGroup):
|
||||
conv = vcu.convert_attribute_to_28
|
||||
library_path = StringProperty(default=""); exec(conv("library_path"))
|
||||
material_list = CollectionProperty(type=FLIPFluidMaterialLibraryMaterial); exec(conv("material_list"))
|
||||
|
||||
|
||||
@classmethod
|
||||
def register(cls):
|
||||
cls.material_icons = bpy.utils.previews.new()
|
||||
|
||||
|
||||
@classmethod
|
||||
def unregister(cls):
|
||||
bpy.utils.previews.remove(cls.material_icons)
|
||||
|
||||
|
||||
def load_post(self):
|
||||
# Data block id (pointer) won't be consistent between saves so
|
||||
# need reinitialize values of imported material library materials
|
||||
for m in bpy.data.materials:
|
||||
if m.flip_fluid_material_library.is_library_material:
|
||||
m.flip_fluid_material_library.reinitialize_data_block_id(m)
|
||||
|
||||
|
||||
def scene_update_post(self, scene):
|
||||
for m in bpy.data.materials:
|
||||
if not m.flip_fluid_material_library.is_library_material:
|
||||
continue
|
||||
if not m.flip_fluid_material_library.is_original_data_block(m):
|
||||
m.flip_fluid_material_library.deactivate()
|
||||
|
||||
|
||||
def initialize(self, library_path):
|
||||
self.library_path = library_path
|
||||
self._initialize_library_material_list()
|
||||
self._initialize_preview_icons()
|
||||
|
||||
|
||||
def check_icons_initialized(self):
|
||||
# Icons will be cleared if Blender scripts are reloaded so
|
||||
# this method can check and re-initialize icons if needed
|
||||
if len(self.material_icons) > 0:
|
||||
return
|
||||
self._initialize_preview_icons()
|
||||
|
||||
|
||||
def import_material(self, material_name):
|
||||
if not self.is_material_in_library(material_name):
|
||||
return material_name
|
||||
|
||||
is_imported, imported_name = self.is_material_imported(material_name)
|
||||
if is_imported:
|
||||
return imported_name
|
||||
|
||||
mdata = self._get_material_data_from_name(material_name)
|
||||
with bpy.data.libraries.load(mdata.path) as (data_from, data_to):
|
||||
for material in data_from.materials:
|
||||
if material == material_name:
|
||||
data_to.materials.append(material)
|
||||
break
|
||||
|
||||
material_object = data_to.materials[0]
|
||||
material_object.flip_fluid_material_library.activate(material_object, material_name)
|
||||
|
||||
return material_object.name
|
||||
|
||||
|
||||
def get_import_material_copy_name(self, name):
|
||||
prefix = "FF "
|
||||
if name.startswith(prefix) and len(name) > len(prefix):
|
||||
name = name[len(prefix):]
|
||||
name = name.split('.')[0]
|
||||
|
||||
if not bpy.data.materials.get(name):
|
||||
return name
|
||||
|
||||
duplicates = []
|
||||
for m in bpy.data.materials:
|
||||
if m.name == name:
|
||||
duplicates.append(0)
|
||||
continue
|
||||
suffix = m.name.rsplit('.', 1)[-1]
|
||||
if m.name.startswith(name) and len(m.name) == len(name) + 4 and suffix.isdigit():
|
||||
duplicates.append(int(suffix))
|
||||
|
||||
if not duplicates:
|
||||
return name
|
||||
|
||||
for i in range(1, 999):
|
||||
if not i in duplicates:
|
||||
return name + "." + str(i).zfill(3)
|
||||
return name
|
||||
|
||||
|
||||
def import_material_copy(self, material_name):
|
||||
mdata = self._get_material_data_from_name(material_name)
|
||||
with bpy.data.libraries.load(mdata.path) as (data_from, data_to):
|
||||
for material in data_from.materials:
|
||||
if material == material_name:
|
||||
data_to.materials.append(material)
|
||||
break
|
||||
|
||||
material_object = data_to.materials[0]
|
||||
material_object.name = self.get_import_material_copy_name(material_object.name)
|
||||
return material_object.name
|
||||
|
||||
|
||||
def get_imported_material(self, library_name):
|
||||
for m in bpy.data.materials:
|
||||
libprops = m.flip_fluid_material_library
|
||||
if libprops.is_library_material and libprops.library_name == library_name:
|
||||
return m.name
|
||||
return None
|
||||
|
||||
|
||||
def is_material_imported(self, library_name):
|
||||
for m in bpy.data.materials:
|
||||
libprops = m.flip_fluid_material_library
|
||||
if libprops.is_library_material and libprops.library_name == library_name:
|
||||
return True, m.name
|
||||
return False, ""
|
||||
|
||||
|
||||
def is_material_in_library(self, material_name):
|
||||
for mdata in self.material_list:
|
||||
if mdata.name == material_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _initialize_library_material_list(self):
|
||||
self.material_list.clear()
|
||||
|
||||
extension = "blend"
|
||||
subdirs = ["surface", "whitewater", "all"]
|
||||
for subdir in subdirs:
|
||||
subdir_path = os.path.join(self.library_path, subdir)
|
||||
subdir_files = glob.glob(subdir_path + "/*." + extension)
|
||||
for f in subdir_files:
|
||||
if subdir == "surface":
|
||||
mtype = 'MATERIAL_TYPE_SURFACE'
|
||||
elif subdir == "whitewater":
|
||||
mtype = 'MATERIAL_TYPE_WHITEWATER'
|
||||
elif subdir == "all":
|
||||
mtype = 'MATERIAL_TYPE_ALL'
|
||||
|
||||
description_path = f[:-len(extension)] + "description"
|
||||
description_text = ""
|
||||
if os.path.isfile(description_path):
|
||||
with open(description_path, 'r', encoding='utf-8') as description_file:
|
||||
description_text = description_file.read()
|
||||
|
||||
new_material = self.material_list.add()
|
||||
new_material.name = os.path.splitext(os.path.basename(f))[0]
|
||||
new_material.description = str(description_text)
|
||||
new_material.path = f
|
||||
new_material.type = mtype
|
||||
new_material.icon_id = -1 # Will be set after icons are initialized
|
||||
|
||||
|
||||
def _calculate_material_library_hash(self):
|
||||
if len(self.material_list) == 0:
|
||||
return "0"
|
||||
|
||||
SHAhash = hashlib.md5()
|
||||
for m in self.material_list:
|
||||
with open(m.path, "rb") as f:
|
||||
bytes = f.read()
|
||||
SHAhash.update(hashlib.md5(bytes).hexdigest().encode('utf-8'))
|
||||
|
||||
return SHAhash.hexdigest()
|
||||
|
||||
|
||||
def _update_material_library_hash(self):
|
||||
icon_dir = os.path.join(self.library_path, "icons")
|
||||
os.makedirs(icon_dir, exist_ok=True)
|
||||
|
||||
icon_hash_path = os.path.join(icon_dir, "material_library_hash")
|
||||
old_material_library_hash = None
|
||||
if os.path.isfile(icon_hash_path):
|
||||
with open(icon_hash_path, 'r', encoding='utf-8') as f:
|
||||
old_material_library_hash = f.read()
|
||||
|
||||
current_material_library_hash = self._calculate_material_library_hash()
|
||||
if current_material_library_hash != old_material_library_hash:
|
||||
with open(icon_hash_path, 'w', encoding='utf-8') as f:
|
||||
f.write(current_material_library_hash)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _set_pixel(self, image, x, y, color):
|
||||
data_offset = (x + int(y * image.size[0])) * 4
|
||||
image.pixels[data_offset + 0] = color[0]
|
||||
image.pixels[data_offset + 1] = color[1]
|
||||
image.pixels[data_offset + 2] = color[2]
|
||||
image.pixels[data_offset + 3] = color[3]
|
||||
|
||||
|
||||
def _draw_icon_overlay(self, image, color):
|
||||
width = image.size[0]
|
||||
height = image.size[1]
|
||||
|
||||
tri_width = 16
|
||||
size = tri_width
|
||||
for j in range(0, tri_width):
|
||||
for i in range(width - size, width):
|
||||
self._set_pixel(image, i, j, color)
|
||||
size -= 1
|
||||
|
||||
|
||||
def _write_icon_to_file(self, material_preview, color, icon_path):
|
||||
image = bpy.data.images.new(
|
||||
"iconimg",
|
||||
width=material_preview.icon_size[0],
|
||||
height=material_preview.icon_size[1],
|
||||
alpha=True
|
||||
)
|
||||
image.pixels = material_preview.icon_pixels_float
|
||||
image.filepath_raw = icon_path
|
||||
image.file_format = 'PNG'
|
||||
self._draw_icon_overlay(image, color)
|
||||
image.save()
|
||||
bpy.data.images.remove(image)
|
||||
|
||||
|
||||
def _generate_material_library_icons(self):
|
||||
imported_material_names = []
|
||||
for m in self.material_list:
|
||||
with bpy.data.libraries.load(m.path) as (data_from, data_to):
|
||||
data_to.materials = data_from.materials
|
||||
for mdata in data_to.materials:
|
||||
imported_material_names.append(mdata)
|
||||
|
||||
icon_path = os.path.join(self.library_path, "icons", m.name + ".png")
|
||||
imported_icon_path = os.path.join(self.library_path, "icons", m.name + "_imported.png")
|
||||
material = data_to.materials[0]
|
||||
self._write_icon_to_file(material.preview, [1.0, 0.25, 0.02, 1.0], icon_path)
|
||||
self._write_icon_to_file(material.preview, [0.0, 1.0, 0.23, 1.0], imported_icon_path)
|
||||
|
||||
for mname in imported_material_names:
|
||||
material = bpy.data.materials.get(mname)
|
||||
if material:
|
||||
bpy.data.materials.remove(material)
|
||||
|
||||
|
||||
def _initialize_preview_icons(self):
|
||||
require_update = self._update_material_library_hash()
|
||||
if require_update:
|
||||
self._generate_material_library_icons()
|
||||
|
||||
self.material_icons.clear()
|
||||
for m in self.material_list:
|
||||
icon_path = os.path.join(self.library_path, "icons", m.name + ".png")
|
||||
imported_icon_path = os.path.join(self.library_path, "icons", m.name + "_imported.png")
|
||||
self.material_icons.load(m.name, icon_path, 'IMAGE')
|
||||
self.material_icons.load(m.name + "_imported", imported_icon_path, 'IMAGE')
|
||||
m.icon_id = self.material_icons[m.name].icon_id
|
||||
m.imported_icon_id = self.material_icons[m.name + "_imported"].icon_id
|
||||
|
||||
|
||||
def _get_material_data_from_name(self, name):
|
||||
mdata = None
|
||||
for m in self.material_list:
|
||||
if m.name == name:
|
||||
mdata = m
|
||||
break
|
||||
return mdata
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(FLIPFluidMaterialLibraryMaterial)
|
||||
bpy.utils.register_class(FLIPFluidMaterialLibrary)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(FLIPFluidMaterialLibraryMaterial)
|
||||
bpy.utils.unregister_class(FLIPFluidMaterialLibrary)
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# 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, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, json, os
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
PointerProperty,
|
||||
CollectionProperty
|
||||
)
|
||||
|
||||
from ..presets import preset_library
|
||||
from ..utils import preset_utils
|
||||
from ..utils import version_compatibility_utils as vcu
|
||||
|
||||
|
||||
class PresetStackProperty(bpy.types.PropertyGroup):
|
||||
conv = vcu.convert_attribute_to_28
|
||||
path = StringProperty(); exec(conv("path"))
|
||||
value = StringProperty(); exec(conv("value"))
|
||||
is_value_set = BoolProperty(default=False); exec(conv("is_value_set"))
|
||||
|
||||
|
||||
def get_value(self):
|
||||
if not self.is_value_set:
|
||||
return None
|
||||
return json.loads(self.value)
|
||||
|
||||
|
||||
def set_value(self, value):
|
||||
if value is None:
|
||||
self.value = ""
|
||||
self.is_value_set = False
|
||||
return
|
||||
self.value = json.dumps(value)
|
||||
self.is_value_set = True
|
||||
|
||||
|
||||
class PresetStackMaterialInfo(bpy.types.PropertyGroup):
|
||||
conv = vcu.convert_attribute_to_28
|
||||
preset_id = StringProperty(); exec(conv("preset_id"))
|
||||
loaded_id = StringProperty(); exec(conv("loaded_id"))
|
||||
|
||||
|
||||
class PresetStackElement(bpy.types.PropertyGroup):
|
||||
conv = vcu.convert_attribute_to_28
|
||||
|
||||
is_enabled = BoolProperty(
|
||||
name="Enabled",
|
||||
description="Enable effects of preset in the stack",
|
||||
default=True,
|
||||
update=lambda self, context: self._update_is_enabled(context),
|
||||
); exec(conv("is_enabled"))
|
||||
|
||||
is_applied = BoolProperty(default=False); exec(conv("is_applied"))
|
||||
is_active = BoolProperty(default=True); exec(conv("is_active"))
|
||||
identifier = StringProperty(); exec(conv("identifier"))
|
||||
stack_uid = IntProperty(default=-1); exec(conv("stack_uid"))
|
||||
saved_properties = CollectionProperty(type=PresetStackProperty); exec(conv("saved_properties"))
|
||||
loaded_materials = CollectionProperty(type=PresetStackMaterialInfo); exec(conv("loaded_materials"))
|
||||
|
||||
|
||||
def clear(self):
|
||||
self.property_unset("is_applied")
|
||||
self.property_unset("is_enabled")
|
||||
self.property_unset("is_active")
|
||||
self.property_unset("identifier")
|
||||
self.property_unset("stack_uid")
|
||||
self._clear_collection_property(self.saved_properties)
|
||||
self._clear_collection_property(self.loaded_materials)
|
||||
|
||||
|
||||
def save_properties(self, property_paths):
|
||||
self._clear_collection_property(self.saved_properties)
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
for path in property_paths:
|
||||
value = dprops.get_property_from_path(path)
|
||||
if value is None:
|
||||
continue
|
||||
saved_property = self.saved_properties.add()
|
||||
saved_property.path = path
|
||||
saved_property.set_value(value)
|
||||
|
||||
|
||||
def apply_saved_properties(self):
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
for p in self.saved_properties:
|
||||
dprops.set_property_from_path(p.path, p.get_value())
|
||||
|
||||
|
||||
def apply_preset_properties(self):
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
pinfo = preset_library.preset_identifier_to_info(self.identifier)
|
||||
|
||||
self._load_preset_materials(pinfo)
|
||||
material_paths = preset_library.get_preset_material_paths()
|
||||
for p in pinfo['properties']:
|
||||
value = p['value']
|
||||
minfo = None
|
||||
if p['path'] in material_paths:
|
||||
minfo = self._get_loaded_material_info(value)
|
||||
if minfo is not None:
|
||||
value = minfo.loaded_id
|
||||
dprops.set_property_from_path(p['path'], value)
|
||||
|
||||
|
||||
def unload_preset_materials(self):
|
||||
preset_utils.unload_preset_materials(self.loaded_materials)
|
||||
self._clear_collection_property(self.loaded_materials)
|
||||
|
||||
|
||||
def copy(self, other):
|
||||
self.is_applied = other.is_applied
|
||||
self.identifier = other.identifier
|
||||
for otherp in other.saved_properties:
|
||||
thisp = self.saved_properties.add()
|
||||
thisp.path = otherp.path
|
||||
thisp.set_value(otherp.get_value())
|
||||
for otherm in other.loaded_materials:
|
||||
thism = self.loaded_materials.add()
|
||||
thism.preset_id = otherm.preset_id
|
||||
thism.loaded_id = otherm.loaded_id
|
||||
|
||||
|
||||
def _get_loaded_material_info(self, preset_material_id):
|
||||
for minfo in self.loaded_materials:
|
||||
if minfo.preset_id == preset_material_id:
|
||||
return minfo
|
||||
return None
|
||||
|
||||
|
||||
def _load_preset_materials(self, preset_info):
|
||||
preset_utils.load_preset_materials(preset_info, self.loaded_materials)
|
||||
|
||||
|
||||
def _update_is_enabled(self, context):
|
||||
dprops = context.scene.flip_fluid.get_domain_properties()
|
||||
stack = dprops.presets.preset_stack
|
||||
for i,p in enumerate(stack.preset_stack):
|
||||
if p.stack_uid == self.stack_uid:
|
||||
if self.is_enabled:
|
||||
stack.activate_preset(i)
|
||||
else:
|
||||
stack.deactivate_preset(i)
|
||||
|
||||
|
||||
def _clear_collection_property(self, collection):
|
||||
num_items = len(collection)
|
||||
for i in range(num_items):
|
||||
collection.remove(0)
|
||||
|
||||
|
||||
class FlipFluidPresetStack(bpy.types.PropertyGroup):
|
||||
conv = vcu.convert_attribute_to_28
|
||||
is_enabled = BoolProperty(default=False); exec(conv("is_enabled"))
|
||||
staged_preset = PointerProperty(type=PresetStackElement); exec(conv("staged_preset"))
|
||||
is_preset_staged = BoolProperty(default=False); exec(conv("is_preset_staged"))
|
||||
preset_stack = CollectionProperty(type=PresetStackElement); exec(conv("preset_stack"))
|
||||
|
||||
|
||||
def enable(self):
|
||||
self.is_enabled = True
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def disable(self):
|
||||
self.is_enabled = False
|
||||
self._unapply_preset_stack()
|
||||
|
||||
|
||||
def stage_preset(self, preset_id):
|
||||
if self.is_preset_staged and self.staged_preset.identifier == preset_id:
|
||||
return
|
||||
if self.is_preset_staged:
|
||||
self.unstage_preset()
|
||||
if preset_id == 'PRESET_NONE':
|
||||
return
|
||||
|
||||
self.staged_preset.identifier = preset_id
|
||||
self._apply_preset(self.staged_preset)
|
||||
self.is_preset_staged = True
|
||||
|
||||
|
||||
def unstage_preset(self):
|
||||
if not self.is_preset_staged:
|
||||
return
|
||||
|
||||
self._unapply_preset(self.staged_preset)
|
||||
self.staged_preset.clear()
|
||||
self.is_preset_staged = False
|
||||
|
||||
|
||||
def add_staged_preset_to_stack(self):
|
||||
if not self.is_preset_staged:
|
||||
return
|
||||
uid = self._generate_stack_uid()
|
||||
|
||||
p = self.preset_stack.add()
|
||||
p.copy(self.staged_preset)
|
||||
p.stack_uid = uid
|
||||
self.staged_preset.clear()
|
||||
self.is_preset_staged = False
|
||||
|
||||
|
||||
def remove_package_presets_from_stack(self, package_id):
|
||||
preset_enums = preset_library.get_package_preset_enums(self, bpy.context, package_id)
|
||||
preset_identifiers = []
|
||||
for e in preset_enums:
|
||||
if e[0] != 'PRESET_NONE':
|
||||
preset_identifiers.append(e[0])
|
||||
|
||||
found_presets = False
|
||||
for pe in self.preset_stack:
|
||||
if pe.identifier in preset_identifiers:
|
||||
found_presets = True
|
||||
break
|
||||
if not found_presets:
|
||||
return
|
||||
|
||||
self._unapply_preset_stack()
|
||||
for i in range(len(self.preset_stack) - 1, -1, -1):
|
||||
if self.preset_stack[i].identifier in preset_identifiers:
|
||||
self.preset_stack.remove(i)
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def remove_preset_from_stack_by_identifier(self, preset_identifier):
|
||||
for idx,se in enumerate(self.preset_stack):
|
||||
if se.identifier == preset_identifier:
|
||||
self.remove_preset_from_stack(idx)
|
||||
return idx
|
||||
return -1
|
||||
|
||||
|
||||
def remove_preset_from_stack(self, stack_index):
|
||||
if stack_index < 0 or stack_index >= len(self.preset_stack):
|
||||
return
|
||||
self._unapply_preset_stack()
|
||||
self.preset_stack.remove(stack_index)
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def apply_and_remove_preset_from_stack(self, stack_index):
|
||||
if stack_index < 0 or stack_index >= len(self.preset_stack):
|
||||
return
|
||||
p = self.preset_stack[stack_index]
|
||||
for minfo in p.loaded_materials:
|
||||
m = bpy.data.materials.get(minfo.loaded_id)
|
||||
if m is not None:
|
||||
m.flip_fluid.skip_preset_unload = True
|
||||
self.preset_stack.remove(stack_index)
|
||||
|
||||
|
||||
def move_preset_up_in_stack(self, stack_index):
|
||||
if stack_index <= 0 or stack_index >= len(self.preset_stack):
|
||||
return
|
||||
self._unapply_preset_stack()
|
||||
self.preset_stack.move(stack_index, stack_index - 1)
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def move_preset_down_in_stack(self, stack_index):
|
||||
if stack_index < 0 or stack_index >= len(self.preset_stack) - 1:
|
||||
return
|
||||
self._unapply_preset_stack()
|
||||
self.preset_stack.move(stack_index, stack_index + 1)
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def insert_preset_into_stack(self, preset_identifier, stack_index):
|
||||
#if stack_index < 0 or stack_index >= len(self.preset_stack) - 1:
|
||||
# return
|
||||
self._unapply_preset_stack()
|
||||
self.stage_preset(preset_identifier)
|
||||
self.add_staged_preset_to_stack()
|
||||
self.preset_stack.move(len(self.preset_stack) - 1, stack_index)
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def activate_preset(self, stack_index):
|
||||
if stack_index < 0 or stack_index >= len(self.preset_stack):
|
||||
return
|
||||
if self.preset_stack[stack_index].is_active:
|
||||
return
|
||||
self._unapply_preset_stack()
|
||||
self.preset_stack[stack_index].is_active = True
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def deactivate_preset(self, stack_index):
|
||||
if stack_index < 0 or stack_index >= len(self.preset_stack):
|
||||
return
|
||||
if not self.preset_stack[stack_index].is_active:
|
||||
return
|
||||
self._unapply_preset_stack()
|
||||
self.preset_stack[stack_index].is_active = False
|
||||
self._apply_preset_stack()
|
||||
|
||||
|
||||
def is_preset_in_stack(self, preset_identifier):
|
||||
for pe in self.preset_stack:
|
||||
if pe.identifier == preset_identifier:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def validate_stack(self):
|
||||
preset_info = preset_library.get_preset_info_list()
|
||||
valid_identifiers = [info['identifier'] for info in preset_info]
|
||||
for i in range(len(self.preset_stack) - 1, -1, -1):
|
||||
pe = self.preset_stack[i]
|
||||
if pe.identifier not in valid_identifiers:
|
||||
errmsg = ("Preset Stack Warning: preset <" + pe.identifier +
|
||||
"> not found on system, removing from stack")
|
||||
print(errmsg)
|
||||
self.preset_stack.remove(i)
|
||||
|
||||
|
||||
def _apply_preset(self, stack_element):
|
||||
pinfo = preset_library.preset_identifier_to_info(stack_element.identifier)
|
||||
|
||||
property_paths = []
|
||||
for p in pinfo['properties']:
|
||||
property_paths.append(p['path'])
|
||||
stack_element.save_properties(property_paths)
|
||||
stack_element.apply_preset_properties()
|
||||
|
||||
stack_element.is_applied = True
|
||||
|
||||
|
||||
def _unapply_preset(self, stack_element):
|
||||
stack_element.apply_saved_properties()
|
||||
|
||||
pid = stack_element.identifier
|
||||
preset_count = 0
|
||||
if self.is_preset_staged and self.staged_preset.identifier == pid:
|
||||
if self.staged_preset.is_applied:
|
||||
preset_count +=1
|
||||
for p in self.preset_stack:
|
||||
if p.identifier == pid and p.is_applied:
|
||||
preset_count += 1
|
||||
if preset_count == 1:
|
||||
stack_element.unload_preset_materials()
|
||||
|
||||
stack_element.is_applied = False
|
||||
|
||||
|
||||
def _apply_preset_stack(self):
|
||||
for p in self.preset_stack:
|
||||
if not p.is_applied and p.is_active:
|
||||
self._apply_preset(p)
|
||||
if self.is_preset_staged and not self.staged_preset.is_applied:
|
||||
self._apply_preset(self.staged_preset)
|
||||
|
||||
|
||||
def _unapply_preset_stack(self):
|
||||
if self.is_preset_staged and self.staged_preset.is_applied:
|
||||
self._unapply_preset(self.staged_preset)
|
||||
for p in reversed(self.preset_stack):
|
||||
if p.is_applied and p.is_active:
|
||||
self._unapply_preset(p)
|
||||
|
||||
def _generate_stack_uid(self):
|
||||
ids = [x.stack_uid for x in self.preset_stack]
|
||||
for i in range(0, 1000000):
|
||||
if not i in ids:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(PresetStackProperty)
|
||||
bpy.utils.register_class(PresetStackMaterialInfo)
|
||||
bpy.utils.register_class(PresetStackElement)
|
||||
bpy.utils.register_class(FlipFluidPresetStack)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(PresetStackProperty)
|
||||
bpy.utils.unregister_class(PresetStackMaterialInfo)
|
||||
bpy.utils.unregister_class(PresetStackElement)
|
||||
bpy.utils.unregister_class(FlipFluidPresetStack)
|
||||
Reference in New Issue
Block a user