feat: allow exporting object in virtools file which has geometry.

- allow exporting object without apply modifier.
- allow exporting any objects which can be mesh (has geometry).
- due to this change, add virtools mesh properties for metaball, font, curve, surface.
- due to this change, remove the virtools group warning for metaball, font, curve and surface.
This commit is contained in:
2026-03-05 21:45:36 +08:00
parent 68fbffad54
commit 8a7e3306a7
4 changed files with 86 additions and 42 deletions

View File

@@ -159,28 +159,33 @@ def _prepare_virtools_3dobjects(
# iterate exported object list # iterate exported object list
for obj3d in export_objects: for obj3d in export_objects:
# only accept mesh object and light object # only accept mesh-like object, camera and light object
# all of other objects will be discard. # all of other objects will be discard.
match(obj3d.type): if UTIL_blender_mesh.TemporaryMesh.has_geometry(obj3d):
case 'MESH': # mesh-like object
# mesh object if obj3d not in obj3d_cret_set:
if obj3d not in obj3d_cret_set: # add into set
# add into set obj3d_cret_set.add(obj3d)
obj3d_cret_set.add(obj3d) # create virtools instance
# create virtools instance vtobj3d: bmap.BM3dObject = writer.create_3dobject()
vtobj3d: bmap.BM3dObject = writer.create_3dobject() # add into result list
# add into result list obj3d_crets.append((obj3d, vtobj3d))
obj3d_crets.append((obj3d, vtobj3d)) else:
case 'LIGHT': match(obj3d.type):
# light object case 'CAMERA':
if obj3d not in light_cret_set: # camera object
# add into set # TODO
light_cret_set.add(obj3d) pass
# create virtools instance case 'LIGHT':
vtlight: bmap.BMTargetLight = writer.create_target_light() # light object
# add into result list if obj3d not in light_cret_set:
light_crets.append((obj3d, typing.cast(bpy.types.Light, obj3d.data), vtlight)) # add into set
light_cret_set.add(obj3d)
# create virtools instance
vtlight: bmap.BMTargetLight = writer.create_target_light()
# add into result list
light_crets.append((obj3d, typing.cast(bpy.types.Light, obj3d.data), vtlight))
# step progress no matter whether create new one # step progress no matter whether create new one
progress.step() progress.step()

View File

@@ -1,6 +1,6 @@
import bpy import bpy
import typing, enum import typing, enum
from . import UTIL_functions, UTIL_icons_manager from . import UTIL_functions, UTIL_icons_manager, UTIL_blender_mesh
#region Virtools Groups Define & Help Class #region Virtools Groups Define & Help Class
@@ -387,9 +387,9 @@ class BBP_PT_virtools_groups(bpy.types.Panel):
target = typing.cast(bpy.types.Object, context.active_object) target = typing.cast(bpy.types.Object, context.active_object)
# notify on non-mesh object # notify on non-mesh object
if target.type != 'MESH': if not UTIL_blender_mesh.TemporaryMesh.has_geometry(target):
layout.label( layout.label(
text='Virtools Group is invalid on non-mesh object!', icon='ERROR', text='Virtools Group is invalid on non-mesh-like object!', icon='ERROR',
text_ctxt='BBP_PT_virtools_groups/draw') text_ctxt='BBP_PT_virtools_groups/draw')
# draw main body # draw main body

View File

@@ -1,6 +1,6 @@
import bpy import bpy
import typing, enum import typing, enum
from . import UTIL_functions, UTIL_virtools_types from . import UTIL_functions, UTIL_blender_mesh, UTIL_virtools_types
# Raw Data # Raw Data
@@ -30,19 +30,21 @@ class BBP_PG_virtools_mesh(bpy.types.PropertyGroup):
# Getter Setter # Getter Setter
def get_virtools_mesh(mesh: bpy.types.Mesh) -> BBP_PG_virtools_mesh: CanToMesh = bpy.types.Mesh | bpy.types.Curve | bpy.types.SurfaceCurve | bpy.types.TextCurve | bpy.types.MetaBall
return mesh.virtools_mesh
def get_raw_virtools_mesh(mesh: bpy.types.Mesh) -> RawVirtoolsMesh: def get_virtools_mesh(meshlike: CanToMesh) -> BBP_PG_virtools_mesh:
props: BBP_PG_virtools_mesh = get_virtools_mesh(mesh) return meshlike.virtools_mesh
def get_raw_virtools_mesh(meshlike: CanToMesh) -> RawVirtoolsMesh:
props: BBP_PG_virtools_mesh = get_virtools_mesh(meshlike)
rawdata: RawVirtoolsMesh = RawVirtoolsMesh() rawdata: RawVirtoolsMesh = RawVirtoolsMesh()
rawdata.mLitMode = _g_Helper_VXMESH_LITMODE.get_selection(props.lit_mode) rawdata.mLitMode = _g_Helper_VXMESH_LITMODE.get_selection(props.lit_mode)
return rawdata return rawdata
def set_raw_virtools_mesh(mesh: bpy.types.Mesh, rawdata: RawVirtoolsMesh) -> None: def set_raw_virtools_mesh(meshlike: CanToMesh, rawdata: RawVirtoolsMesh) -> None:
props: BBP_PG_virtools_mesh = get_virtools_mesh(mesh) props: BBP_PG_virtools_mesh = get_virtools_mesh(meshlike)
props.lit_mode = _g_Helper_VXMESH_LITMODE.to_selection(rawdata.mLitMode) props.lit_mode = _g_Helper_VXMESH_LITMODE.to_selection(rawdata.mLitMode)
@@ -59,12 +61,22 @@ class BBP_PT_virtools_mesh(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.mesh is not None if context.mesh is not None: return True
if context.curve is not None: return True
if context.meta_ball is not None: return True
return False
def draw(self, context): def draw(self, context):
# get layout and target # get layout
layout = self.layout layout = self.layout
props: BBP_PG_virtools_mesh = get_virtools_mesh(context.mesh) # get target
datablock: typing.Any
if context.mesh is not None: datablock = context.mesh
elif context.curve is not None: datablock = context.curve
elif context.meta_ball is not None: datablock = context.meta_ball
else: datablock = None
# get mesh properties
props: BBP_PG_virtools_mesh = get_virtools_mesh(datablock)
# draw data # draw data
layout.prop(props, 'lit_mode') layout.prop(props, 'lit_mode')
@@ -75,11 +87,21 @@ def register() -> None:
bpy.utils.register_class(BBP_PG_virtools_mesh) bpy.utils.register_class(BBP_PG_virtools_mesh)
bpy.utils.register_class(BBP_PT_virtools_mesh) bpy.utils.register_class(BBP_PT_virtools_mesh)
# add into mesh metadata # Add metadata into mesh-like data block.
# according to TemporaryMesh, we need add it into:
# mesh, curve, surface, font, and metaball.
bpy.types.Mesh.virtools_mesh = bpy.props.PointerProperty(type = BBP_PG_virtools_mesh) bpy.types.Mesh.virtools_mesh = bpy.props.PointerProperty(type = BBP_PG_virtools_mesh)
bpy.types.Curve.virtools_mesh = bpy.props.PointerProperty(type = BBP_PG_virtools_mesh)
bpy.types.SurfaceCurve.virtools_mesh = bpy.props.PointerProperty(type = BBP_PG_virtools_mesh)
bpy.types.TextCurve.virtools_mesh = bpy.props.PointerProperty(type = BBP_PG_virtools_mesh)
bpy.types.MetaBall.virtools_mesh = bpy.props.PointerProperty(type = BBP_PG_virtools_mesh)
def unregister() -> None: def unregister() -> None:
# remove from metadata # remove from metadata
del bpy.types.MetaBall.virtools_mesh
del bpy.types.TextCurve.virtools_mesh
del bpy.types.SurfaceCurve.virtools_mesh
del bpy.types.Curve.virtools_mesh
del bpy.types.Mesh.virtools_mesh del bpy.types.Mesh.virtools_mesh
bpy.utils.unregister_class(BBP_PT_virtools_mesh) bpy.utils.unregister_class(BBP_PT_virtools_mesh)

View File

@@ -105,22 +105,37 @@ def _nest_custom_split_normal(nml_array: array.array) -> typing.Iterator[UTIL_vi
class TemporaryMesh(): class TemporaryMesh():
""" """
Create a temporary mesh for convenient exporting. Create a temporary mesh for convenient exporting.
When exporting mesh, we need triangulate it first. When exporting mesh, we need evaluate it first, then triangulate it.
We create a temporary mesh to hold the triangulated mesh result. We create a temporary mesh to hold the evaluated and triangulated mesh result.
So that original object will not be affected and keep its original geometry. So that original object will not be affected and keep its original geometry.
Please note passed bpy.types.Object must be Mesh Object. Please note passed bpy.types.Object must be an object which can be converted into mesh.
You can use this class provided static method to check it.
""" """
__cAllowedObjectType: typing.ClassVar[set[str]] = {
'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'
}
@staticmethod
def has_geometry(obj: bpy.types.Object):
"""
Check whether given Blender object has geometry.
If it has, it can safely utilize this class for visiting mesh.
"""
return obj.type in TemporaryMesh.__cAllowedObjectType
__mBindingObject: bpy.types.Object __mBindingObject: bpy.types.Object
__mEvaluatedObject: bpy.types.Object
__mTempMesh: bpy.types.Mesh __mTempMesh: bpy.types.Mesh
def __init__(self, binding_obj: bpy.types.Object): def __init__(self, binding_obj: bpy.types.Object):
depsgraph = bpy.context.evaluated_depsgraph_get()
self.__mBindingObject = binding_obj self.__mBindingObject = binding_obj
self.__mTempMesh = None self.__mEvaluatedObject = self.__mBindingObject.evaluated_get(depsgraph)
self.__mTempMesh = self.__mEvaluatedObject.to_mesh()
if self.__mBindingObject.data is None: if self.__mTempMesh is None:
raise UTIL_functions.BBPException('try getting mesh from an object without mesh.') raise UTIL_functions.BBPException('try getting mesh from an object without mesh.')
self.__mTempMesh = self.__mBindingObject.to_mesh()
def __enter__(self): def __enter__(self):
return self return self
@@ -129,6 +144,7 @@ class TemporaryMesh():
self.dispose() self.dispose()
def is_valid(self) -> bool: def is_valid(self) -> bool:
if self.__mBindingObject is None: return False
if self.__mBindingObject is None: return False if self.__mBindingObject is None: return False
if self.__mTempMesh is None: return False if self.__mTempMesh is None: return False
return True return True
@@ -136,7 +152,8 @@ class TemporaryMesh():
def dispose(self) -> None: def dispose(self) -> None:
if self.is_valid(): if self.is_valid():
self.__mTempMesh = None self.__mTempMesh = None
self.__mBindingObject.to_mesh_clear() self.__mEvaluatedObject.to_mesh_clear()
self.__mEvaluatedObject = None
self.__mBindingObject = None self.__mBindingObject = None
def get_temp_mesh(self) -> bpy.types.Mesh: def get_temp_mesh(self) -> bpy.types.Mesh: