2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,250 @@
# 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/>.
bl_info = {
"name" : "FLIP Fluids",
"description": "A FLIP Fluid Simulation Tool for Blender (v1.8.4 Release 2025-07-17)",
"author" : "Ryan Guy & Dennis Fassbaender <support[at]flipfluids.com>",
"version" : (1, 8, 4),
"blender" : (3, 6, 0),
"location" : "Properties > Physics > FLIP Fluid",
"warning" : "",
"wiki_url" : "https://github.com/rlguy/Blender-FLIP-Fluids/wiki",
"doc_url" : "https://github.com/rlguy/Blender-FLIP-Fluids/wiki",
"category" : "Animation"
}
if "bpy" in locals():
import importlib
reloadable_modules = [
'filesystem',
'utils',
'objects',
'materials',
'properties',
'operators',
'ui',
'presets',
'export',
'bake',
'render',
'exit_handler'
]
for module_name in reloadable_modules:
if module_name in locals():
importlib.reload(locals()[module_name])
import bpy, atexit, shutil, os
from bpy.props import (
PointerProperty,
StringProperty
)
from . import (
filesystem,
utils,
objects,
materials,
properties,
operators,
ui,
presets,
export,
bake,
render,
exit_handler
)
from .utils import installation_utils
from .utils import version_compatibility_utils as vcu
@bpy.app.handlers.persistent
def scene_update_post(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
installation_utils.scene_update_post(scene)
if installation_utils.is_addon_active():
if not render.is_rendering():
# We don't want to update these while rendering to prevent
# odd behaviour in the depsgraph
properties.scene_update_post(scene)
materials.scene_update_post(scene)
render.scene_update_post(scene)
@bpy.app.handlers.persistent
def render_init(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_init(scene)
@bpy.app.handlers.persistent
def render_complete(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_complete(scene)
@bpy.app.handlers.persistent
def render_cancel(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_cancel(scene)
@bpy.app.handlers.persistent
def frame_change_pre(scene, depsgraph=None):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
@bpy.app.handlers.persistent
def frame_change_post(scene, depsgraph=None):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.frame_change_post(scene, depsgraph)
render.frame_change_post(scene, depsgraph)
@bpy.app.handlers.persistent
def render_pre(scene, depsgraph=None):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_pre(scene)
@bpy.app.handlers.persistent
def load_pre(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.load_pre()
@bpy.app.handlers.persistent
def load_post(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
if vcu.is_blender_28() and not vcu.is_blender_281():
print("FLIP FLUIDS WARNING: Blender 2.80 contains bugs that can cause frequent crashes during render, Alembic export, and rigid/cloth simulation baking. Blender version 2.81 or higher is recommended.")
installation_utils.load_post()
materials.load_post()
properties.load_post()
presets.load_post()
exit_handler.load_post()
@bpy.app.handlers.persistent
def save_pre(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.save_pre()
@bpy.app.handlers.persistent
def save_post(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.save_post()
exit_handler.save_post()
def on_exit():
exit_handler.on_exit()
class FlipFluidCompleteInstallation(bpy.types.Operator):
bl_idname = "flip_fluid_operators.complete_installation"
bl_label = "Complete Installation"
bl_description = ("Click to complete the installation of the FLIP Fluids addon. Alternatively, restarting Blender or re-loading the Blend file will also complete the installation process")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
load_post(None)
return {'FINISHED'}
def register():
objects.register()
materials.register()
properties.register()
operators.register()
ui.register()
presets.register()
if vcu.is_blender_28():
bpy.app.handlers.depsgraph_update_post.append(scene_update_post)
else:
bpy.app.handlers.scene_update_post.append(scene_update_post)
bpy.app.handlers.render_init.append(render_init)
bpy.app.handlers.render_complete.append(render_complete)
bpy.app.handlers.render_cancel.append(render_cancel)
bpy.app.handlers.frame_change_pre.append(frame_change_pre)
bpy.app.handlers.frame_change_post.append(frame_change_post)
bpy.app.handlers.render_pre.append(render_pre)
bpy.app.handlers.load_pre.append(load_pre)
bpy.app.handlers.load_post.append(load_post)
bpy.app.handlers.save_pre.append(save_pre)
bpy.app.handlers.save_post.append(save_post)
atexit.register(on_exit)
bpy.utils.register_class(FlipFluidCompleteInstallation)
def unregister():
objects.unregister()
materials.unregister()
properties.unregister()
operators.unregister()
ui.unregister()
presets.unregister()
if vcu.is_blender_28():
bpy.app.handlers.depsgraph_update_post.remove(scene_update_post)
else:
bpy.app.handlers.scene_update_post.remove(scene_update_post)
bpy.app.handlers.render_init.remove(render_init)
bpy.app.handlers.render_complete.remove(render_complete)
bpy.app.handlers.render_cancel.remove(render_cancel)
bpy.app.handlers.frame_change_pre.remove(frame_change_pre)
bpy.app.handlers.frame_change_post.remove(frame_change_post)
bpy.app.handlers.render_pre.remove(render_pre)
bpy.app.handlers.load_pre.remove(load_pre)
bpy.app.handlers.load_post.remove(load_post)
bpy.app.handlers.save_pre.remove(save_pre)
bpy.app.handlers.save_post.remove(save_post)
atexit.unregister(on_exit)
bpy.utils.unregister_class(FlipFluidCompleteInstallation)
@@ -0,0 +1,250 @@
# 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/>.
bl_info = {
"name" : "FLIP Fluids",
"description": "A FLIP Fluid Simulation Tool for Blender (v@FLUIDENGINE_VERSION_LABEL@)",
"author" : "Ryan Guy & Dennis Fassbaender <support[at]flipfluids.com>",
"version" : (@FLUIDENGINE_VERSION_MAJOR@, @FLUIDENGINE_VERSION_MINOR@, @FLUIDENGINE_VERSION_REVISION@),
"blender" : (3, 6, 0),
"location" : "Properties > Physics > FLIP Fluid",
"warning" : "",
"wiki_url" : "https://github.com/rlguy/Blender-FLIP-Fluids/wiki",
"doc_url" : "https://github.com/rlguy/Blender-FLIP-Fluids/wiki",
"category" : "Animation"
}
if "bpy" in locals():
import importlib
reloadable_modules = [
'filesystem',
'utils',
'objects',
'materials',
'properties',
'operators',
'ui',
'presets',
'export',
'bake',
'render',
'exit_handler'
]
for module_name in reloadable_modules:
if module_name in locals():
importlib.reload(locals()[module_name])
import bpy, atexit, shutil, os
from bpy.props import (
PointerProperty,
StringProperty
)
from . import (
filesystem,
utils,
objects,
materials,
properties,
operators,
ui,
presets,
export,
bake,
render,
exit_handler
)
from .utils import installation_utils
from .utils import version_compatibility_utils as vcu
@bpy.app.handlers.persistent
def scene_update_post(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
installation_utils.scene_update_post(scene)
if installation_utils.is_addon_active():
if not render.is_rendering():
# We don't want to update these while rendering to prevent
# odd behaviour in the depsgraph
properties.scene_update_post(scene)
materials.scene_update_post(scene)
render.scene_update_post(scene)
@bpy.app.handlers.persistent
def render_init(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_init(scene)
@bpy.app.handlers.persistent
def render_complete(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_complete(scene)
@bpy.app.handlers.persistent
def render_cancel(scene):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_cancel(scene)
@bpy.app.handlers.persistent
def frame_change_pre(scene, depsgraph=None):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
@bpy.app.handlers.persistent
def frame_change_post(scene, depsgraph=None):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.frame_change_post(scene, depsgraph)
render.frame_change_post(scene, depsgraph)
@bpy.app.handlers.persistent
def render_pre(scene, depsgraph=None):
if scene.flip_fluid.is_addon_disabled_in_blend_file():
return
render.render_pre(scene)
@bpy.app.handlers.persistent
def load_pre(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.load_pre()
@bpy.app.handlers.persistent
def load_post(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
if vcu.is_blender_28() and not vcu.is_blender_281():
print("FLIP FLUIDS WARNING: Blender 2.80 contains bugs that can cause frequent crashes during render, Alembic export, and rigid/cloth simulation baking. Blender version 2.81 or higher is recommended.")
installation_utils.load_post()
materials.load_post()
properties.load_post()
presets.load_post()
exit_handler.load_post()
@bpy.app.handlers.persistent
def save_pre(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.save_pre()
@bpy.app.handlers.persistent
def save_post(nonedata):
if bpy.context.scene.flip_fluid.is_addon_disabled_in_blend_file():
return
properties.save_post()
exit_handler.save_post()
def on_exit():
exit_handler.on_exit()
class FlipFluidCompleteInstallation(bpy.types.Operator):
bl_idname = "flip_fluid_operators.complete_installation"
bl_label = "Complete Installation"
bl_description = ("Click to complete the installation of the FLIP Fluids addon. Alternatively, restarting Blender or re-loading the Blend file will also complete the installation process")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
load_post(None)
return {'FINISHED'}
def register():
objects.register()
materials.register()
properties.register()
operators.register()
ui.register()
presets.register()
if vcu.is_blender_28():
bpy.app.handlers.depsgraph_update_post.append(scene_update_post)
else:
bpy.app.handlers.scene_update_post.append(scene_update_post)
bpy.app.handlers.render_init.append(render_init)
bpy.app.handlers.render_complete.append(render_complete)
bpy.app.handlers.render_cancel.append(render_cancel)
bpy.app.handlers.frame_change_pre.append(frame_change_pre)
bpy.app.handlers.frame_change_post.append(frame_change_post)
bpy.app.handlers.render_pre.append(render_pre)
bpy.app.handlers.load_pre.append(load_pre)
bpy.app.handlers.load_post.append(load_post)
bpy.app.handlers.save_pre.append(save_pre)
bpy.app.handlers.save_post.append(save_post)
atexit.register(on_exit)
bpy.utils.register_class(FlipFluidCompleteInstallation)
def unregister():
objects.unregister()
materials.unregister()
properties.unregister()
operators.unregister()
ui.unregister()
presets.unregister()
if vcu.is_blender_28():
bpy.app.handlers.depsgraph_update_post.remove(scene_update_post)
else:
bpy.app.handlers.scene_update_post.remove(scene_update_post)
bpy.app.handlers.render_init.remove(render_init)
bpy.app.handlers.render_complete.remove(render_complete)
bpy.app.handlers.render_cancel.remove(render_cancel)
bpy.app.handlers.frame_change_pre.remove(frame_change_pre)
bpy.app.handlers.frame_change_post.remove(frame_change_post)
bpy.app.handlers.render_pre.remove(render_pre)
bpy.app.handlers.load_pre.remove(load_pre)
bpy.app.handlers.load_post.remove(load_post)
bpy.app.handlers.save_pre.remove(save_pre)
bpy.app.handlers.save_post.remove(save_post)
atexit.unregister(on_exit)
bpy.utils.unregister_class(FlipFluidCompleteInstallation)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
schema_version = "1.0.0"
id = "flip_fluids_addon"
type = "add-on"
version = "1.8.4"
name = "FLIP Fluids"
tagline = "A FLIP Fluid Simulation Tool for Blender (v1.8.4 Release 2025-07-17)"
maintainer = "Ryan Guy & Dennis Fassbaender // support@flipfluids.com"
website = "https://github.com/rlguy/Blender-FLIP-Fluids/wiki"
tags = ["Animation", "Physics"]
blender_version_min = "4.2.0"
license = ["SPDX:GPL-3.0-or-later"]
platforms = ["windows-x64", "macos-x64", "macos-arm64", "linux-x64"]
[permissions]
files = "Read and write simulation cache files to the filesystem"
clipboard = "Copy and paste render and bake commands"
@@ -0,0 +1,17 @@
schema_version = "1.0.0"
id = "flip_fluids_addon"
type = "add-on"
version = "@FLUIDENGINE_VERSION_MAJOR@.@FLUIDENGINE_VERSION_MINOR@.@FLUIDENGINE_VERSION_REVISION@"
name = "FLIP Fluids"
tagline = "A FLIP Fluid Simulation Tool for Blender (v@FLUIDENGINE_VERSION_LABEL@)"
maintainer = "Ryan Guy & Dennis Fassbaender // support@flipfluids.com"
website = "https://github.com/rlguy/Blender-FLIP-Fluids/wiki"
tags = ["Animation", "Physics"]
blender_version_min = "4.2.0"
license = ["SPDX:GPL-3.0-or-later"]
platforms = ["windows-x64", "macos-x64", "macos-arm64", "linux-x64"]
[permissions]
files = "Read and write simulation cache files to the filesystem"
clipboard = "Copy and paste render and bake commands"
@@ -0,0 +1,45 @@
# 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, shutil
# These variables are used when running an exit handler where
# access to Blender data may no longer be available
IS_BLEND_FILE_SAVED = False
CACHE_DIRECTORY = ""
def on_exit():
# nothing currently to do on exit
pass
def save_post():
global IS_BLEND_FILE_SAVED
IS_BLEND_FILE_SAVED = True
def load_post():
global IS_BLEND_FILE_SAVED
base = os.path.basename(bpy.data.filepath)
save_file = os.path.splitext(base)[0]
is_unsaved = not base or not save_file
IS_BLEND_FILE_SAVED = not is_unsaved
def set_cache_directory(dirpath):
global CACHE_DIRECTORY
CACHE_DIRECTORY = dirpath
@@ -0,0 +1,480 @@
# 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, math, array, json, os, zipfile, shutil
from mathutils import Vector
from .objects import flip_fluid_map
from .objects.flip_fluid_geometry_exporter import GeometryExportObject, MotionExportType, GeometryExportType
from .utils import export_utils as utils
from .objects.flip_fluid_aabb import AABB
from .ffengine import TriangleMesh
from .utils import version_compatibility_utils as vcu
from .utils import cache_utils, export_utils, installation_utils
def __get_domain_object():
return bpy.context.scene.flip_fluid.get_domain_object()
def __get_domain_properties():
return bpy.context.scene.flip_fluid.get_domain_properties()
def __export_simulation_data_to_file(context, simobjects, filename):
success, data = __get_simulation_data_dict(context, simobjects)
if not success:
return False
jsonstr = json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, 'w', encoding='utf-8') as f:
f.write(jsonstr)
return True
def __get_simulation_data_dict(context, simobjects):
data = {}
success, data['domain_data'] = __get_domain_data_dict(context, simobjects.domain)
if not success:
return False, None
data['fluid_data'] = __get_fluid_data(context, simobjects.fluid_objects)
data['obstacle_data'] = __get_obstacle_data(context, simobjects.obstacle_objects)
data['inflow_data'] = __get_inflow_data(context, simobjects.inflow_objects)
data['outflow_data'] = __get_outflow_data(context, simobjects.outflow_objects)
data['force_field_data'] = __get_force_field_data(context, simobjects.force_field_objects)
return True, data
def __get_domain_data_dict(context, dobj):
dprops = dobj.flip_fluid.domain
d = utils.flip_fluid_object_to_dict(dobj, dprops)
# A KeyError in this dict at this point may indicate that the FLIP Fluids addon installation
# was not completed by restarting Blender, or that the addon version may not be compatible
# with a newer version of Blender.
if not 'advanced' in d:
errmsg = "This error may indicate that either (1) A Blender restart is required "
errmsg += "to complete installation of the FLIP Fluids addon. Save, restart Blender, and "
errmsg += "try again. Or (2) This version of the FLIP Fluids addon is not compatible "
errmsg += "with the Blender version. Update to the latest version of the FLIP Fluids "
errmsg += "addon and try again. Contact the developers at support@flipfluids.com for "
errmsg += "assistance."
bpy.ops.flip_fluid_operators.display_error(
'INVOKE_DEFAULT',
error_message="Installation or Compatibility Error",
error_description=errmsg,
popup_width=600
)
return False, None
initialize_properties = {}
initialize_properties['name'] = dobj.name
bbox = AABB.from_blender_object(dobj)
isize, jsize, ksize, viewport_dx = dprops.simulation.get_viewport_grid_dimensions()
_, _, _, simulation_dx = dprops.simulation.get_simulation_grid_dimensions()
simulation_preview_dx = dprops.simulation.get_simulation_preview_dx()
initialize_properties['isize'] = isize
initialize_properties['jsize'] = jsize
initialize_properties['ksize'] = ksize
dwidth = initialize_properties['isize'] * viewport_dx
dheight = initialize_properties['jsize'] * viewport_dx
ddepth = initialize_properties['ksize'] * viewport_dx
initialize_properties['bbox'] = AABB(bbox.x, bbox.y, bbox.z,
dwidth, dheight, ddepth).to_dict()
initialize_properties['scale'] = dprops.world.get_world_scale()
initialize_properties['dx'] = simulation_dx
initialize_properties['preview_dx'] = simulation_preview_dx
initialize_properties['upscale_simulation'] = dprops.simulation.is_current_grid_upscaled()
if initialize_properties['upscale_simulation']:
initialize_properties['savestate_isize'] = dprops.simulation.savestate_isize
initialize_properties['savestate_jsize'] = dprops.simulation.savestate_jsize
initialize_properties['savestate_ksize'] = dprops.simulation.savestate_ksize
initialize_properties['savestate_dx'] = dprops.simulation.savestate_dx
initialize_properties['logfile_name'] = dprops.cache.logfile_name
initialize_properties['frame_start'] = dprops.simulation.frame_start
initialize_properties['frame_end'] = dprops.simulation.frame_end
initialize_properties['enable_savestates'] = dprops.simulation.enable_savestates
initialize_properties['savestate_interval'] = dprops.simulation.savestate_interval
initialize_properties['delete_outdated_savestates'] = dprops.simulation.delete_outdated_savestates
initialize_properties['delete_outdated_meshes'] = dprops.simulation.delete_outdated_meshes
initialize_properties['geometry_database_filepath'] = dprops.cache.get_geometry_database_abspath()
d['initialize'] = initialize_properties
dprops.advanced.initialize_num_threads_auto_detect()
d['advanced']['num_threads_auto_detect'] = dprops.advanced.num_threads_auto_detect
d['simulation']['frames_per_second'] = dprops.simulation.get_frame_rate_data_dict()
d['simulation']['time_scale'] = dprops.simulation.get_time_scale_data_dict()
d['world']['gravity'] = dprops.world.get_gravity_data_dict()
d['world']['scene_use_gravity'] = dprops.world.get_scene_use_gravity_data_dict()
d['world']['native_surface_tension_scale'] = dprops.world.native_surface_tension_scale
d['world']['minimum_surface_tension_cfl'] = dprops.world.minimum_surface_tension_cfl
d['world']['maximum_surface_tension_cfl'] = dprops.world.maximum_surface_tension_cfl
d['surface']['native_particle_scale'] = dprops.surface.native_particle_scale
d['surface']['compute_chunks_auto'] = dprops.surface.compute_chunks_auto
meshing_volume_object_name = ""
meshing_volume_object = dprops.surface.get_meshing_volume_object()
if meshing_volume_object is not None:
meshing_volume_object_name = meshing_volume_object.name
d['surface']['meshing_volume_object'] = meshing_volume_object_name
installation_utils.update_mixbox_installation_status()
is_mixbox_supported = installation_utils.is_mixbox_supported()
is_mixbox_installed = installation_utils.is_mixbox_installation_complete()
color_mixing_mode = dprops.surface.color_attribute_mixing_mode
if not is_mixbox_supported or not is_mixbox_installed:
color_mixing_mode = 'COLOR_MIXING_MODE_RGB'
d['surface']['color_attribute_mixing_mode'] = color_mixing_mode
return True, d
def __get_fluid_data(context, objects):
d = []
for idx, obj in enumerate(objects):
fprops = obj.flip_fluid.fluid
data = utils.flip_fluid_object_to_dict(obj, obj.flip_fluid.fluid)
data['name'] = obj.name
target_object = fprops.get_target_object()
target_object_name = ""
if target_object:
target_object_name = target_object.name
data['target_object'] = target_object_name
d.append(data)
return d
def __get_obstacle_data(context, objects):
d = []
for idx, obj in enumerate(objects):
data = utils.flip_fluid_object_to_dict(obj, obj.flip_fluid.obstacle)
data['name'] = obj.name
d.append(data)
return d
def __get_inflow_data(context, objects):
d = []
for idx, obj in enumerate(objects):
props = obj.flip_fluid.inflow
data = utils.flip_fluid_object_to_dict(obj, obj.flip_fluid.inflow)
data['name'] = obj.name
target_object = props.get_target_object()
target_object_name = ""
if target_object:
target_object_name = target_object.name
data['target_object'] = target_object_name
d.append(data)
return d
def __get_outflow_data(context, objects):
d = []
for idx, obj in enumerate(objects):
data = utils.flip_fluid_object_to_dict(obj, obj.flip_fluid.outflow)
data['name'] = obj.name
d.append(data)
return d
def __get_force_field_data(context, objects):
d = []
for idx, obj in enumerate(objects):
data = utils.flip_fluid_object_to_dict(obj, obj.flip_fluid.force_field)
data['name'] = obj.name
d.append(data)
return d
def __format_bytes(num):
# Method adapted from: http://stackoverflow.com/a/10171475
unit_list = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']
decimal_list = [0, 0, 1, 2, 2, 2]
if num > 1:
exponent = min(int(math.log(num, 1024)), len(unit_list) - 1)
quotient = float(num) / 1024**exponent
unit, num_decimals = unit_list[exponent], decimal_list[exponent]
format_string = '{:.%sf} {}' % (num_decimals)
return format_string.format(quotient, unit)
if num == 0:
return '0 bytes'
if num == 1:
return '1 byte'
def __export_static_mesh_data(object_data, mesh_directory):
mesh_data = object_data['data']['mesh_data']
bobj_data = mesh_data.to_bobj()
filepath = os.path.join(mesh_directory, "mesh.bobj")
with open(filepath, 'wb') as mesh_file:
mesh_file.write(bobj_data)
info = {'mesh_type': 'STATIC'}
info_json = json.dumps(info, sort_keys=True)
info_filepath = os.path.join(mesh_directory, "mesh.info")
with open(info_filepath, 'w', encoding='utf-8') as f:
f.write(info_json)
dprops = __get_domain_properties()
if dprops.debug.display_console_output:
export_str = "Exporting static mesh: <" + object_data['name'] + ">, "
export_str += "verts: " + str(len(mesh_data.vertices) // 3)
export_str += ", tris: " + str(len(mesh_data.triangles) // 3)
export_str += ", filesize: " + __format_bytes(len(bobj_data))
print(export_str)
object_data['data']['mesh_data'] = None
def __export_keyframed_mesh_data(object_data, mesh_directory):
mesh_data = object_data['data']['mesh_data']
bobj_data = mesh_data.to_bobj()
mesh_filepath = os.path.join(mesh_directory, "mesh.bobj")
with open(mesh_filepath, 'wb') as mesh_file:
mesh_file.write(bobj_data)
matrix_data = object_data['data']['matrix_data']
matrix_json = json.dumps(matrix_data)
matrix_filepath = os.path.join(mesh_directory, "transforms.data")
with open(matrix_filepath, 'w', encoding='utf-8') as f:
f.write(matrix_json)
info = {
'mesh_type': 'KEYFRAMED',
'frame_start': min(matrix_data.keys()),
'frame_end': max(matrix_data.keys())
}
info_json = json.dumps(info, sort_keys=True)
info_filepath = os.path.join(mesh_directory, "mesh.info")
with open(info_filepath, 'w', encoding='utf-8') as f:
f.write(info_json)
matrix_filesize = os.stat(matrix_filepath).st_size
filesize = len(bobj_data) + matrix_filesize
dprops = __get_domain_properties()
if dprops.debug.display_console_output:
export_str = "Exporting keyframed mesh: <" + object_data['name'] + ">, "
export_str += "numframes: " + str(len(matrix_data))
export_str += ", verts: " + str(len(mesh_data.vertices) // 3)
export_str += ", tris: " + str(len(mesh_data.triangles) // 3)
export_str += ", filesize: " + __format_bytes(filesize)
print(export_str)
object_data['data']['mesh_data'] = None
object_data['data']['matrix_data'] = None
def __export_animated_mesh_data(object_data, mesh_directory):
mesh_data = object_data['data']['mesh_data']
frame_data = object_data['data']['frame_data']
dprops = __get_domain_properties()
for i, mesh in enumerate(mesh_data):
bobj_data = mesh.to_bobj()
frameno = frame_data[i]
mesh_name = "mesh" + str(frameno).zfill(6) + ".bobj"
filepath = os.path.join(mesh_directory, mesh_name)
with open(filepath, 'wb') as mesh_file:
mesh_file.write(bobj_data)
if dprops.debug.display_console_output:
export_str = "Exporting animated mesh: <" + object_data['name'] + ">, "
export_str += "frame: " + str(frameno)
export_str += ", verts: " + str(len(mesh.vertices) // 3)
export_str += ", tris: " + str(len(mesh.triangles) // 3)
export_str += ", filesize: " + __format_bytes(len(bobj_data))
print(export_str)
files = os.listdir(mesh_directory)
files = [f.split('.')[0][-6:] for f in files]
file_numbers = [int(f) for f in files if f.isdigit()]
info = {
'mesh_type': 'ANIMATED',
'frame_start': min(file_numbers),
'frame_end': max(file_numbers)
}
info_json = json.dumps(info, sort_keys=True)
info_filepath = os.path.join(mesh_directory, "mesh.info")
with open(info_filepath, 'w', encoding='utf-8') as f:
f.write(info_json)
object_data['data']['mesh_data'] = []
frame_data = object_data['data']['frame_data'] = []
def __initialize_export_object_geometry_types(export_object):
bl_object = export_object.get_blender_object()
if bl_object.type == 'MESH':
export_object.add_geometry_export_type(GeometryExportType.MESH)
elif bl_object.type == 'CURVE':
export_object.add_geometry_export_type(GeometryExportType.CURVE)
elif bl_object.type == 'EMPTY':
export_object.add_geometry_export_type(GeometryExportType.CENTROID)
fprops = bl_object.flip_fluid
if fprops.is_fluid() or fprops.is_inflow() or fprops.is_force_field():
export_object.add_geometry_export_type(GeometryExportType.AXIS)
def __generate_export_object(bl_object):
export_object = GeometryExportObject(bl_object.name)
__initialize_export_object_geometry_types(export_object)
return export_object
def __get_target_object_export_type(bl_target, bl_target_parent):
parent_props = bl_target_parent.flip_fluid.get_property_group()
if hasattr(parent_props, 'export_animated_target') and parent_props.export_animated_target:
return MotionExportType.ANIMATED
if export_utils.is_object_keyframe_animated(bl_target):
return MotionExportType.KEYFRAMED
return MotionExportType.STATIC
def __generate_target_export_object(bl_target_object, bl_target_parent_object):
export_object = GeometryExportObject(bl_target_object.name)
export_type = __get_target_object_export_type(bl_target_object, bl_target_parent_object)
export_object.set_motion_export_type(export_type)
export_object.add_geometry_export_type(GeometryExportType.CENTROID)
return export_object
def __get_meshing_volume_object_export_type(bl_meshing_volume, bl_domain):
dprops = bl_domain.flip_fluid.get_property_group()
if dprops.surface.export_animated_meshing_volume_object:
return MotionExportType.ANIMATED
if export_utils.is_object_keyframe_animated(bl_meshing_volume):
return MotionExportType.KEYFRAMED
return MotionExportType.STATIC
def __generate_meshing_volume_export_object(bl_meshing_object, bl_domain_object):
export_object = GeometryExportObject(bl_meshing_object.name)
export_type = __get_meshing_volume_object_export_type(bl_meshing_object, bl_domain_object)
export_object.set_motion_export_type(export_type)
export_object.add_geometry_export_type(GeometryExportType.MESH)
return export_object
def add_objects_to_geometry_exporter(geometry_exporter):
domain = bpy.context.scene.flip_fluid.get_domain_object()
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
# Objects disabled in the viewport shouldn't be exported for the simulation, so skip these
objects = bpy.context.scene.flip_fluid.get_simulation_objects(skip_hide_viewport=True)
disable_topology_warning = dprops.advanced.disable_changing_topology_warning
# Add regular FLIP Fluid objects
for obj in objects:
props = obj.flip_fluid.get_property_group()
export_object = __generate_export_object(obj)
is_dynamic_topology_exception = False
if obj.flip_fluid.is_force_field():
is_dynamic_topology_exception = True
elif obj.flip_fluid.is_inflow():
if not obj.flip_fluid.inflow.append_object_velocity:
is_dynamic_topology_exception = True
elif obj.flip_fluid.is_fluid():
if not obj.flip_fluid.fluid.append_object_velocity:
is_dynamic_topology_exception = True
elif obj.flip_fluid.is_outflow():
is_dynamic_topology_exception = True
skip_reexport = hasattr(props, "skip_reexport") and props.skip_reexport
force_reexport = hasattr(props, "force_reexport_on_next_bake") and props.force_reexport_on_next_bake
skip_reexport = skip_reexport and not force_reexport
export_object.skip_reexport = skip_reexport and not force_reexport
export_object.disable_changing_topology_warning = disable_topology_warning or is_dynamic_topology_exception
geometry_exporter.add_geometry_export_object(export_object)
# Add Fluid/Inflow target objects
for obj in objects:
if not obj.flip_fluid.is_fluid() and not obj.flip_fluid.is_inflow():
continue
props = obj.flip_fluid.get_property_group()
if not props.is_target_valid():
continue
target_object = props.get_target_object()
export_object = __generate_target_export_object(target_object, obj)
export_object.disable_changing_topology_warning = True
geometry_exporter.add_geometry_export_object(export_object)
# Add Meshing Volume object
if dprops.surface.is_meshing_volume_object_valid():
meshing_volume_object = dprops.surface.get_meshing_volume_object()
if meshing_volume_object is not None:
export_object = __generate_meshing_volume_export_object(meshing_volume_object, domain)
export_object.disable_changing_topology_warning = True
geometry_exporter.add_geometry_export_object(export_object)
geometry_exporter.initialize()
def export_simulation_data(context, data_filepath):
domain_object = __get_domain_object()
dprops = __get_domain_properties()
if domain_object is None:
return False
simprops = bpy.context.scene.flip_fluid
simulation_objects = flip_fluid_map.Map({})
simulation_objects.domain = domain_object
# Objects disabled in the viewport shouldn't be exported for the simulation, so skip these
simulation_objects.fluid_objects = simprops.get_fluid_objects(skip_hide_viewport=True)
simulation_objects.obstacle_objects = simprops.get_obstacle_objects(skip_hide_viewport=True)
simulation_objects.inflow_objects = simprops.get_inflow_objects(skip_hide_viewport=True)
simulation_objects.outflow_objects = simprops.get_outflow_objects(skip_hide_viewport=True)
simulation_objects.force_field_objects = simprops.get_force_field_objects(skip_hide_viewport=True)
success = __export_simulation_data_to_file(context, simulation_objects, data_filepath)
return success
@@ -0,0 +1,37 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from . import ffengine
from .aabb import AABB, AABB_t
from .fluidsimulation import FluidSimulation, MarkerParticle_t, DiffuseParticle_t
from .meshobject import MeshObject
from .meshfluidsource import MeshFluidSource
from .forcefieldgrid import ForceFieldGrid
from .forcefield import ForceField
from .forcefieldpoint import ForceFieldPoint
from .forcefieldsurface import ForceFieldSurface
from .forcefieldvolume import ForceFieldVolume
from .forcefieldcurve import ForceFieldCurve
from .trianglemesh import TriangleMesh, TriangleMesh_t
from .gridindex import GridIndex, GridIndex_t
from .vector3 import Vector3, Vector3_t
from . import mixbox
@@ -0,0 +1,237 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .vector3 import Vector3, Vector3_t
from .gridindex import GridIndex
from . import method_decorators as decorators
import ctypes
class AABB_t(ctypes.Structure):
_fields_ = [("position", Vector3_t),
("width", ctypes.c_float),
("height", ctypes.c_float),
("depth", ctypes.c_float)]
class AABB(object):
def __init__(self, *args):
if len(args) == 4 and isinstance(args[0], Vector3):
self.position = args[0]
self.width = args[1]
self.height = args[2]
self.depth = args[3]
elif len(args) == 6:
self._position = Vector3(args[0], args[1], args[2])
self.width = args[3]
self.height = args[4]
self.depth = args[5]
elif len(args) == 0:
self.position = Vector3()
self.width = 0.0
self.height = 0.0
self.depth = 0.0
else:
errmsg = "AABB must be initialized with types:\n"
errmsg += "x: " + (str(float) + "\n" +
"y: " + str(float) + "\n" +
"z: " + str(float) + "\n" +
"width: " + str(float) + "\n" +
"height: " + str(float) + "\n" +
"depth: " + str(float) + "\n\n" +
"or\n\n" +
"position: " + (str(Vector3)) + "\n" +
"width: " + str(float) + "\n" +
"height: " + str(float) + "\n" +
"depth: " + str(float))
raise TypeError(errmsg)
def __str__(self):
return (str(self.position) + " " + str(self.width) + " " +
str(self.height) + " " +
str(self.depth))
@classmethod
@decorators.check_type(Vector3)
def from_corners(cls, pmin = Vector3(), pmax = Vector3()):
minx = min(pmin.x, pmax.x)
miny = min(pmin.y, pmax.y)
minz = min(pmin.z, pmax.z)
maxx = max(pmin.x, pmax.x)
maxy = max(pmin.y, pmax.y)
maxz = max(pmin.z, pmax.z)
width = maxx - minx
height = maxy - miny
depth = maxz - minz
return cls(minx, miny, minz, width, height, depth)
@classmethod
def from_points(cls, point_list):
if len(point_list) == 0:
return cls()
minx, miny, minz = point_list[0]
maxx, maxy, maxz = point_list[0]
for p in point_list:
minx = min(p.x, minx);
miny = min(p.y, miny);
minz = min(p.z, minz);
maxx = max(p.x, maxx);
maxy = max(p.y, maxy);
maxz = max(p.z, maxz);
eps = 1e-9;
width = maxx - minx + eps;
height = maxy - miny + eps;
depth = maxz - minz + eps;
return cls(minx, miny, minz, width, height, depth)
@classmethod
def from_struct(cls, cstruct):
return cls(Vector3.from_struct(cstruct.position),
float(cstruct.width),
float(cstruct.height),
float(cstruct.depth))
def to_struct(self):
return AABB_t(Vector3_t(self.x, self.y, self.z),
self.width, self.height, self.depth)
@classmethod
def from_grid_index(cls, grid_index = GridIndex(), dx = 0.0):
return cls(grid_index.i*dx, grid_index.j*dx, grid_index.k*dx, dx, dx, dx)
@property
def x(self):
return self._position.x
@property
def y(self):
return self._position.y
@property
def z(self):
return self._position.z
@property
def width(self):
return self._width
@property
def height(self):
return self._height
@property
def depth(self):
return self._depth
@property
def position(self):
return self._position
@x.setter
def x(self, value):
self._position.x = value
@y.setter
def y(self, value):
self._position.y = value
@z.setter
def z(self, value):
self._position.z = value
@width.setter
def width(self, value):
self._width = float(value)
@height.setter
def height(self, value):
self._height = float(value)
@depth.setter
def depth(self, value):
self._depth = float(value)
@position.setter
@decorators.check_type(Vector3)
def position(self, vector):
self._position = vector
def expand(self, v):
h = 0.5 * v;
self.position -= Vector3(h, h, h);
self.width += v;
self.height += v;
self.depth += v;
@decorators.xyz_or_vector
def contains_point(self, x, y, z):
return (x >= self.x and y >= self.y and z >= self.z and
x < self.x + self.width and
y < self.y + self.height and
z < self.z + self.depth)
def get_min_point(self):
return self.position
def get_max_point(self):
return self.position + Vector3(self.width, self.height, self.depth)
def get_intersection(self, bbox):
minp1 = self.get_min_point()
minp2 = bbox.get_min_point()
maxp1 = self.get_max_point()
maxp2 = bbox.get_max_point()
if minp1.x > maxp2.x or minp1.y > maxp2.y or minp1.z > maxp2.z:
return AABB()
interminx = max(minp1.x, minp2.x)
interminy = max(minp1.y, minp2.y)
interminz = max(minp1.z, minp2.z)
intermaxx = min(maxp1.x, maxp2.x)
intermaxy = min(maxp1.y, maxp2.y)
intermaxz = min(maxp1.z, maxp2.z)
return AABB.from_corners(Vector3(interminx, interminy, interminz),
Vector3(intermaxx, intermaxy, intermaxz))
def get_union(self, bbox):
minp1 = self.get_min_point()
minp2 = bbox.get_min_point()
maxp1 = self.get_max_point()
maxp2 = bbox.get_max_point()
if minp1.x > maxp2.x or minp1.y > maxp2.y or minp1.z > maxp2.z:
return AABB()
unionminx = min(minp1.x, minp2.x)
unionminy = min(minp1.y, minp2.y)
unionminz = min(minp1.z, minp2.z)
unionmaxx = max(maxp1.x, maxp2.x)
unionmaxy = max(maxp1.y, maxp2.y)
unionmaxz = max(maxp1.z, maxp2.z)
return AABB.from_corners(Vector3(unionminx, unionminy, unionminz),
Vector3(unionmaxx, unionmaxy, unionmaxz))
@@ -0,0 +1,112 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from abc import ABCMeta, abstractmethod
import array
from gridindex import GridIndex
import method_decorators as decorators
class Array3d:
__metaclass__ = ABCMeta
def __init__(self, isize, jsize, ksize):
self.width, self.height, self.depth = isize, jsize, ksize
self._num_elements = isize*jsize*ksize
@abstractmethod
def _init_grid(self, data):
pass
def fill(self, value):
for i in range(self._num_elements):
self._grid[i] = value
@decorators.ijk_or_gridindex
def __call__(self, i, j, k):
if not self._is_index_in_range(i, j, k) and self._out_of_range_value != None:
return self._out_of_range_value
return self._grid[self._get_flat_index(i, j, k)]
def __iter__(self):
i = j = k = 0
for v in self._grid:
yield i, j, k, v
i += 1
if i >= self.width:
i = 0
j += 1
if j >= self.height:
j = 0
k += 1
@decorators.ijk_or_gridindex
def get(self, i, j, k):
return self(i, j, k)
@decorators.ijk_or_gridindex_and_value
def set(self, i, j, k, value):
self._grid[self._get_flat_index(i, j, k)] = value
@decorators.ijk_or_gridindex_and_value
def add(self, i, j, k, value):
self._grid[self._get_flat_index(i, j, k)] += value
def get_num_elements(self):
return self._num_elements
def set_out_of_range_value(self, value = None):
self._out_of_range_value = value
def get_out_of_range_value(self):
return self._out_of_range_value
def _is_index_in_range(self, i, j, k):
return (i >= 0 and j >= 0 and k >= 0 and
i < self.width and j < self.height and k < self.depth)
def _get_flat_index(self, i, j, k):
return i + self.width*(j + self.height*k)
class Array3di(Array3d):
def __init__(self, isize, jsize, ksize, default_value = int()):
Array3d.__init__(self, isize, jsize, ksize)
self._init_grid(default_value)
def _init_grid(self, default_value):
self._grid = array.array('i', [default_value]*self.get_num_elements())
class Array3df(Array3d):
def __init__(self, isize, jsize, ksize, default_value = float()):
Array3d.__init__(self, isize, jsize, ksize)
self._init_grid(default_value)
def _init_grid(self, default_value):
self._grid = array.array('f', [default_value]*self.get_num_elements())
class Array3dd(Array3d):
def __init__(self, isize, jsize, ksize, default_value = float()):
Array3d.__init__(self, isize, jsize, ksize)
self._init_grid(default_value)
def _init_grid(self, default_value):
self._grid = array.array('d', [default_value]*self.get_num_elements())
@@ -0,0 +1,138 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import ctypes
import os
import platform
class LibraryLoadError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
# The FFEngineLib class loads the FLIP Fluids addon simulation engine. The engine
# is a dynamic library which contains methods to process simulation calculations.
# The simulation engine is written in C and C++ and is controlled through Python
# using the built-in ctypes module (https://docs.python.org/3/library/ctypes.html).
#
# The files in src/engine/ffengine contain Python bindings for the fluid simulation
# objects and methods. The Python bindings use ctypes to call corresponding C bindings
# found in src/engine/c_bindings. The C bindings call C++ methods found in src/engine.
#
# To begin following how the simulator is run from Python to C to C++, refer to the
# baking script located at src/addon/bake.py starting at the bake(...) method. The
# arguments passed to bake(...) are generated and formed in the addon within the Bake
# Operators found in src/addon/operators/bake_operators.py as well as the Export
# Operators found in src/addon/operators/export_operators.py.
class FFEngineLib():
def __init__(self):
self._lib = None
def __getattr__(self, name):
if self.__dict__['_lib'] is None:
self._lib = self._load_library("ffengine")
return getattr(self._lib, name)
def _load_library(self, name):
libname_release_prefix = "libffengine"
system = platform.system()
if system == "Windows":
library_extension = ".dll"
elif system == "Darwin":
library_extension = ".dylib"
elif system == "Linux":
library_extension = ".so"
else:
raise LibraryLoadError("Unable to recognize system: " + system)
libdir = os.path.join(os.path.dirname(__file__), "lib")
libnames= [f for f in os.listdir(libdir) if os.path.isfile(os.path.join(libdir, f))]
libnames_release = [n for n in libnames if n.startswith(libname_release_prefix) and n.endswith(library_extension)]
# Sorting the library names by length is not necessary, but sorting from
# longest name to shortest will bypass a possible user-error if the user does not
# completely remove the previous installation before installing a new version.
# A version update required a possible increase in the length of the library
# name. This sort will ensure that the longer library name (newer version)
# is used before the shorter named file (older version) that could remain
# from an incorrect install or compile process.
libnames_release.sort(key=len, reverse=True)
libpaths_release = [os.path.join(libdir, n) for n in libnames_release]
# The addon requires a functioning ffengine library version
missing_libraries = []
if not libpaths_release:
missing_libraries.append(libname_release_prefix + library_extension)
if missing_libraries:
err_msg = "Cannot find fluid engine libraries: "
for libname in missing_libraries:
err_msg += "<" + libname + "> "
raise LibraryLoadError(err_msg)
# The addon may be packaged with multiple versions of a library for the OS, not
# all of which may be compatible with the specific OS version. Choose the first
# library that loads without error.
# Refer to the LIBRARY_SUFFIX variable in the CMakeLists.txt file for generating a
# library with a suffix added to the name.
loaded_library = None
failed_libraries = []
for libpath in libpaths_release:
try:
loaded_library = ctypes.cdll.LoadLibrary(libpath)
break
except:
failed_libraries.append(libpath)
loaded_library = None
pass
# Additional notes on the error message:
# (1) Blender 2.80 and later are 64-bit and require a library that has been
# built as 64-bit. Make sure you are using a 64-bit compiler for these versions.
# Blender 2.79 distributes both 32-bit and 64-bit versions, so make sure your
# your compiler matches the target version of Blender 2.79.
# (2) This resolves possible errors due to incorrect installation of the addon and
# possible conflicts between Blender versions (such as multiple daily builds).
# Refer to this document for addon installation troubleshooting:
# https://github.com/rlguy/Blender-FLIP-Fluids/wiki/Addon-Installation-Troubleshooting
if loaded_library is None:
failed_libraries_string = ""
for libpath in failed_libraries:
failed_libraries_string += "<" + libpath + "> "
msg = "Unable to load fluid engine libraries: " + failed_libraries_string
msg += " (1) Make sure that you are using a 64-bit version of Python/Blender"
msg += " if built for 64-bit and likewise if built for 32-bit."
msg += " (2) Try clearing your Blender user settings (make a backup first!)."
msg += " (3) Contact the developers if you think that this is an error."
raise LibraryLoadError(msg)
return loaded_library
ffengine = FFEngineLib()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,231 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from abc import ABCMeta, abstractmethod
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from . import pybindings as pb
from . import method_decorators as decorators
from .trianglemesh import TriangleMesh_t
class ForceField():
__metaclass__ = ABCMeta
@abstractmethod
def __init__():
pass
def __call__(self):
return self._obj
def update_mesh_static(self, mesh):
mesh_struct = mesh.to_struct()
libfunc = lib.ForceField_update_mesh_static
args = [c_void_p, TriangleMesh_t, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
pb.execute_lib_func(libfunc, [self(), mesh_struct])
def update_mesh_animated(self, mesh_previous, mesh_current, mesh_next):
mesh_struct_previous = mesh_previous.to_struct()
mesh_struct_current = mesh_current.to_struct()
mesh_struct_next = mesh_next.to_struct()
libfunc = lib.ForceField_update_mesh_animated
args = [c_void_p, TriangleMesh_t, TriangleMesh_t, TriangleMesh_t, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
pb.execute_lib_func(libfunc, [self(), mesh_struct_previous,
mesh_struct_current,
mesh_struct_next])
@property
def enable(self):
libfunc = lib.ForceField_is_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable.setter
def enable(self, boolval):
if boolval:
libfunc = lib.ForceField_enable
else:
libfunc = lib.ForceField_disable
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def strength(self):
libfunc = lib.ForceField_get_strength
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@strength.setter
def strength(self, value):
libfunc = lib.ForceField_set_strength
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def falloff_power(self):
libfunc = lib.ForceField_get_falloff_power
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@falloff_power.setter
def falloff_power(self, value):
libfunc = lib.ForceField_set_falloff_power
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def max_force_limit_factor(self):
libfunc = lib.ForceField_get_max_force_limit_factor
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@max_force_limit_factor.setter
def max_force_limit_factor(self, value):
libfunc = lib.ForceField_set_max_force_limit_factor
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def enable_min_distance(self):
libfunc = lib.ForceField_is_min_distance_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_min_distance.setter
def enable_min_distance(self, boolval):
if boolval:
libfunc = lib.ForceField_enable_min_distance
else:
libfunc = lib.ForceField_disable_min_distance
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def min_distance(self):
libfunc = lib.ForceField_get_min_distance
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@min_distance.setter
def min_distance(self, value):
libfunc = lib.ForceField_set_min_distance
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def enable_max_distance(self):
libfunc = lib.ForceField_is_max_distance_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_max_distance.setter
def enable_max_distance(self, boolval):
if boolval:
libfunc = lib.ForceField_enable_max_distance
else:
libfunc = lib.ForceField_disable_max_distance
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def max_distance(self):
libfunc = lib.ForceField_get_max_distance
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@max_distance.setter
def max_distance(self, value):
libfunc = lib.ForceField_set_max_distance
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def enable_frontfacing(self):
libfunc = lib.ForceField_is_frontfacing_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_frontfacing.setter
def enable_frontfacing(self, boolval):
if boolval:
libfunc = lib.ForceField_enable_frontfacing
else:
libfunc = lib.ForceField_disable_frontfacing
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def enable_backfacing(self):
libfunc = lib.ForceField_is_backfacing_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_backfacing.setter
def enable_backfacing(self, boolval):
if boolval:
libfunc = lib.ForceField_enable_backfacing
else:
libfunc = lib.ForceField_disable_backfacing
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def enable_edgefacing(self):
libfunc = lib.ForceField_is_edgefacing_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_edgefacing.setter
def enable_edgefacing(self, boolval):
if boolval:
libfunc = lib.ForceField_enable_edgefacing
else:
libfunc = lib.ForceField_disable_edgefacing
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def gravity_scale(self):
libfunc = lib.ForceField_get_gravity_scale
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@gravity_scale.setter
def gravity_scale(self, value):
libfunc = lib.ForceField_set_gravity_scale
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def gravity_scale_width(self):
libfunc = lib.ForceField_get_gravity_scale
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@gravity_scale_width.setter
def gravity_scale_width(self, value):
libfunc = lib.ForceField_set_gravity_scale_width
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@@ -0,0 +1,87 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from abc import ABCMeta, abstractmethod
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from .forcefield import ForceField
from . import pybindings as pb
from . import method_decorators as decorators
class ForceFieldCurve(ForceField):
def __init__(self):
libfunc = lib.ForceFieldCurve_new
args = [c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
self._obj = pb.execute_lib_func(libfunc, [])
def __del__(self):
try:
libfunc = lib.ForceFieldCurve_destroy
pb.init_lib_func(libfunc, [c_void_p], None)
libfunc(self._obj)
except:
pass
def __call__(self):
return self._obj
@property
def flow_strength(self):
libfunc = lib.ForceFieldCurve_get_flow_strength
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@flow_strength.setter
def flow_strength(self, value):
libfunc = lib.ForceFieldCurve_set_flow_strength
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def spin_strength(self):
libfunc = lib.ForceFieldCurve_get_spin_strength
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@spin_strength.setter
def spin_strength(self, value):
libfunc = lib.ForceFieldCurve_set_spin_strength
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def enable_endcaps(self):
libfunc = lib.ForceFieldCurve_is_endcaps_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_endcaps.setter
def enable_endcaps(self, boolval):
if boolval:
libfunc = lib.ForceFieldCurve_enable_endcaps
else:
libfunc = lib.ForceFieldCurve_disable_endcaps
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@@ -0,0 +1,59 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from . import pybindings as pb
from . import method_decorators as decorators
class ForceFieldGrid():
def __init__(self, c_pointer=None):
if c_pointer is not None:
self._obj = c_pointer
self._is_owner = False
else:
libfunc = lib.ForceFieldGrid_new
args = [c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
self._obj = pb.execute_lib_func(libfunc, [])
self._is_owner = True
def __del__(self):
if not self._is_owner:
return
try:
libfunc = lib.MeshObject_destroy
pb.init_lib_func(libfunc, [c_void_p], None)
libfunc(self._obj)
except:
pass
def __call__(self):
return self._obj
def add_force_field(self, field):
libfunc = lib.ForceFieldGrid_add_force_field
args = [c_void_p, c_void_p, c_void_p]
pb.init_lib_func(libfunc, args, None)
pb.execute_lib_func(libfunc, [self(), field()])
@@ -0,0 +1,45 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from abc import ABCMeta, abstractmethod
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from .forcefield import ForceField
from . import pybindings as pb
from . import method_decorators as decorators
class ForceFieldPoint(ForceField):
def __init__(self):
libfunc = lib.ForceFieldPoint_new
args = [c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
self._obj = pb.execute_lib_func(libfunc, [])
def __del__(self):
try:
libfunc = lib.ForceFieldPoint_destroy
pb.init_lib_func(libfunc, [c_void_p], None)
libfunc(self._obj)
except:
pass
@@ -0,0 +1,45 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from abc import ABCMeta, abstractmethod
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from .forcefield import ForceField
from . import pybindings as pb
from . import method_decorators as decorators
class ForceFieldSurface(ForceField):
def __init__(self):
libfunc = lib.ForceFieldSurface_new
args = [c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
self._obj = pb.execute_lib_func(libfunc, [])
def __del__(self):
try:
libfunc = lib.ForceFieldSurface_destroy
pb.init_lib_func(libfunc, [c_void_p], None)
libfunc(self._obj)
except:
pass
@@ -0,0 +1,45 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from abc import ABCMeta, abstractmethod
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from .forcefield import ForceField
from . import pybindings as pb
from . import method_decorators as decorators
class ForceFieldVolume(ForceField):
def __init__(self):
libfunc = lib.ForceFieldVolume_new
args = [c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
self._obj = pb.execute_lib_func(libfunc, [])
def __del__(self):
try:
libfunc = lib.ForceFieldVolume_destroy
pb.init_lib_func(libfunc, [c_void_p], None)
libfunc(self._obj)
except:
pass
@@ -0,0 +1,85 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import array
import ctypes
class GridIndex_t(ctypes.Structure):
_fields_ = [("i", ctypes.c_int),
("j", ctypes.c_int),
("k", ctypes.c_int)]
class GridIndex(object):
def __init__(self, i = 0, j = 0, k = 0):
if isinstance(i, GridIndex):
self._values = array.array('i', [i.i, i.j, i.k])
else:
self._values = array.array('i', [i, j, k])
def __str__(self):
return str(self.i) + " " + str(self.j) + " " + str(self.k)
def __getitem__(self, key):
if key < 0 or key > 2:
raise IndexError("Index must be in range [0, 2]")
if not isinstance(key, int):
raise TypeError("Index must be an integer")
return self._values[key]
def __setitem__(self, key, value):
if key < 0 or key > 2:
raise IndexError("Index must be in range [0, 2]")
if not isinstance(key, int):
raise TypeError("Index must be an integer")
self._values[key] = value
def __iter__(self):
yield self._values[0]
yield self._values[1]
yield self._values[2]
@property
def i(self):
return self._values[0]
@property
def j(self):
return self._values[1]
@property
def k(self):
return self._values[2]
@i.setter
def i(self, value):
self._values[0] = value
@j.setter
def j(self, value):
self._values[1] = value
@k.setter
def k(self, value):
self._values[2] = value
@@ -0,0 +1,298 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from . import pybindings as pb
from . import method_decorators as decorators
from .trianglemesh import TriangleMesh_t
class MeshFluidSource():
def __init__(self, i, j, k, dx):
libfunc = lib.MeshFluidSource_new
args = [c_int, c_int, c_int, c_double, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
self._obj = pb.execute_lib_func(libfunc, [i, j, k, dx])
def __del__(self):
try:
libfunc = lib.MeshFluidSource_destroy
pb.init_lib_func(libfunc, [c_void_p], None)
libfunc(self._obj)
except:
pass
def __call__(self):
return self._obj
def update_mesh_static(self, mesh):
mesh_struct = mesh.to_struct()
libfunc = lib.MeshFluidSource_update_mesh_static
args = [c_void_p, TriangleMesh_t, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
pb.execute_lib_func(libfunc, [self(), mesh_struct])
def update_mesh_animated(self, mesh_previous, mesh_current, mesh_next):
mesh_struct_previous = mesh_previous.to_struct()
mesh_struct_current = mesh_current.to_struct()
mesh_struct_next = mesh_next.to_struct()
libfunc = lib.MeshFluidSource_update_mesh_animated
args = [c_void_p, TriangleMesh_t, TriangleMesh_t, TriangleMesh_t, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
pb.execute_lib_func(libfunc, [self(), mesh_struct_previous,
mesh_struct_current,
mesh_struct_next])
@property
def enable(self):
libfunc = lib.MeshFluidSource_is_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable.setter
def enable(self, boolval):
if boolval:
libfunc = lib.MeshFluidSource_enable
else:
libfunc = lib.MeshFluidSource_disable
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def substep_emissions(self):
libfunc = lib.MeshFluidSource_get_substep_emissions
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@substep_emissions.setter
def substep_emissions(self, n):
libfunc = lib.MeshFluidSource_set_substep_emissions
pb.init_lib_func(libfunc, [c_void_p, c_int, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), int(n)])
@property
def inflow(self):
libfunc = lib.MeshFluidSource_is_inflow
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@inflow.setter
def inflow(self, boolval):
if boolval:
libfunc = lib.MeshFluidSource_set_inflow
else:
libfunc = lib.MeshFluidSource_set_outflow
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def outflow(self):
libfunc = lib.MeshFluidSource_is_inflow
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@outflow.setter
def outflow(self, boolval):
if boolval:
libfunc = lib.MeshFluidSource_set_outflow
else:
libfunc = lib.MeshFluidSource_set_inflow
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def fluid_outflow(self):
libfunc = lib.MeshFluidSource_is_fluid_outflow_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@fluid_outflow.setter
def fluid_outflow(self, boolval):
if boolval:
libfunc = lib.MeshFluidSource_enable_fluid_outflow
else:
libfunc = lib.MeshFluidSource_disable_fluid_outflow
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def diffuse_outflow(self):
libfunc = lib.MeshFluidSource_is_diffuse_outflow_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@diffuse_outflow.setter
def diffuse_outflow(self, boolval):
if boolval:
libfunc = lib.MeshFluidSource_enable_diffuse_outflow
else:
libfunc = lib.MeshFluidSource_disable_diffuse_outflow
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
def get_velocity(self):
libfunc = lib.MeshFluidSource_get_velocity
pb.init_lib_func(libfunc, [c_void_p, c_void_p], Vector3_t)
cvect = pb.execute_lib_func(libfunc, [self()])
return Vector3.from_struct(cvect)
@decorators.xyz_or_vector
def set_velocity(self, vx, vy, vz):
libfunc = lib.MeshFluidSource_set_velocity
pb.init_lib_func(
libfunc,
[c_void_p, c_double, c_double, c_double, c_void_p], None
)
pb.execute_lib_func(libfunc, [self(), vx, vy, vz])
@property
def enable_append_object_velocity(self):
libfunc = lib.MeshFluidSource_is_append_object_velocity_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_append_object_velocity.setter
def enable_append_object_velocity(self, boolval):
if boolval:
libfunc = lib.MeshFluidSource_enable_append_object_velocity
else:
libfunc = lib.MeshFluidSource_disable_append_object_velocity
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def object_velocity_influence(self):
libfunc = lib.MeshFluidSource_get_object_velocity_influence
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@object_velocity_influence.setter
def object_velocity_influence(self, value):
libfunc = lib.MeshFluidSource_set_object_velocity_influence
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def priority(self):
libfunc = lib.MeshFluidSource_get_priority
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@priority.setter
def priority(self, n):
libfunc = lib.MeshFluidSource_set_priority
pb.init_lib_func(libfunc, [c_void_p, c_int, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), int(n)])
@property
def enable_constrained_fluid_velocity(self):
libfunc = lib.MeshFluidSource_is_constrained_fluid_velocity_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_constrained_fluid_velocity.setter
def enable_constrained_fluid_velocity(self, boolval):
if boolval:
libfunc = lib.MeshFluidSource_enable_constrained_fluid_velocity
else:
libfunc = lib.MeshFluidSource_disable_constrained_fluid_velocity
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def outflow_inverse(self):
libfunc = lib.MeshFluidSource_is_outflow_inversed
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@outflow_inverse.setter
def outflow_inverse(self, boolval):
do_inverse = (boolval and not self.outflow_inverse) or (not boolval and self.outflow_inverse)
if do_inverse:
libfunc = lib.MeshFluidSource_outflow_inverse
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def source_id(self):
libfunc = lib.MeshFluidSource_get_source_id
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@source_id.setter
def source_id(self, n):
libfunc = lib.MeshFluidSource_set_source_id
pb.init_lib_func(libfunc, [c_void_p, c_int, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), int(n)])
@property
def viscosity(self):
libfunc = lib.MeshFluidSource_get_viscosity
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@viscosity.setter
def viscosity(self, v):
libfunc = lib.MeshFluidSource_set_viscosity
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), float(v)])
@property
def lifetime(self):
libfunc = lib.MeshFluidSource_get_lifetime
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@lifetime.setter
def lifetime(self, v):
libfunc = lib.MeshFluidSource_set_lifetime
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), float(v)])
@property
def lifetime_variance(self):
libfunc = lib.MeshFluidSource_get_lifetime_variance
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@lifetime_variance.setter
def lifetime_variance(self, v):
libfunc = lib.MeshFluidSource_set_lifetime_variance
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), float(v)])
def get_source_color(self):
libfunc = lib.MeshFluidSource_get_source_color
pb.init_lib_func(libfunc, [c_void_p, c_void_p], Vector3_t)
cvect = pb.execute_lib_func(libfunc, [self()])
return Vector3.from_struct(cvect)
@decorators.xyz_or_vector
def set_source_color(self, r, g, b):
libfunc = lib.MeshFluidSource_set_source_color
pb.init_lib_func(
libfunc,
[c_void_p, c_double, c_double, c_double, c_void_p], None
)
pb.execute_lib_func(libfunc, [self(), r, g, b])
@@ -0,0 +1,273 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from ctypes import c_void_p, c_char_p, c_int, c_float, c_double, byref
from .ffengine import ffengine as lib
from . import pybindings as pb
from . import method_decorators as decorators
from .trianglemesh import TriangleMesh_t
class MeshObject():
def __init__(self, i, j, k, dx):
libfunc = lib.MeshObject_new
args = [c_int, c_int, c_int, c_double, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
self._obj = pb.execute_lib_func(libfunc, [i, j, k, dx])
def __del__(self):
try:
libfunc = lib.MeshObject_destroy
pb.init_lib_func(libfunc, [c_void_p], None)
libfunc(self._obj)
except:
pass
def __call__(self):
return self._obj
def update_mesh_static(self, mesh):
mesh_struct = mesh.to_struct()
libfunc = lib.MeshObject_update_mesh_static
args = [c_void_p, TriangleMesh_t, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
pb.execute_lib_func(libfunc, [self(), mesh_struct])
def update_mesh_animated(self, mesh_previous, mesh_current, mesh_next):
mesh_struct_previous = mesh_previous.to_struct()
mesh_struct_current = mesh_current.to_struct()
mesh_struct_next = mesh_next.to_struct()
libfunc = lib.MeshObject_update_mesh_animated
args = [c_void_p, TriangleMesh_t, TriangleMesh_t, TriangleMesh_t, c_void_p]
pb.init_lib_func(libfunc, args, c_void_p)
pb.execute_lib_func(libfunc, [self(), mesh_struct_previous,
mesh_struct_current,
mesh_struct_next])
@property
def enable(self):
libfunc = lib.MeshObject_is_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable.setter
def enable(self, boolval):
if boolval:
libfunc = lib.MeshObject_enable
else:
libfunc = lib.MeshObject_disable
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def inverse(self):
libfunc = lib.MeshObject_is_inversed
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@inverse.setter
def inverse(self, boolval):
do_inverse = (boolval and not self.inverse) or (not boolval and self.inverse)
if do_inverse:
libfunc = lib.MeshObject_inverse
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def friction(self):
libfunc = lib.MeshObject_get_friction
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@friction.setter
@decorators.check_ge_zero
@decorators.check_le(1.0)
def friction(self, value):
libfunc = lib.MeshObject_set_friction
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def velocity_scale(self):
libfunc = lib.MeshObject_get_velocity_scale
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@velocity_scale.setter
def velocity_scale(self, value):
libfunc = lib.MeshObject_set_velocity_scale
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def whitewater_influence(self):
libfunc = lib.MeshObject_get_whitewater_influence
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@whitewater_influence.setter
@decorators.check_ge_zero
def whitewater_influence(self, value):
libfunc = lib.MeshObject_set_whitewater_influence
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def dust_emission_strength(self):
libfunc = lib.MeshObject_get_dust_emission_strength
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@dust_emission_strength.setter
@decorators.check_ge_zero
def dust_emission_strength(self, value):
libfunc = lib.MeshObject_set_dust_emission_strength
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def sheeting_strength(self):
libfunc = lib.MeshObject_get_sheeting_strength
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@sheeting_strength.setter
@decorators.check_ge_zero
def sheeting_strength(self, value):
libfunc = lib.MeshObject_set_sheeting_strength
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def mesh_expansion(self):
libfunc = lib.MeshObject_get_mesh_expansion
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@mesh_expansion.setter
def mesh_expansion(self, value):
libfunc = lib.MeshObject_set_mesh_expansion
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def enable_append_object_velocity(self):
libfunc = lib.MeshObject_is_append_object_velocity_enabled
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, [self()]))
@enable_append_object_velocity.setter
def enable_append_object_velocity(self, boolval):
if boolval:
libfunc = lib.MeshObject_enable_append_object_velocity
else:
libfunc = lib.MeshObject_disable_append_object_velocity
pb.init_lib_func(libfunc, [c_void_p, c_void_p], None)
pb.execute_lib_func(libfunc, [self()])
@property
def object_velocity_influence(self):
libfunc = lib.MeshObject_get_object_velocity_influence
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_float)
return pb.execute_lib_func(libfunc, [self()])
@object_velocity_influence.setter
def object_velocity_influence(self, value):
libfunc = lib.MeshObject_set_object_velocity_influence
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), value])
@property
def priority(self):
libfunc = lib.MeshObject_get_priority
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@priority.setter
def priority(self, n):
libfunc = lib.MeshObject_set_priority
pb.init_lib_func(libfunc, [c_void_p, c_int, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), int(n)])
@property
def source_id(self):
libfunc = lib.MeshObject_get_source_id
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@source_id.setter
def source_id(self, n):
libfunc = lib.MeshObject_set_source_id
pb.init_lib_func(libfunc, [c_void_p, c_int, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), int(n)])
@property
def viscosity(self):
libfunc = lib.MeshObject_get_viscosity
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@viscosity.setter
def viscosity(self, v):
libfunc = lib.MeshObject_set_viscosity
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), float(v)])
@property
def lifetime(self):
libfunc = lib.MeshObject_get_lifetime
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@lifetime.setter
def lifetime(self, v):
libfunc = lib.MeshObject_set_lifetime
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), float(v)])
@property
def lifetime_variance(self):
libfunc = lib.MeshObject_get_lifetime_variance
pb.init_lib_func(libfunc, [c_void_p, c_void_p], c_int)
return pb.execute_lib_func(libfunc, [self()])
@lifetime_variance.setter
def lifetime_variance(self, v):
libfunc = lib.MeshObject_set_lifetime_variance
pb.init_lib_func(libfunc, [c_void_p, c_float, c_void_p], None)
pb.execute_lib_func(libfunc, [self(), float(v)])
def get_source_color(self):
libfunc = lib.MeshObject_get_source_color
pb.init_lib_func(libfunc, [c_void_p, c_void_p], Vector3_t)
cvect = pb.execute_lib_func(libfunc, [self()])
return Vector3.from_struct(cvect)
@decorators.xyz_or_vector
def set_source_color(self, r, g, b):
libfunc = lib.MeshObject_set_source_color
pb.init_lib_func(
libfunc,
[c_void_p, c_double, c_double, c_double, c_void_p], None
)
pb.execute_lib_func(libfunc, [self(), r, g, b])
@@ -0,0 +1,127 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import numbers
from .vector3 import Vector3
from .gridindex import GridIndex
def ijk_or_gridindex(func):
def ijk_or_gridindex_wrapper(self, *args):
try:
i, j, k = args
except:
i, j, k = args[0]
return func(self, i, j, k)
return ijk_or_gridindex_wrapper
def ijk_or_gridindex_and_value(func):
def ijk_or_gridindex_and_value_wrapper(self, *args):
try:
return func(self, *args)
except:
i, j, k = args[0]
return func(self, i, j, k, args[1])
return ijk_or_gridindex_and_value_wrapper
def xyz_or_vector(func):
def xyz_or_vector_wrapper(self, *args):
try:
return func(self, *args)
except:
return func(self, *args[0])
return xyz_or_vector_wrapper
def xyz_or_vector_and_radius(func):
def xyz_or_vector_wrapper(self, *args):
try:
return func(self, *args)
except:
x, y, z = args[0]
return func(self, x, y, z, args[1])
return xyz_or_vector_wrapper
def check_gt_zero(func):
def check_values(self, *args):
for arg in args:
if isinstance(arg, numbers.Real) and arg <= 0:
raise ValueError("Value must be greater than zero")
return func(self, *args)
return check_values
def check_ge_zero(func):
def check_values(self, *args):
for arg in args:
if isinstance(arg, numbers.Real) and arg < 0:
raise ValueError("Value must be greater than or equal to zero")
return func(self, *args)
return check_values
def check_gt(value):
def check_gt_decorator(func):
def check_gt_wrapper(self, *args):
for arg in args:
if isinstance(arg, numbers.Real) and arg <= value:
raise ValueError("Value must be greater than " + str(value))
return func(self, *args)
return check_gt_wrapper
return check_gt_decorator
def check_ge(value):
def check_ge_decorator(func):
def check_ge_wrapper(self, *args):
for arg in args:
if isinstance(arg, numbers.Real) and arg < value:
raise ValueError("Value must be greater than or equal to " + str(value))
return func(self, *args)
return check_ge_wrapper
return check_ge_decorator
def check_lt(value):
def check_lt_decorator(func):
def check_lt_wrapper(self, *args):
for arg in args:
if isinstance(arg, numbers.Real) and arg >= value:
raise ValueError("Value must be less than " + str(value))
return func(self, *args)
return check_lt_wrapper
return check_lt_decorator
def check_le(value):
def check_le_decorator(func):
def check_le_wrapper(self, *args):
for arg in args:
if isinstance(arg, numbers.Real) and arg > value:
raise ValueError("Value must be less than or equal to " + str(value))
return func(self, *args)
return check_le_wrapper
return check_le_decorator
def check_type(argtype):
def check_type_decorator(func):
def check_type_wrapper(self, *args):
for arg in args:
if not isinstance(arg, argtype):
raise TypeError("Argument must be of type " + str(argtype))
return func(self, *args)
return check_type_wrapper
return check_type_decorator
@@ -0,0 +1,58 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import ctypes
from ctypes import c_void_p, c_char_p, c_char, c_int, c_uint, c_float, c_double, byref
from .ffengine import ffengine as lib
from .vector3 import Vector3, Vector3_t
from . import pybindings as pb
class MixboxLutData_t(ctypes.Structure):
_fields_ = [("size", c_int),
("data", c_char_p)]
def initialize(lut_data, lut_data_size):
c_lut_data = (c_char * len(lut_data)).from_buffer_copy(lut_data)
mb_data = MixboxLutData_t()
mb_data.size = lut_data_size
mb_data.data = ctypes.cast(c_lut_data, c_char_p)
libfunc = lib.Mixbox_initialize
pb.init_lib_func(libfunc, [MixboxLutData_t, c_void_p], None)
pb.execute_lib_func(libfunc, [mb_data])
def is_initialized():
libfunc = lib.Mixbox_is_initialized
pb.init_lib_func(libfunc, [c_void_p], c_int)
return bool(pb.execute_lib_func(libfunc, []))
def lerp_srgb32f(r1, g1, b1, r2, g2, b2, t):
libfunc = lib.Mixbox_lerp_srgb32f
pb.init_lib_func(libfunc, [c_float, c_float, c_float, c_float, c_float, c_float, c_float, c_void_p], Vector3_t)
cvect = pb.execute_lib_func(libfunc, [r1, g1, b1, r2, g2, b2, t])
return cvect.x, cvect.y, cvect.z
@@ -0,0 +1,60 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .ffengine import ffengine as lib
from ctypes import c_char_p, c_int, byref
def check_success(success, errprefix):
libfunc = lib.CBindings_get_error_message
init_lib_func(libfunc, [], c_char_p)
if not success:
raise RuntimeError(errprefix + str(libfunc().decode("utf-8")))
def init_lib_func(libfunc, argtypes, restype):
if libfunc.argtypes is None:
libfunc.argtypes = argtypes
libfunc.restype = restype
def execute_lib_func(libfunc, params):
args = []
for idx, arg in enumerate(params):
try:
cval = libfunc.argtypes[idx](arg)
except:
cval = arg
args.append(cval)
success = c_int();
args.append(byref(success))
result = None
if libfunc.restype:
funcresult = libfunc(*args)
check_success(success, libfunc.__name__ + " - ")
try:
return libfunc.restype(funcresult).value
except:
return funcresult
else:
libfunc(*args)
check_success(success, libfunc.__name__ + " - ")
return result
@@ -0,0 +1,113 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import ctypes
import array
import struct
class TriangleMesh_t(ctypes.Structure):
_fields_ = [("vertices", ctypes.c_void_p),
("triangles", ctypes.c_void_p),
("num_vertices", ctypes.c_int),
("num_triangles", ctypes.c_int)]
class TriangleMesh(object):
def __init__(self):
self.vertices = array.array('f', [])
self.triangles = array.array('i', [])
@classmethod
def from_bobj(cls, bobj_data):
int_data, bobj_data = bobj_data[:4], bobj_data[4:]
num_vertices = struct.unpack('i', int_data)[0]
num_floats = 3 * num_vertices
num_bytes = 4 * num_floats
vertex_data, bobj_data = bobj_data[:num_bytes], bobj_data[num_bytes:]
vertices = list(struct.unpack('{0}f'.format(num_floats), vertex_data))
int_data, bobj_data = bobj_data[:4], bobj_data[4:]
num_triangles = struct.unpack('i', int_data)[0]
num_ints = 3 * num_triangles
num_bytes = 4 * num_ints
triangle_data, bobj_data = bobj_data[:num_bytes], bobj_data[num_bytes:]
triangles = list(struct.unpack('{0}i'.format(num_ints), triangle_data))
self = cls()
self.vertices = array.array('f', vertices)
self.triangles = array.array('i', triangles)
return self
def to_bobj(self):
num_vertices = len(self.vertices) // 3
num_triangles = len(self.triangles) // 3
datastr = struct.pack('i', num_vertices)
datastr += self.vertices.tobytes()
datastr += struct.pack('i', num_triangles)
datastr += self.triangles.tobytes()
return datastr
def to_struct(self):
num_vertices = len(self.vertices) // 3
num_triangles = len(self.triangles) // 3
vertex_data = (ctypes.c_float * len(self.vertices))()
for i in range(len(self.vertices)):
vertex_data[i] = self.vertices[i]
triangle_data = (ctypes.c_int * len(self.triangles))()
for i in range(len(self.triangles)):
triangle_data[i] = self.triangles[i]
struct = TriangleMesh_t()
struct.vertices = ctypes.cast(vertex_data, ctypes.c_void_p)
struct.triangles = ctypes.cast(triangle_data, ctypes.c_void_p)
struct.num_vertices = num_vertices
struct.num_triangles = num_triangles
return struct
def apply_transform(self, matrix_world):
m = matrix_world
for i in range(0, len(self.vertices), 3):
v = [self.vertices[i + 0], self.vertices[i + 1], self.vertices[i + 2], 1]
self.vertices[i + 0] = v[0]*m[0] + v[1]*m[1] + v[2]*m[2] + v[3]*m[3]
self.vertices[i + 1] = v[0]*m[4] + v[1]*m[5] + v[2]*m[6] + v[3]*m[7]
self.vertices[i + 2] = v[0]*m[8] + v[1]*m[9] + v[2]*m[10] + v[3]*m[11]
def translate(self, tx, ty, tz):
for i in range(0, len(self.vertices), 3):
self.vertices[i + 0] += tx
self.vertices[i + 1] += ty
self.vertices[i + 2] += tz
def scale(self, scale):
for i in range(0, len(self.vertices), 3):
self.vertices[i + 0] *= scale
self.vertices[i + 1] *= scale
self.vertices[i + 2] *= scale
@@ -0,0 +1,217 @@
# MIT License
#
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import array
import ctypes
import math
class Vector3_t(ctypes.Structure):
_fields_ = [("x", ctypes.c_float),
("y", ctypes.c_float),
("z", ctypes.c_float)]
class Vector3(object):
def __init__(self, x = 0.0, y = 0.0, z = 0.0):
if isinstance(x, Vector3):
self._values = array.array('f', [x.x, x.y, x.z])
else:
self._values = array.array('f', [x, y, z])
@classmethod
def from_struct(cls, cstruct):
return cls(cstruct.x, cstruct.y, cstruct.z)
def to_struct(self):
return Vector3_t(self.x, self.y, self.z)
def __str__(self):
return str(self.x) + " " + str(self.y) + " " + str(self.z)
def __getitem__(self, key):
if key < 0 or key > 2:
raise IndexError("Index must be in range [0, 2]")
if not isinstance(key, int):
raise TypeError("Index must be an integer")
return self._values[key]
def __setitem__(self, key, value):
if key < 0 or key > 2:
raise IndexError("Index must be in range [0, 2]")
if not isinstance(key, int):
raise TypeError("Index must be an integer")
self._values[key] = value
def __iter__(self):
yield self._values[0]
yield self._values[1]
yield self._values[2]
def __add__(self, other):
return Vector3(self.x + other.x,
self.y + other.y,
self.z + other.z)
def __iadd__(self, other):
self.x += other.x
self.y += other.y
self.z += other.z
return self
def __sub__(self, other):
return Vector3(self.x - other.x,
self.y - other.y,
self.z - other.z)
def __isub__(self, other):
self.x -= other.x
self.y -= other.y
self.z -= other.z
return self
def __mul__(self, scale):
return Vector3(scale * self.x,
scale * self.y,
scale * self.z)
def __imul__(self, scale):
self.x *= scale
self.y *= scale
self.z *= scale
return self
def __rmul__(self, scale):
return Vector3(scale * self.x,
scale * self.y,
scale * self.z)
def __div__(self, denominator):
if denominator == 0.0:
raise ZeroDivisionError
inv = 1.0 / denominator
return Vector3(self.x * inv,
self.y * inv,
self.z * inv)
def __idiv__(self, denominator):
if denominator == 0.0:
raise ZeroDivisionError
inv = 1.0 / denominator
self.x *= inv
self.y *= inv
self.z *= inv
return self
def __neg__(self):
return Vector3(-self.x, -self.y, -self.z)
def __pos__(self):
return Vector3(self)
def __abs__(self):
return Vector3(abs(self.x), abs(self.y), abs(self.z))
def __invert__(self):
return Vector3(1.0 / self.x,
1.0 / self.y,
1.0 / self.z)
@property
def x(self):
return self._values[0]
@property
def y(self):
return self._values[1]
@property
def z(self):
return self._values[2]
@x.setter
def x(self, value):
self._values[0] = value
@y.setter
def y(self, value):
self._values[1] = value
@z.setter
def z(self, value):
self._values[2] = value
def add(self, vector):
self += vector
return self
def sub(self, vector):
self -= vector
return self
def mult(self, scale):
self *= scale
return self
def div(self, denominator):
self /= denominator
return self
def neg(self):
self.x = -self.x
self.y = -self.y
self.z = -self.z
return self
def invert(self):
self.x = 1.0 / self.x
self.y = 1.0 / self.y
self.z = 1.0 / self.z
return self
def dot(vector):
return self.x*vector.x + self.y*vector.y + self.z*vector.z
def cross(vector):
return Vector3(self.y*vector.z - self.z*vector.y,
self.z*vector.x - self.x*vector.z,
self.x*vector.y - self.y*vector.x)
def lengthsq(self):
return self.x*self.x + self.y*self.y + self.z*self.z
def length(self):
return math.sqrt(self.x*self.x + self.y*self.y + self.z*self.z)
def normalize(self):
length = self.length()
if length == 0.0:
raise ZeroDivisionError
inv = 1.0 / length
self *= inv
return self
@@ -0,0 +1,30 @@
# 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 = [
'filesystem_protection_layer',
]
for module_name in reloadable_modules:
if module_name in locals():
importlib.reload(locals()[module_name])
import bpy
from . import (
filesystem_protection_layer,
)
@@ -0,0 +1,261 @@
# 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, pathlib
from ..utils import installation_utils
__EXTENSION_WHITELIST = [
".backup",
".bat",
".sh",
".bbox",
".bin",
".blend",
".bobj",
".cpp",
".data",
".ffd",
".ffp3",
".fpd",
".h",
".info",
".json",
".md",
".png",
".preset",
".sim",
".sqlite3",
".state",
".txt",
".txt~",
".wwi",
".wwf",
".wwp"
]
# These extensions are not allowed to be mass-deleted within a directory
__DELETE_DIRECTORY_EXTENSION_BLACKLIST = [
".blend"
]
class FilesystemProtectionError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
def get_extension_whitelist():
global __EXTENSION_WHITELIST
return __EXTENSION_WHITELIST
def get_delete_directory_extension_blacklist():
global __DELETE_DIRECTORY_EXTENSION_BLACKLIST
return __DELETE_DIRECTORY_EXTENSION_BLACKLIST
def get_directory_whitelist():
whitelist = []
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is not None:
whitelist.append(dprops.cache.get_cache_abspath())
this_filepath = os.path.realpath(__file__)
addon_filepath = os.path.dirname(os.path.dirname(this_filepath))
whitelist.append(addon_filepath)
preset_library_installations = installation_utils.get_preset_library_installations()
for install in preset_library_installations:
whitelist.append(install["path"])
if bpy.data.filepath:
compositing_tools_filepath = os.path.dirname(bpy.data.filepath)
whitelist.append(compositing_tools_filepath)
return whitelist
def path_is_parent(parent_path, child_path):
parent_path = os.path.abspath(parent_path)
child_path = os.path.abspath(child_path)
try:
parent_child_commonpath = os.path.commonpath([parent_path, child_path])
except ValueError:
# paths not on same drive
return False
return os.path.commonpath([parent_path]) == parent_child_commonpath
def check_extensions_valid(extensions):
extension_whitelist = get_extension_whitelist()
bad_extensions = []
for ext in extensions:
if ext not in extension_whitelist:
bad_extensions.append(ext)
if bad_extensions:
error_msg = "Extension not in whitelist: "
for ext in bad_extensions:
error_msg += "<" + ext + "> "
error_msg += "***Please contact the developers with this error message***"
raise FilesystemProtectionError(error_msg)
def check_directory_valid(base_directory):
directory_whitelist = get_directory_whitelist()
is_safe_sub_directory = False
for d in directory_whitelist:
if path_is_parent(d, base_directory):
is_safe_sub_directory = True
break
if not is_safe_sub_directory:
error_msg = "Directory is not in whitelist: <" + base_directory + "> Whitelist: "
for d in directory_whitelist:
error_msg += "<" + d + "> "
error_msg += "***Please contact the developers with this error message***"
raise FilesystemProtectionError(error_msg)
def delete_files_in_directory(base_directory, extensions, remove_directory=False, display_popup_on_error=True):
extension_blacklist = get_delete_directory_extension_blacklist()
for ext in extension_blacklist:
if ext in extensions:
error_msg = "Extension in directory deletion blacklist: "
error_msg += "<" + ext + "> "
error_msg += "***Please contact the developers with this error message***"
raise FilesystemProtectionError(error_msg)
check_extensions_valid(extensions)
check_directory_valid(base_directory)
if not os.path.isdir(base_directory):
return
file_list = [f for f in os.listdir(base_directory) if os.path.isfile(os.path.join(base_directory, f))]
valid_filepaths = []
for f in file_list:
if pathlib.Path(f).suffix in extensions:
valid_filepaths.append(os.path.join(base_directory, f))
remove_error_count = 0
first_error = None
for f in valid_filepaths:
try:
os.remove(f)
except OSError as e:
remove_error_count += 1
if first_error is None:
first_error = str(e)
if remove_directory and not os.listdir(base_directory):
try:
os.rmdir(base_directory)
except OSError as e:
remove_error_count += 1
if first_error is None:
first_error = str(e)
if remove_error_count > 0:
errmsg = "Error encountered attempting to remove " + str(remove_error_count) + " file(s) in <" + base_directory + ">. Reason: <" + first_error + ">. "
errmsg += "Try closing all applications accessing the directory, restarting Blender/System, or deleting directory manually."
if display_popup_on_error:
# Method may be run from bake.py script where there is not window from this context
# in this case, 'display_popup_on_error' should be set so this operator does not run.
bpy.ops.flip_fluid_operators.display_error(
'INVOKE_DEFAULT',
error_message="Error Removing Files",
error_description=errmsg,
popup_width=700
)
else:
print(errmsg)
raise PermissionError(errmsg)
def delete_file(filepath, error_ok=False, display_popup_on_error=True):
if not os.path.isfile(filepath):
return
extension = pathlib.Path(filepath).suffix
check_extensions_valid([extension])
check_directory_valid(filepath)
try:
os.remove(filepath)
except Exception as e:
if error_ok:
pass
else:
errmsg = "Error encountered attempting to remove file. Reason: <" + str(e) + ">."
if display_popup_on_error:
# Method may be run from bake.py script where there is not window from this context
# in this case, 'display_popup_on_error' should be set so this operator does not run.
bpy.ops.flip_fluid_operators.display_error(
'INVOKE_DEFAULT',
error_message="Error Removing Files",
error_description=errmsg,
popup_width=700
)
else:
print(errmsg)
raise PermissionError(errmsg)
def clear_cache_directory(cache_directory, clear_export=False, clear_logs=False, remove_directory=False):
stats_filepath = os.path.join(cache_directory, "flipstats.data")
delete_file(stats_filepath)
bakefiles_dir = os.path.join(cache_directory, "bakefiles")
extensions = [".bbox", ".bobj", ".data", ".wwp", ".wwf", ".wwi", ".fpd", ".ffd", ".ffp3", ".txt"]
delete_files_in_directory(bakefiles_dir, extensions, remove_directory=True)
temp_dir = os.path.join(cache_directory, "temp")
extensions = [".data"]
delete_files_in_directory(temp_dir, extensions, remove_directory=True)
scripts_dir = os.path.join(cache_directory, "scripts")
extensions = [".bat", ".sh", ".txt"]
delete_files_in_directory(scripts_dir, extensions, remove_directory=True)
savestates_dir = os.path.join(cache_directory, "savestates")
if os.path.isdir(savestates_dir):
extensions = [".data", ".state", ".backup"]
savestate_subdirs = [d for d in os.listdir(savestates_dir) if os.path.isdir(os.path.join(savestates_dir, d))]
for subd in savestate_subdirs:
if subd.startswith("autosave"):
dirpath = os.path.join(savestates_dir, subd)
delete_files_in_directory(dirpath, extensions, remove_directory=True)
delete_files_in_directory(savestates_dir, [], remove_directory=True)
if clear_export:
export_dir = os.path.join(cache_directory, "export")
if os.path.isdir(export_dir):
extensions = [".sqlite3", ".sim"]
delete_files_in_directory(export_dir, extensions, remove_directory=True)
if clear_logs:
logs_dir = os.path.join(cache_directory, "logs")
if os.path.isdir(logs_dir):
extensions = [".txt"]
delete_files_in_directory(logs_dir, extensions, remove_directory=True)
if remove_directory and not os.listdir(cache_directory):
os.rmdir(cache_directory)
@@ -0,0 +1,28 @@
The FLIP Fluids Material Library Blend files and containing materials in this directory
and subdirectories are licensed under the MIT License:
--------------------------------------------------------------------------------
MIT License
Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
@@ -0,0 +1,46 @@
# 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 = [
'material_library'
]
for module_name in reloadable_modules:
if module_name in locals():
importlib.reload(locals()[module_name])
import bpy
from . import (
material_library
)
def load_post():
material_library.load_post()
def scene_update_post(scene):
material_library.scene_update_post(scene)
def register():
material_library.register()
def unregister():
material_library.unregister()
@@ -0,0 +1,140 @@
# 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, hashlib
from bpy.props import (
PointerProperty
)
from ..objects import flip_fluid_material_library
from ..utils import version_compatibility_utils as vcu
def get_surface_material_enums_ui(scene=None, context=None):
bpy.context.scene.flip_fluid_material_library.check_icons_initialized()
enums = []
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_SURFACE')
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_ALL')
enums += __get_non_material_library_enums_by_type()
enums += [__get_none_material_enum()]
enums.reverse()
return enums
def get_fluid_particles_material_enums_ui(scene=None, context=None):
bpy.context.scene.flip_fluid_material_library.check_icons_initialized()
enums = []
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_SURFACE')
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_WHITEWATER')
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_ALL')
enums += __get_non_material_library_enums_by_type()
enums += [__get_none_material_enum()]
enums.reverse()
return enums
def get_whitewater_material_enums_ui(scene=None, context=None):
bpy.context.scene.flip_fluid_material_library.check_icons_initialized()
enums = []
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_WHITEWATER')
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_ALL')
enums += __get_non_material_library_enums_by_type()
enums += [__get_none_material_enum()]
enums.reverse()
return enums
def get_material_import_enums_ui(scene = None, context = None):
bpy.context.scene.flip_fluid_material_library.check_icons_initialized()
enums = []
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_SURFACE')
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_WHITEWATER')
enums += __get_material_library_enums_by_type('MATERIAL_TYPE_ALL')
enums += [__get_all_materials_enum()]
enums.reverse()
return enums
def import_material(material_name):
return bpy.context.scene.flip_fluid_material_library.import_material(material_name)
def import_material_copy(material_name):
return bpy.context.scene.flip_fluid_material_library.import_material_copy(material_name)
def is_material_imported(material_name):
return bpy.context.scene.flip_fluid_material_library.is_material_imported(material_name)
def is_material_library_available():
library = bpy.context.scene.flip_fluid_material_library
return len(library.material_list) > 0
def __get_material_library_enums_by_type(material_type):
library = bpy.context.scene.flip_fluid_material_library
enums = []
for mdata in library.material_list:
if mdata.type == material_type:
enums.append(mdata.get_ui_enum())
return enums
def __get_non_material_library_enums_by_type():
enums = []
for m in bpy.data.materials:
if not m.flip_fluid_material_library.is_library_material:
if vcu.is_blender_30():
# material preview can be None in Blender 3.0. Use the preview_ensure function
# to make sure the preview is loaded
m.preview_ensure()
e = (m.name, m.name, "", m.preview.icon_id, __get_non_material_library_material_hash(m))
enums.append(e)
return enums
def __get_non_material_library_material_hash(material):
return int(hashlib.sha1(material.name.encode('utf-8')).hexdigest(), 16) % int(1e6)
def __get_none_material_enum():
return ("MATERIAL_NONE", "None", "", 0, 0)
def __get_all_materials_enum():
return ("ALL_MATERIALS", "All Materials", "Import all materials", 0, 0)
def load_post():
library_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "material_library")
bpy.context.scene.flip_fluid_material_library.load_post()
bpy.context.scene.flip_fluid_material_library.initialize(library_path)
def scene_update_post(scene):
bpy.context.scene.flip_fluid_material_library.scene_update_post(scene)
def register():
bpy.types.Scene.flip_fluid_material_library = PointerProperty(
name="Flip Fluid Material Library",
description="",
type=flip_fluid_material_library.FLIPFluidMaterialLibrary,
)
def unregister():
del bpy.types.Scene.flip_fluid_material_library
@@ -0,0 +1 @@
8de393985416f34d933299f6d8828ee9

Some files were not shown because too many files have changed in this diff Show More