2025-04-02 12:54:43 -05:00

199 lines
4.7 KiB
Python

from dataclasses import dataclass
import sys
import os
@dataclass(frozen=True)
class FixedPoint:
s: int # sign
w: int # whole
f: int # fraction
b: int # base
def to_string(self):
s_s = "-" if self.s < 0 else ""
w_s = f"{self.w}"
f_s = f"{self.f}".rjust(self.b, '0')
return f"{s_s}{w_s}.{f_s}"
@dataclass(frozen=True)
class VertexPosition:
x: FixedPoint
y: FixedPoint
z: FixedPoint
@dataclass(frozen=True)
class VertexNormal:
x: FixedPoint
y: FixedPoint
z: FixedPoint
@dataclass(frozen=True)
class VertexTexture:
x: FixedPoint
y: FixedPoint
z: FixedPoint
@dataclass(frozen=True)
class ObjectEvent:
name: str
@dataclass(frozen=True)
class FacePTN:
position: int
texture: int
normal: int
@dataclass
class Face:
ptn: list[FacePTN]
@dataclass
class Material:
name: str
@dataclass
class Object:
name: str
faces: list[Face]
material: Material
def __init__(self, name, faces=None, material=None):
self.name = name
self.faces = [] if faces is None else faces
self.material = material
@dataclass
class ObjFile:
position: list[VertexPosition]
normal: list[VertexNormal]
texture: list[VertexTexture]
objects: list[Object]
def __init__(self, position=None, normal=None, texture=None, objects=None):
self.position = [] if position is None else position
self.normal = [] if normal is None else normal
self.texture = [] if texture is None else texture
self.objects = [] if objects is None else objects
def parse_float(s):
sign = -1 if s.startswith("-") else 1
s = s.removeprefix("-")
if '.' not in s:
return sign * int(s, 10)
i, f = s.split('.')
f_digits = len(f)
i = int(i, 10)
f = int(f, 10)
return FixedPoint(sign, i, f, f_digits)
def parse_vertex(line, n, type):
assert len(line) == n
vs = [parse_float(line[i]) for i in range(n)]
if len(vs) < 3:
vs.append(0)
return type(*vs)
def parse_vertex_position(line):
return parse_vertex(line, 3, VertexPosition)
def parse_vertex_normal(line):
return parse_vertex(line, 3, VertexNormal)
def parse_vertex_texture(line):
return parse_vertex(line, 2, VertexTexture)
def parse_face_indices(indices):
assert "/" in indices
indices = indices.split("/")
assert len(indices) == 3, indices
def face_ix(s):
i = int(s, 10)
assert i >= 1
return i - 1
return FacePTN(*(face_ix(i) for i in indices))
def parse_face(line):
return Face([parse_face_indices(indices) for indices in line])
def parse_object_event(line):
assert len(line) == 1
name, = line
name = name.replace(".", "_").replace("-", "_")
return ObjectEvent(name)
def parse_usemtl(line):
assert len(line) == 1
name, = line
return Material(name)
def parse_line(line):
t, *line = line.split(' ')
if t == '#':
return None
if t == 'usemtl':
return parse_usemtl(line)
if t == 'mtllib':
return None
if t == 'o':
return parse_object_event(line)
if t == 'v':
return parse_vertex_position(line)
if t == 'vn':
return parse_vertex_normal(line)
if t == 'vt':
return parse_vertex_texture(line)
if t == 'f':
return parse_face(line)
if t == 's':
# smooth shading
return None
if t == 'l':
# polyline
return None
assert False, (t, line)
def parse_obj_lines(lines):
file = ObjFile()
object = Object(None)
for line in lines:
x = parse_line(line)
if x is None:
continue
elif type(x) is VertexPosition:
file.position.append(x)
elif type(x) is VertexNormal:
file.normal.append(x)
elif type(x) is VertexTexture:
file.texture.append(x)
elif type(x) is ObjectEvent:
if object.faces:
assert object.name != None
file.objects.append(object)
object = Object(x.name.replace('-', '_').replace('.', '_'))
elif type(x) is Face:
object.faces.append(x)
elif type(x) is Material:
if object.material != None:
file.objects.append(object)
object = Object(object.name + "_mtl_" + x.name)
object.material = x
else:
assert False, x
if object.faces:
assert object.name != None
file.objects.append(object)
return file
def parse_obj_file(filename):
with open(filename, "r") as f:
lines = f.read().strip().split("\n")
file = parse_obj_lines(lines)
return file
if __name__ == "__main__":
file = parse_obj_file(sys.argv[1])
from pprint import pprint
pprint(file)