diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..831e89d --- /dev/null +++ b/__init__.py @@ -0,0 +1,155 @@ + +bl_info={ + "name": "Import and export old Unreal .T3D format", + "author": "Crapola", + "version": (1,0,0), + "blender": (2,80,0), + "location": "File > Import-Export ; Object", + "description": "Import and export UnrealED .T3D files.", + "doc_url":"", # TODO: Link to repo. + "tracker_url":"", # TODO: Link to repo. + "support":"COMMUNITY", + "category":"Import-Export", # Category in Add-ons browser. +} + +import bpy + +from . import exporter, importer + +INVALID_FILENAME="Invalid file name." + +class OBJECT_OT_export_t3d_clipboard(bpy.types.Operator): + """Export selected meshes to T3D into the clipboard.""" + bl_idname="object.export_t3d_clipboard" + bl_label="Export T3D to clipboard" + + scale:bpy.props.FloatProperty(name="Scale Multiplier",default=128.0) + + @classmethod + def poll(cls,context): + return context.selected_objects + + def execute(self,context): + sel_objs=[obj for obj in context.selected_objects if obj.type=='MESH'] + num_brushes,txt=exporter.export(sel_objs,self.scale) + context.window_manager.clipboard=txt + self.report({'INFO'},f"{num_brushes} brushes exported to clipboard.") + return {'FINISHED'} + + def invoke(self, context, event): + wm=context.window_manager + return wm.invoke_props_dialog(self) + +class BT3D_MT_file_export(bpy.types.Operator): + """Export T3D file.""" + bl_idname="bt3d.file_export" + bl_label="Export Unreal T3D (.t3d)" + filename:bpy.props.StringProperty(subtype='FILE_NAME') + filepath:bpy.props.StringProperty(subtype='FILE_PATH') + scale:bpy.props.FloatProperty( + name="Scale Multiplier", + default=1.0, + subtype='FACTOR') + def execute(self,context): + if not self.filename.split(".")[0]: + self.report({'ERROR'},INVALID_FILENAME) + return {'CANCELLED'} + # Get meshes in scene. + objs=[obj for obj in context.scene.objects if obj.type=='MESH'] + if not objs: + self.report({'WARNING'},"There are no meshes in scene to export.") + return {'CANCELLED'} + num_brushes,txt=exporter.export(objs,self.scale) + self.filepath=bpy.path.ensure_ext(self.filepath,".t3d") + if not num_brushes: + self.report({'WARNING'},"Nothing was converted.") + return {'CANCELLED'} + with open(self.filepath,"w") as f: + f.write(txt) + self.report({'INFO'},f"{num_brushes} brushes saved to {self.filepath}.") + return {'FINISHED'} + def invoke(self, context, event): + if not self.filepath: + self.filepath=bpy.path.ensure_ext(bpy.data.filepath,".t3d") + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + +class BT3D_MT_file_import(bpy.types.Operator): + """Import T3D file.""" + bl_idname="bt3d.file_import" + bl_label="Import Unreal T3D (.t3d)" + + filename:bpy.props.StringProperty( + name="input filename", + subtype='FILE_NAME' + ) + filepath:bpy.props.StringProperty( + name="input file", + subtype='FILE_PATH' + ) + filter_glob:bpy.props.StringProperty( + default="*.t3d", + options={'HIDDEN'}, + ) + # Options. + flip:bpy.props.BoolProperty( + name="Flip normals", + description="Flip normals of CSG_Subtract brushes", + default=False + ) + snap_vertices:bpy.props.BoolProperty( + name="Snap vertices", + description="Snap to grid.", + default=False + ) + snap_distance:bpy.props.FloatProperty( + name="Snap distance", + default=1.0 + ) + + def execute(self,context): + if not self.filename.split(".")[0]: + self.report({'ERROR'},INVALID_FILENAME) + return {'CANCELLED'} + importer.import_t3d_file( + context, + self.filepath, + self.filename, + self.snap_vertices, + self.snap_distance, + self.flip) + return {'FINISHED'} + + def invoke(self, context, event): + wm=context.window_manager + wm.fileselect_add(self) + return {'RUNNING_MODAL'} + +classes = ( + BT3D_MT_file_export, + BT3D_MT_file_import, + OBJECT_OT_export_t3d_clipboard, +) +register_classes, unregister_classes = bpy.utils.register_classes_factory(classes) + +menus=( + lambda x,_:x.layout.operator(OBJECT_OT_export_t3d_clipboard.bl_idname), + lambda x,_:x.layout.operator(BT3D_MT_file_export.bl_idname), + lambda x,_:x.layout.operator(BT3D_MT_file_import.bl_idname), +) + +def register(): + print("Registering.") + register_classes() + # Add to menu. + bpy.types.VIEW3D_MT_object.append(menus[0]) + bpy.types.TOPBAR_MT_file_export.append(menus[1]) + bpy.types.TOPBAR_MT_file_import.append(menus[2]) + +def unregister(): + print("Unregistering.") + # Remove from menu. + bpy.types.VIEW3D_MT_object.remove(menus[0]) + bpy.types.TOPBAR_MT_file_export.remove(menus[1]) + bpy.types.TOPBAR_MT_file_import.remove(menus[2]) + unregister_classes() \ No newline at end of file diff --git a/exporter.py b/exporter.py new file mode 100644 index 0000000..d48e214 --- /dev/null +++ b/exporter.py @@ -0,0 +1,117 @@ +import math + +import bmesh +import bpy +from mathutils import Euler, Matrix, Vector + +try: + from .t3d import Brush, Polygon, Vertex +except: + from t3d import Brush, Polygon, Vertex + +DEBUG=0 +def _print(*_):pass +if DEBUG:_print=print + +TEXTURE_SIZE=256 + +def basis_from_points(points:list)->Matrix: + """ 2D Basis, middle point is origin. """ + m=Matrix( ((1,0,0),(0,1,0),(0,0,1)) ) + v0,v1,v2=points + m[0].xy=(v2-v1) #X + m[1].xy=(v0-v1) #Y + m[2].xy=v1 # Origin + m.transpose() + return m + +def export(object_list,scale_multiplier=128.0)->tuple: + stuff="" + for obj in object_list: + mesh:bpy.types.Mesh=obj.data + bm=bmesh.new() + bm.from_mesh(mesh) + + uv_layer_0=None + if bm.loops.layers.uv: + uv_layer_0: bmesh.types.BMLayerItem = bm.loops.layers.uv[0] + layer_texture=bm.faces.layers.string.get("texture") + poly_list=[] + for f in bm.faces: + # Vertices. + verts=[Vertex(v.co*scale_multiplier) for v in f.verts] + poly=Polygon(verts,f.normal) + + # Get texture name, either from custom attribute if it exists (brush + # was imported), or material. + if layer_texture: + poly.texture=f[layer_texture].decode('utf-8') + elif len(obj.data.materials)>0: + name=obj.data.materials[f.material_index].name + poly.texture=name + + _print(f"---- Face {poly.texture} ----") + + # Texture coordinates. + + # Compute floor/UV (plane where Z is 0) to surface transform, with + # the first three verts of polygon. + n=f.normal + first_three=f.loops[0:3] + v0,v1,v2=[v.vert.co for v in first_three] + b=Matrix() + b[2].xyz=n + b[0].xyz=(v0-v1) + b[1].xyz=(v2-v1) + b[3].xyz=v0 + b.transpose() # Needed. + b.invert() + axis_x=Vector((1,0,0)) + axis_y=Vector((0,1,0)) + # Basic texturing. + poly.u=b.transposed()@axis_x + poly.v=b.transposed()@axis_y + + # Convert Blender UV Map if it exists. + if uv_layer_0: + v0i=(b@v0) + v1i=(b@v1) + v2i=(b@v2) + # We assume UVs are a linear transform of the polygon shape. + # Figure out that transform by using the first three verts. + first_three_uvs=[l[uv_layer_0].uv for l in f.loops[0:3]] + u0,u1,u2=first_three_uvs + # mv=quad->poly, mu=poly->uv. + mv=basis_from_points( (v0i.xy,v1i.xy,v2i.xy) ) + mu=basis_from_points( (u0,u1,u2) ) + t=mu @ mv.inverted() + t.resize_4x4() + t.transpose() + b.transpose() + + poly.u=b @ (t @ axis_x*TEXTURE_SIZE/scale_multiplier) + poly.v=b @ (t @ axis_y*TEXTURE_SIZE/scale_multiplier) + + # Pan. + pan=mu[1]*TEXTURE_SIZE + if pan: + poly.pan=(int(pan.x),int(pan.y)) + + poly_list.append(poly) + + brush=Brush(poly_list,obj.location*scale_multiplier,obj.name) + + if obj.rotation_euler!=Euler((0,0,0)): + brush.rotation=Vector(obj.rotation_euler)*65536/math.tau + if obj.scale!=Vector((0,0,0)): + brush.mainscale=obj.scale + if obj.get("csg"): + brush.csg=obj["csg"] + + stuff+=str(brush) + + bm.to_mesh(mesh) + bm.free() + + everything=f"""Begin Map\n{stuff}End Map\n""" + return len(object_list),everything \ No newline at end of file diff --git a/importer.py b/importer.py new file mode 100644 index 0000000..10cfed1 --- /dev/null +++ b/importer.py @@ -0,0 +1,143 @@ +import math + +import bmesh +import bpy +import mathutils +from mathutils import Euler, Vector + +try: + from . import t3d_parser +except: + import t3d_parser + pass + #t3d_parser=bpy.data.texts["t3d_parser.py"].as_module() + +TEXTURE_SIZE:float=256.0 + +def convert_uv(mesh_vertex:Vector,texture_u:Vector,texture_v:Vector)->Vector: + return Vector((mesh_vertex.dot(texture_u),mesh_vertex.dot(texture_v)))/TEXTURE_SIZE + +# Get material index using material name in object. +# If material is not found on object, return 0. +def material_index_by_name(obj,matname:str)->int: + mat_dict = {mat.name: i for i, mat in enumerate(obj.data.materials)} + try: + ret=mat_dict[matname] + return ret + except KeyError: + return 0 + +def import_t3d_file( + context:bpy.context, + filepath:str, + filename:str, + snap_vertices:bool, + snap_distance:float, + flip:bool, + ): + + brushes=t3d_parser.t3d_open(filepath) + coll=bpy.data.collections.new(filename) + context.scene.collection.children.link(coll) + for b in brushes: + print(f"Importing {b.actor_name}...") + if b.group=='"Cube"': + # Ignore red brush. + print(f"{b.actor_name} is the red brush.") + continue + data=b.get_pydata() + + # Invert Y's + # for v in data[0]: + # v[1]=-v[1] + # if b.location: + # b.location=(b.location[0],-b.location[1],b.location[2]) + # if b.prepivot: + # b.prepivot=(b.prepivot[0],-b.prepivot[1],b.prepivot[2]) + + # Snap to grid. + if snap_vertices: + b.snap(snap_distance) + + # Create mesh. + m=bpy.data.meshes.new(b.actor_name) + m.from_pydata(*data) + m.update() + + # Flip. + if b.csg=="CSG_Subtract" and flip: + print("FLIP!") + m.flip_normals() + + # Create object. + o=bpy.data.objects.new(b.actor_name,m) + coll.objects.link(o) + # Location. + o.location=b.location or (0,0,0) + # Color by CSG (for ViewPort Shading in Object mode). + o.color=(1,0.5,0,1) if b.csg=="CSG_Subtract" else (0,0,1,1) + + # Apply transforms. + + mainscale=Vector(b.mainscale or (1,1,1)) + pivot=Vector(b.prepivot or (0,0,0)) + postscale=Vector(b.postscale or (1,1,1)) + rotation=Vector(b.rotation or (0,0,0))*math.tau/65536 + rotation.xy=-rotation.xy + rotation=Euler(rotation) + + pivot.rotate(rotation) + pivot*=postscale*mainscale + + print(f"{b.actor_name} PrePivot=",pivot) + + o.scale=mainscale + o.rotation_euler=rotation + o.location-=pivot + + bm=bmesh.new() + bm.from_mesh(m) + # Create UV layer. + uv_layer=bm.loops.layers.uv.verify() + # Polygon attributes. + texture_names=[p.texture for p in b.polygons] + flags=[p.flags for p in b.polygons] + layer_texture=bm.faces.layers.string.get("texture") or bm.faces.layers.string.new("texture") + layer_flags=bm.faces.layers.int.get("flags") or bm.faces.layers.int.new("flags") + for i,face in enumerate(bm.faces): + if texture_names[i]: + face[layer_texture]=bytes(str(texture_names[i]),'utf-8') + scene_mat=bpy.data.materials.get(texture_names[i]) + # Add material to object if it's not there yet. + if scene_mat and not (texture_names[i] in o.data.materials): + o.data.materials.append(scene_mat) + # Assign the face. + face.material_index=material_index_by_name(o,texture_names[i]) + face[layer_flags]=flags[i] + print(face.calc_tangent_edge()) + + #bm.faces.layers.tex.verify() + # UV coordinates. + poly=b.polygons[i] + for j,loop in enumerate(face.loops): + vert=loop.vert.co + tu=Vector(poly.u) + tv=Vector(poly.v) + pan=Vector(poly.pan)/TEXTURE_SIZE if poly.pan else Vector((0,0)) + loop[uv_layer].uv=convert_uv(vert,tu,tv)+pan + + bm.to_mesh(m) + bm.free() + + # PostScale requires applying previous transforms. + if b.postscale: + print("Postscale ",b.postscale) + o.select_set(True) + bpy.context.view_layer.objects.active=o + bpy.ops.object.transform_apply(scale=True,rotation=True,location=False) + o.scale=b.postscale + + # Keep Unreal stuff as Custom Properties. + o["csg"]=b.csg + + diff --git a/t3d.py b/t3d.py new file mode 100644 index 0000000..c46a679 --- /dev/null +++ b/t3d.py @@ -0,0 +1,162 @@ +import math + +def format_float(value:float)->str: + return f"{value:+#013.06f}" + +def format_vector(values:tuple)->str: + return ",".join([format_float(x) for x in values]) + +def round_to_grid(value:float,grid:float)->float: + return round(value/grid)*grid + +class Vertex: + def __init__(self,coords:list[float]=None): + self.coords=list(coords) if coords else [0.,0.,0.] + def __add__(self,other:'Vertex'): + return Vertex([self.coords[i]+other.coords[i] for i in range(3)]) + def __getitem__(self,index:int): + return self.coords[index] + def __mul__(self,value:float): + return Vertex([self.coords[i]*value for i in range(3)]) + def __str__(self)->str: + return f"Vertex\t{format_vector(self.coords)}\n" + def __sub__(self,other:'Vertex'): + return Vertex([self.coords[i]-other.coords[i] for i in range(3)]) + def __truediv__(self,value:float): + return Vertex([self.coords[i]/value for i in range(3)]) + def distance_to(self,other:'Vertex'): + return (other-self).length() + def length(self)->float: + return math.hypot(*self.coords) + def normalized(self)->'Vertex': + return self/self.length() + def setx(self,value:float): + self.coords[0]=value + def sety(self,value:float): + self.coords[1]=value + def setz(self,value:float): + self.coords[2]=value + def snap(self,grid_distance:float): + for i in range(3): + self.coords[i]=round_to_grid(self.coords[i],grid_distance) + x=property(lambda self:self.coords[0],setx,None,"X") + y=property(lambda self:self.coords[1],sety,None,"Y") + z=property(lambda self:self.coords[2],setz,None,"Z") + +class Polygon: + def __init__(self,verts:list[Vertex]=None,normal=None): + self.pan:tuple[int]=None + self.u=(1.,0.,0.) + self.v=(0.,0.,1.) + self.vertices=verts if verts else [] + self.normal=normal + self.texture:str="" + self.flags:int=0 + def __str__(self)->str: + vertices="".join([str(v) for v in self.vertices]) + uv=f"TextureU\t{format_vector(self.u)}\nTextureV\t{format_vector(self.v)}\n" + texture=f" Texture={self.texture}" if self.texture else "" + flags=f" Flags={self.flags}" if self.flags else "" + pan=f"Pan U={self.pan[0]} V={self.pan[1]}\n" if self.pan else "" + polygon=f"Begin Polygon{texture}{flags}\n{pan}{uv}{vertices}End Polygon\n" + return polygon + def add_vertices(self,vert_list:list): + self.vertices+=[Vertex(v) for v in vert_list] + def length_xy(self)->float: + l=0 + for v in self.vertices: + for v2 in self.vertices: + v.z=0.0 + v2.z=0.0 + curr=v.distance_to(v2) + l=curr if curr>l else l + return l + +class Brush: + def __init__(self,poly_list:list[Polygon]=None,location:list=None,actor_name=None): + # Actor name can be omitted, UED will create one. + self.actor_name:str=actor_name + self.brush_name:str="Brush" + self.csg:str="CSG_Subtract" + self.mainscale:tuple=None + self.postscale:tuple=None + self.tempscale:tuple=None # TODO: remove tempscale + self.group:str=None + self.polygons=poly_list if poly_list else [] + self.location=location + self.rotation:tuple=None + self.prepivot=None + def __str__(self)->str: + polygons="".join([str(p) for p in self.polygons]) + location_prefixes=("X","Y","Z") + + if self.mainscale: + mainscale_txt=f"MainScale=(Scale=("+ ",".join([x[0]+'='+str(x[1]) for x in zip(location_prefixes,self.mainscale)]) +"),SheerAxis=SHEER_ZX)\n" + else: + mainscale_txt="" + + if self.postscale: + postscale_txt=f"PostScale=(Scale=("+ ",".join([x[0]+'='+str(x[1]) for x in zip(location_prefixes,self.postscale)]) +"),SheerAxis=SHEER_ZX)\n" + else: + postscale_txt="" + + if self.tempscale: + tempscale_txt=f"TempScale=(Scale=("+ ",".join([x[0]+'='+str(x[1]) for x in zip(location_prefixes,self.tempscale)]) +"),SheerAxis=SHEER_ZX)\n" + else: + tempscale_txt="" + + if self.group: + group_txt=f"Group={self.group}\n" + else: + group_txt="" + if self.location: + location_txt="Location=("+",".join([x[0]+'='+str(x[1]) for x in zip(location_prefixes,self.location)])+")\n" + else: + location_txt="" + + rotation_prefixes=("Roll","Pitch","Yaw") + if self.rotation: + rotation_txt="Rotation=("+ ",".join([x[0]+'='+str(int(round(x[1]))) for x in zip(rotation_prefixes,self.rotation)]) +")\n" + else: + rotation_txt="" + + if self.prepivot: + prepivot_txt="PrePivot=("+",".join([x[0]+'='+str(x[1]) for x in zip(location_prefixes,self.prepivot)])+")\n" + else: + prepivot_txt="" + actor_name=f"Name={self.actor_name}" if self.actor_name else "" + nl="\n" + brush=f"""Begin Actor Class=Brush {actor_name} +CsgOper={self.csg} +bSelected=True +{mainscale_txt}\ +{postscale_txt}\ +{tempscale_txt}\ +{group_txt}\ +{location_txt}\ +{rotation_txt}\ +Begin Brush Name={self.brush_name} +Begin PolyList +{polygons}End PolyList +End Brush +Brush=Model'MyLevel.{self.brush_name}' +{prepivot_txt}\ +{actor_name}{nl if actor_name else ""}\ +End Actor +""" + return brush + def get_pydata(self)->list: + verts=[v.coords for p in self.polygons for v in p.vertices] + edges=[] + faces=[] + i=0 + for p in self.polygons: + faces.append(list(range(i,i+len(p.vertices)))) + i+=len(p.vertices) + return verts,edges,faces + def snap(self,grid_distance:float=1.0): + for p in self.polygons: + for i in range(len(p.vertices)): + p.vertices[i].snap(grid_distance) + if self.location: + self.location=[round_to_grid(v,grid_distance) for v in self.location] \ No newline at end of file diff --git a/t3d_parser.py b/t3d_parser.py new file mode 100644 index 0000000..503c963 --- /dev/null +++ b/t3d_parser.py @@ -0,0 +1,218 @@ +import lark +from lark.visitors import Visitor + +try: + from . import t3d +except: + import t3d + +DEBUG=0 +def _print(*_):pass +if DEBUG:_print=print + +class Vis(Visitor): + """ Build brushes as it visits the tree.""" + + def __init__(self) -> None: + super().__init__() + # List of brushes created after full visit. + self.brushes=[] + # Current brush info. + self.context=[] + + def begin_actor(self,tree): + actor_class=tree.children[0].value + actor_name=tree.children[1].value + self.context.append((actor_class,actor_name)) + if actor_class=="Brush": + self.context.append(t3d.Brush(actor_name=actor_name)) + _print(f"") + else: + self.context.append(None) + + def block_end(self,tree): + n=tree.children[0].children[0] + if n=="Actor": + if self.context[0][0]=="Brush": + self.brushes.append(self.context[1]) + _print(f"") + self.context.clear() + if n=="Polygon": + _print(" ") + + def csg(self,tree): + csg=tree.children[0] + _print(f" ") + self.curr_brush().csg=tree.children[0] + + def curr_brush(self)->t3d.Brush: + return self.context[-1] + def curr_brush_name(self)->str: + return self.curr_brush().actor_name + def curr_polygon(self)->t3d.Polygon: + return self.curr_brush().polygons[-1] + + def group(self,tree): + group_name=tree.children[0] + self.curr_brush().group=group_name + _print(f" ") + + def location(self,tree): + if self.context[0][0]!="Brush": + _print("Skip location for ",self.context) + return + curr_brush=self.curr_brush() + coords="loc_x","loc_y","loc_z" + x,y,z=[list(tree.find_data(c)) for c in coords] + x,y,z=[float(c[0].children[0] if c else 0) for c in (x,y,z)] + curr_brush.location=x,y,z + _print(f" <{self.curr_brush_name()} Location={curr_brush.location}>") + + def mainscale(self,tree): + if not self.curr_brush():return + if not tree.children:return + coords="loc_x","loc_y","loc_z" + x,y,z=[list(tree.find_data(c)) for c in coords] + x,y,z=[float(c[0].children[0] if c else 1) for c in (x,y,z)] + self.curr_brush().mainscale=x,y,z + _print(f" <{self.curr_brush_name()} MainScale={x,y,z}>") + + def pan(self,tree): + if not self.curr_brush():return + u,v=[int(n.value) for n in tree.children] + self.curr_polygon().pan=u,v + _print(f" ") + + def postscale(self,tree): + if not self.curr_brush():return + if not tree.children:return + coords="loc_x","loc_y","loc_z" + x,y,z=[list(tree.find_data(c)) for c in coords] + x,y,z=[float(c[0].children[0] if c else 1) for c in (x,y,z)] + self.curr_brush().postscale=x,y,z + _print(f" <{self.curr_brush_name()} PostScale={x,y,z}>") + + def rotation(self,tree): + if not self.curr_brush() or not tree.children:return + coords="roll","pitch","yaw" + x,y,z=[list(tree.find_data(c)) for c in coords] + x,y,z=[float(c[0].children[0] if c else 0) for c in (x,y,z)] + self.curr_brush().rotation=x,y,z + _print(f" <{self.curr_brush_name()} Rotation={x,y,z}>") + + def tempscale(self,tree): + if not self.curr_brush():return + if not tree.children:return + coords="loc_x","loc_y","loc_z" + x,y,z=[list(tree.find_data(c)) for c in coords] + x,y,z=[float(c[0].children[0] if c else 1) for c in (x,y,z)] + self.curr_brush().tempscale=x,y,z + _print(f" <{self.curr_brush_name()} TempScale={x,y,z}>") + + def prepivot(self,tree): + if not self.curr_brush():return + curr_brush=self.curr_brush() + coords="loc_x","loc_y","loc_z" + x,y,z=[list(tree.find_data(c)) for c in coords] + x,y,z=[float(c[0].children[0] if c else 0) for c in (x,y,z)] + curr_brush.prepivot=x,y,z + _print(f" <{self.curr_brush_name()} PrePivot={x,y,z}>") + + def begin_polygon(self,tree): + if not self.curr_brush():return + texname=list(tree.find_data("texture_name")) + flags=list(tree.find_data("flags")) + flags=flags[0].children[0].value if flags else 0 + if texname: + texname=texname[0].children[0] + else: + texname=None + _print(f" ") + curr_brush=self.curr_brush() + new_poly=t3d.Polygon() + new_poly.texture=texname + new_poly.flags=int(flags) + curr_brush.polygons.append(new_poly) + + def _get_coords(tree): + return [float(token.value) for token in tree.children] + + def textureu(self,tree): + if not self.curr_brush():return + x,y,z=Vis._get_coords(tree) + _print(f" ") + self.curr_polygon().u=x,y,z + + def texturev(self,tree): + if not self.curr_brush():return + x,y,z=Vis._get_coords(tree) + _print(f" ") + self.curr_polygon().v=x,y,z + + def vertex(self,tree): + if not self.curr_brush():return + x,y,z=[float(token.value) for token in tree.children] + _print(f" ") + self.curr_polygon().add_vertices(((x,y,z),)) + +def t3d_open(path:str)->list[t3d.Brush]: + with open(path) as f: + text=f.read() + l=lark.Lark(""" +start: block+ +block: block_start content+ block_end +block_start: begin_actor|begin_brush|begin_polygon|begin_other +block_end: "End" block_name +block_name:WORD +?vertex:"Vertex" SIGNED_NUMBER "," SIGNED_NUMBER "," SIGNED_NUMBER +pan:"Pan" WS* "U=" SIGNED_NUMBER "V=" SIGNED_NUMBER +?textureu:"TextureU" SIGNED_NUMBER "," SIGNED_NUMBER "," SIGNED_NUMBER +?texturev:"TextureV" SIGNED_NUMBER "," SIGNED_NUMBER "," SIGNED_NUMBER +content:block|csg|group|mainscale|postscale|tempscale|location|rotation|prepivot|pan|textureu|texturev|vertex|IGNORED +begin_actor: "Begin Actor Class="resource_name "Name="resource_name +begin_brush: "Begin Brush Name=" WORD +begin_polygon: "Begin Polygon" ("Item="resource_name| "Texture="texture_name | "Flags="flags | "Link="NUMBER)* +begin_other.-1: "Begin" block_name +mainscale: "MainScale=(" scale? ","? "SheerAxis=SHEER_ZX)" +postscale: "PostScale=(" scale? ","? "SheerAxis=SHEER_ZX)" +tempscale: "TempScale=(" scale? ","? "SheerAxis=SHEER_ZX)" +scale: "Scale=(" loc_x? ","? loc_y? ","? loc_z? ")" +location: "Location=(" loc_x? ","? loc_y? ","? loc_z? ")" +prepivot: "PrePivot=(" loc_x? ","? loc_y? ","? loc_z? ")" +csg:"CsgOper="resource_name +loc_x: "X=" SIGNED_NUMBER +loc_y: "Y=" SIGNED_NUMBER +loc_z: "Z=" SIGNED_NUMBER +rotation: "Rotation=(" (roll|pitch|yaw|",")* ")" +roll: "Roll=" SIGNED_NUMBER +pitch: "Pitch=" SIGNED_NUMBER +yaw: "Yaw=" SIGNED_NUMBER +group: "Group=" STRING +texture_name:resource_name +flags:NUMBER +?resource_name:/[\S.\d_]+/ +IGNORED.-1:WS?/.+/ NL +%import common.NUMBER +%import common.SIGNED_NUMBER +%import common.DIGIT +%import common.WORD +%import common.ESCAPED_STRING -> STRING +%import common.WS +%import common.NEWLINE -> NL +%ignore WS +""",parser="lalr") + tree=l.parse(text) + v=Vis() + v.visit_topdown(tree) + return v.brushes + +def main(): + brushes=t3d_open("samples/sample.t3d") + #brushes=t3d_open("samples/dm-deck16.t3d") + print("Test") + print("----") + print(brushes) + for b in brushes:print(b) + +if __name__=="__main__": + main() \ No newline at end of file