diff --git a/pywavefront/__init__.pyi b/pywavefront/__init__.pyi new file mode 100644 index 0000000..0cbd2b9 --- /dev/null +++ b/pywavefront/__init__.pyi @@ -0,0 +1,10 @@ +from _typeshed import Incomplete +from pywavefront.exceptions import PywavefrontException as PywavefrontException +from pywavefront.obj import ObjParser as ObjParser +from pywavefront.wavefront import Wavefront as Wavefront + +__version__: str +logger: Incomplete +log_handler: Incomplete + +def configure_logging(level, formatter: Incomplete | None = None) -> None: ... diff --git a/pywavefront/cache.pyi b/pywavefront/cache.pyi new file mode 100644 index 0000000..ae3af9d --- /dev/null +++ b/pywavefront/cache.pyi @@ -0,0 +1,59 @@ +from _typeshed import Incomplete +from pywavefront.material import MaterialParser + +logger: Incomplete + +def cache_name(path): + """Generate the name of the binary cache file""" +def meta_name(path): + """Generate the name of the meta file""" + +class CacheLoader: + material_parser_cls = MaterialParser + wavefront: Incomplete + file_name: Incomplete + path: Incomplete + encoding: Incomplete + strict: Incomplete + dir: Incomplete + meta: Incomplete + def __init__(self, file_name, wavefront, strict: bool = False, create_materials: bool = False, encoding: str = 'utf-8', parse: bool = True, **kwargs) -> None: ... + def parse(self): ... + def load_vertex_buffer(self, fd, material, length) -> None: + """ + Load vertex data from file. Can be overriden to reduce data copy + + :param fd: file object + :param material: The material these vertices belong to + :param length: Byte length of the vertex data + """ + +class CacheWriter: + file_name: Incomplete + wavefront: Incomplete + meta: Incomplete + def __init__(self, file_name, wavefront) -> None: ... + def write(self) -> None: ... + +class Meta: + """ + Metadata for binary obj cache files + """ + format_version: str + def __init__(self, **kwargs) -> None: ... + def add_vertex_buffer(self, material, vertex_format, byte_offset, byte_length) -> None: + """Add a vertex buffer""" + @classmethod + def from_file(cls, path): ... + def write(self, path) -> None: + """Save the metadata as json""" + @property + def version(self): ... + @property + def created_at(self): ... + @property + def vertex_buffers(self): ... + @property + def mtllibs(self): ... + @mtllibs.setter + def mtllibs(self, value) -> None: ... diff --git a/pywavefront/exceptions.pyi b/pywavefront/exceptions.pyi new file mode 100644 index 0000000..63480ed --- /dev/null +++ b/pywavefront/exceptions.pyi @@ -0,0 +1,2 @@ +class PywavefrontException(Exception): + """Generic exception for this package to separate from common ones""" diff --git a/pywavefront/material.pyi b/pywavefront/material.pyi new file mode 100644 index 0000000..c70b716 --- /dev/null +++ b/pywavefront/material.pyi @@ -0,0 +1,102 @@ +from _typeshed import Incomplete +from pathlib import Path as Path +from pywavefront.parser import Parser +from pywavefront.texture import Texture + +logger: Incomplete + +class Material: + texture_cls = Texture + name: Incomplete + diffuse: Incomplete + ambient: Incomplete + specular: Incomplete + emissive: Incomplete + transparency: float + shininess: float + optical_density: float + illumination_model: int + texture: Incomplete + texture_ambient: Incomplete + texture_specular_color: Incomplete + texture_specular_highlight: Incomplete + texture_alpha: Incomplete + texture_bump: Incomplete + is_default: Incomplete + vertex_format: str + vertices: Incomplete + gl_floats: Incomplete + def __init__(self, name, is_default: bool = False, has_faces: bool = False) -> None: + """ + Create a new material + :param name: Name of the material + :param is_default: Is this an auto created default material? + """ + @property + def has_normals(self): ... + @property + def has_uvs(self): ... + @property + def has_colors(self): ... + @property + def vertex_size(self): + """How many float each vertex contains in the interleaved data""" + def pad_light(self, values): + """Accept an array of up to 4 values, and return an array of 4 values. + If the input array is less than length 4, pad it with zeroes until it + is length 4. Also ensure each value is a float""" + def set_alpha(self, alpha) -> None: + """Set alpha/last value on all four lighting attributes.""" + def set_diffuse(self, values: Incomplete | None = None) -> None: ... + def set_ambient(self, values: Incomplete | None = None) -> None: ... + def set_specular(self, values: Incomplete | None = None) -> None: ... + def set_emissive(self, values: Incomplete | None = None) -> None: ... + def set_texture(self, name, search_path) -> None: ... + def set_texture_ambient(self, name, search_path) -> None: ... + def set_texture_specular_color(self, name, search_path) -> None: ... + def set_texture_specular_highlight(self, name, search_path) -> None: ... + def set_texture_alpha(self, name, search_path) -> None: ... + def set_texture_bump(self, name, search_path) -> None: ... + def unset_texture(self) -> None: ... + +class MaterialParser(Parser): + """Object to parse lines of a materials definition file.""" + materials: Incomplete + this_material: Incomplete + collect_faces: Incomplete + def __init__(self, file_name, strict: bool = False, encoding: str = 'utf-8', parse: bool = True, collect_faces: bool = False) -> None: + """ + Create a new material parser + :param file_name: file name and path of obj file to read + :param strict: Enable strict mode + :param encoding: Encoding to read the text files + :param parse: Should parse be called immediately or manually called later? + """ + def parse_newmtl(self) -> None: ... + def parse_Kd(self) -> None: ... + def parse_Ka(self) -> None: ... + def parse_Ks(self) -> None: ... + def parse_Ke(self) -> None: ... + def parse_Ns(self) -> None: ... + def parse_d(self) -> None: + """Transparency""" + def parse_Tr(self) -> None: + """Transparency (alternative)""" + def parse_map_Kd(self) -> None: + """Diffuse map""" + def parse_map_Ka(self) -> None: + """Ambient map""" + def parse_map_Ks(self) -> None: + """Specular color map""" + def parse_map_Ns(self) -> None: + """Specular color map""" + def parse_map_d(self) -> None: + """Alpha map""" + def parse_bump(self) -> None: + """Bump map (from the spec)""" + def parse_map_bump(self) -> None: + """Bump map (variant)""" + def parse_map_Bump(self) -> None: + """Bump map (variant)""" + def parse_Ni(self) -> None: ... + def parse_illum(self) -> None: ... diff --git a/pywavefront/mesh.pyi b/pywavefront/mesh.pyi new file mode 100644 index 0000000..dadfcdd --- /dev/null +++ b/pywavefront/mesh.pyi @@ -0,0 +1,14 @@ +from _typeshed import Incomplete + +class Mesh: + """This is a basic mesh for drawing using OpenGL. Interestingly, it does + not contain its own vertices. These are instead drawn via materials.""" + name: Incomplete + materials: Incomplete + has_faces: Incomplete + faces: Incomplete + def __init__(self, name: Incomplete | None = None, has_faces: bool = False) -> None: ... + def has_material(self, new_material): + """Determine whether we already have a material of this name.""" + def add_material(self, material) -> None: + """Add a material to the mesh, IF it's not already present.""" diff --git a/pywavefront/obj.pyi b/pywavefront/obj.pyi new file mode 100644 index 0000000..5000e45 --- /dev/null +++ b/pywavefront/obj.pyi @@ -0,0 +1,84 @@ +from _typeshed import Incomplete +from collections.abc import Generator +from pywavefront.cache import CacheLoader, CacheWriter, Meta as Meta +from pywavefront.material import MaterialParser +from pywavefront.parser import Parser + +logger: Incomplete + +class ObjParser(Parser): + """This parser parses lines from .obj files.""" + material_parser_cls = MaterialParser + cache_loader_cls = CacheLoader + cache_writer_cls = CacheWriter + wavefront: Incomplete + mesh: Incomplete + material: Incomplete + create_materials: Incomplete + collect_faces: Incomplete + cache: Incomplete + cache_loaded: Incomplete + normals: Incomplete + tex_coords: Incomplete + def __init__(self, wavefront, file_name, strict: bool = False, encoding: str = 'utf-8', create_materials: bool = False, collect_faces: bool = False, parse: bool = True, cache: bool = False) -> None: + """ + Create a new obj parser + :param wavefront: The wavefront object + :param file_name: file name and path of obj file to read + :param strict: Enable strict mode + :param encoding: Encoding to read the text files + :param create_materials: Create materials if they don't exist + :param cache: Cache the loaded obj files in binary format + :param parse: Should parse be called immediately or manually called later? + """ + def parse(self) -> None: + """Trigger cache load or call superclass parse()""" + def load_cache(self) -> None: + """Loads the file using cached data""" + def post_parse(self) -> None: + """Called after parsing is done""" + def parse_v(self) -> None: ... + line: Incomplete + values: Incomplete + def consume_vertices(self) -> Generator[Incomplete]: + """ + Consumes all consecutive vertices. + NOTE: There is no guarantee this will consume all vertices since other + statements can also occur in the vertex list + """ + def parse_vn(self) -> None: ... + def consume_normals(self) -> Generator[Incomplete]: + """Consumes all consecutive texture coordinate lines""" + def parse_vt(self) -> None: ... + def consume_texture_coordinates(self) -> Generator[Incomplete]: + """Consume all consecutive texture coordinates""" + def parse_mtllib(self) -> None: ... + def parse_usemtl(self) -> None: ... + def parse_usemat(self) -> None: ... + def parse_o(self) -> None: ... + def parse_f(self) -> None: ... + def consume_faces(self, collected_faces: Incomplete | None = None) -> Generator[Incomplete, Incomplete]: + """ + Consume all consecutive faces + + If more than three vertices are specified, we triangulate by the following procedure: + + Let the face have n vertices in the order v_1 v_2 v_3 ... v_n, n >= 3. + We emit the first face as usual: (v_1, v_2, v_3). For each remaining vertex v_j, + j > 3, we emit (v_j, v_1, v_{j - 1}), e.g. (v_4, v_1, v_3), (v_5, v_1, v_4). + + In a perfect world we could consume all vertices straight forward and draw using + GL_TRIANGLE_FAN (which exactly matches the procedure above). + This is however rarely the case. + + * If the face is co-planar but concave, then you need to triangulate the face. + * If the face is not-coplanar, you are screwed, because OBJ doesn't preserve enough information + to know what tessellation was intended. + + We always triangulate to make it simple. + + :param collected_faces: A list into which all (possibly triangulated) faces will be written in the form + of triples of the corresponding absolute vertex IDs. These IDs index the list + self.wavefront.vertices. + Specify None to prevent consuming faces (and thus saving memory usage). + """ diff --git a/pywavefront/parser.pyi b/pywavefront/parser.pyi new file mode 100644 index 0000000..12f766d --- /dev/null +++ b/pywavefront/parser.pyi @@ -0,0 +1,47 @@ +from _typeshed import Incomplete +from collections.abc import Generator + +logger: Incomplete + +def auto_consume(func): + """Decorator for auto consuming lines when leaving the function""" + +class Parser: + """This defines a generalized parse dispatcher; all parse functions + reside in subclasses.""" + auto_post_parse: bool + file_name: Incomplete + strict: Incomplete + encoding: Incomplete + dir: Incomplete + dispatcher: Incomplete + lines: Incomplete + line: Incomplete + values: Incomplete + def __init__(self, file_name, strict: bool = False, encoding: str = 'utf-8') -> None: + """ + Initializer for the base parser + :param file_name: Name and path of the file to read + :param strict: Enable or disable strict mode + """ + def create_line_generator(self) -> Generator[Incomplete]: + """ + Creates a generator function yielding lines in the file + Should only yield non-empty lines + """ + def next_line(self) -> None: + """Read the next line from the line generator and split it""" + def consume_line(self) -> None: + """ + Tell the parser we are done with this line. + This is simply by setting None values. + """ + def parse(self) -> None: + """ + Parse all the lines in the obj file + Determines what type of line we are and dispatch appropriately. + """ + def post_parse(self) -> None: + """Override to trigger operations after parsing is complete""" + def parse_fallback(self) -> None: + """Fallback method when parser doesn't know the statement""" diff --git a/pywavefront/texture.pyi b/pywavefront/texture.pyi new file mode 100644 index 0000000..e9c5d97 --- /dev/null +++ b/pywavefront/texture.pyi @@ -0,0 +1,112 @@ +from _typeshed import Incomplete + +class TextureOptions: + name: str + blendu: str + blendv: str + bm: float + boost: float + cc: str + clamp: str + imfchan: str + mm: Incomplete + o: Incomplete + s: Incomplete + t: Incomplete + texres: Incomplete + def __init__(self) -> None: + """Set up options with sane defaults""" + +class TextureOptionsParser: + def __init__(self, line) -> None: ... + def parse(self): ... + def parse_blendu(self) -> None: + """The -blendu option turns texture blending in the horizontal direction + (u direction) on or off. The default is on. + """ + def parse_blendv(self) -> None: + """The -blendv option turns texture blending in the vertical direction (v + direction) on or off. The default is on. + """ + def parse_bm(self) -> None: + """The -bm option specifies a bump multiplier""" + def parse_boost(self) -> None: + """The -boost option increases the sharpness, or clarity, of mip-mapped + texture files + """ + def parse_cc(self) -> None: + """The -cc option turns on color correction for the texture""" + def parse_clamp(self) -> None: + """The -clamp option turns clamping on or off.""" + def parse_imfchan(self) -> None: + """The -imfchan option specifies the channel used to create a scalar or + bump texture. + """ + def parse_mm(self) -> None: + """The -mm option modifies the range over which scalar or color texture + values may vary + """ + def parse_o(self) -> None: + """The -o option offsets the position of the texture map on the surface by + shifting the position of the map origin. + """ + def parse_s(self) -> None: + """The -s option scales the size of the texture pattern on the textured + surface by expanding or shrinking the pattern + """ + def parse_t(self) -> None: + """The -t option turns on turbulence for textures.""" + def parse_texres(self) -> None: + """The -texres option specifies the resolution of texture created when an + image is used. + """ + +class Texture: + image: Incomplete + def __init__(self, name, search_path) -> None: + """Create a texture. + + Args: + name (str): The texture name possibly with path and options as it appear in the material + search_path (str): Absolute or relative path the texture might be located. + """ + @property + def name(self): + """str: The texture path as it appears in the material""" + @name.setter + def name(self, value) -> None: ... + @property + def options(self) -> TextureOptions: + """TextureOptions: Options for this texture""" + def find(self, path: Incomplete | None = None): + """Find the texture in the configured search path + By default a search will be done in the same directory as + the obj file including all subdirectories if ``path`` does not exist. + + Args: + path: Override the search path + Raises: + FileNotFoundError if not found + """ + @property + def file_name(self): + """str: Obtains the file name of the texture. + Sometimes materials contains a relative or absolute path + to textures, something that often doesn't reflect the + textures real location. + """ + @property + def path(self): + """str: search_path + name""" + @path.setter + def path(self, value) -> None: ... + @property + def image_name(self): + """Wrap the old property name to not break compatibility. + The value will always be the texture path as it appears in the material. + """ + @image_name.setter + def image_name(self, value) -> None: + """Wrap the old property name to not break compatibility""" + def exists(self): + """bool: Does the texture exist""" diff --git a/pywavefront/visualization.pyi b/pywavefront/visualization.pyi new file mode 100644 index 0000000..9729595 --- /dev/null +++ b/pywavefront/visualization.pyi @@ -0,0 +1,22 @@ +from pyglet.gl import * +from _typeshed import Incomplete +from pywavefront.mesh import Mesh as Mesh + +def same(v): ... + +VERTEX_FORMATS: Incomplete + +def draw(instance, lighting_enabled: bool = True, textures_enabled: bool = True) -> None: + """Generic draw function""" +def draw_materials(materials, lighting_enabled: bool = True, textures_enabled: bool = True) -> None: + """Draw a dict of meshes""" +def draw_material(material, face=..., lighting_enabled: bool = True, textures_enabled: bool = True) -> None: + """Draw a single material""" +def gl_light(lighting): + """Return a GLfloat with length 4, containing the 4 lighting values.""" +def bind_texture(texture) -> None: + """Draw a single texture""" +def load_image(name): + """Load an image""" +def verify_dimensions(image) -> None: ... +def verify(image, dimension) -> None: ... diff --git a/pywavefront/wavefront.pyi b/pywavefront/wavefront.pyi new file mode 100644 index 0000000..87d1e12 --- /dev/null +++ b/pywavefront/wavefront.pyi @@ -0,0 +1,26 @@ +from _typeshed import Incomplete +from pywavefront import ObjParser + +logger: Incomplete + +class Wavefront: + parser_cls = ObjParser + file_name: Incomplete + mtllibs: Incomplete + materials: Incomplete + meshes: Incomplete + vertices: Incomplete + mesh_list: Incomplete + parser: Incomplete + def __init__(self, file_name, strict: bool = False, encoding: str = 'utf-8', create_materials: bool = False, collect_faces: bool = False, parse: bool = True, cache: bool = False) -> None: + """ + Create a Wavefront instance + :param file_name: file name and path of obj file to read + :param strict: Enable strict mode + :param encoding: What text encoding the parser should use + :param create_materials: Create materials if they don't exist + :param parse: Should parse be called immediately or manually called later? + """ + def parse(self) -> None: + """Manually call the parser. This is used when parse=False""" + def add_mesh(self, the_mesh) -> None: ...