diff --git a/examples/basic_usage.py b/examples/basic_usage.py index 8164b5f..5e29d8f 100755 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -36,6 +36,14 @@ def main(): print(f" - Materials: {len(scene.materials)}") print() + # Coordinate axes (right, up, front) + axes = scene.axes + print("Coordinate axes:") + print(f" - right: {axes.right.name}") + print(f" - up: {axes.up.name}") + print(f" - front: {axes.front.name}") + print() + # Root node info root = scene.root_node if root: diff --git a/tests/test_axes.py b/tests/test_axes.py new file mode 100644 index 0000000..65376d0 --- /dev/null +++ b/tests/test_axes.py @@ -0,0 +1,91 @@ +""" +Tests for CoordinateAxis, CoordinateAxes, and Scene.axes +""" + +import os + +import pytest + +import ufbx + + +def test_coordinate_axis_enum(): + """CoordinateAxis has all 7 values matching ufbx C API.""" + assert hasattr(ufbx.CoordinateAxis, "COORDINATE_AXIS_POSITIVE_X") + assert hasattr(ufbx.CoordinateAxis, "COORDINATE_AXIS_NEGATIVE_X") + assert hasattr(ufbx.CoordinateAxis, "COORDINATE_AXIS_POSITIVE_Y") + assert hasattr(ufbx.CoordinateAxis, "COORDINATE_AXIS_NEGATIVE_Y") + assert hasattr(ufbx.CoordinateAxis, "COORDINATE_AXIS_POSITIVE_Z") + assert hasattr(ufbx.CoordinateAxis, "COORDINATE_AXIS_NEGATIVE_Z") + assert hasattr(ufbx.CoordinateAxis, "COORDINATE_AXIS_UNKNOWN") + + assert ufbx.CoordinateAxis.COORDINATE_AXIS_POSITIVE_X == 0 + assert ufbx.CoordinateAxis.COORDINATE_AXIS_NEGATIVE_X == 1 + assert ufbx.CoordinateAxis.COORDINATE_AXIS_POSITIVE_Y == 2 + assert ufbx.CoordinateAxis.COORDINATE_AXIS_NEGATIVE_Y == 3 + assert ufbx.CoordinateAxis.COORDINATE_AXIS_POSITIVE_Z == 4 + assert ufbx.CoordinateAxis.COORDINATE_AXIS_NEGATIVE_Z == 5 + assert ufbx.CoordinateAxis.COORDINATE_AXIS_UNKNOWN == 6 + + assert isinstance(ufbx.CoordinateAxis.COORDINATE_AXIS_POSITIVE_X, int) + + +def test_coordinate_axes(): + """CoordinateAxes construction, .right/.up/.front, clamping, __repr__.""" + from ufbx._ufbx import CoordinateAxes + + ax = CoordinateAxes(0, 2, 4) + assert ax.right == ufbx.CoordinateAxis.COORDINATE_AXIS_POSITIVE_X + assert ax.up == ufbx.CoordinateAxis.COORDINATE_AXIS_POSITIVE_Y + assert ax.front == ufbx.CoordinateAxis.COORDINATE_AXIS_POSITIVE_Z + + ax2 = CoordinateAxes(1, 3, 5) + assert ax2.right == ufbx.CoordinateAxis.COORDINATE_AXIS_NEGATIVE_X + assert ax2.up == ufbx.CoordinateAxis.COORDINATE_AXIS_NEGATIVE_Y + assert ax2.front == ufbx.CoordinateAxis.COORDINATE_AXIS_NEGATIVE_Z + + # Out-of-range -> UNKNOWN + ax3 = CoordinateAxes(99, -1, 7) + assert ax3.right == ufbx.CoordinateAxis.COORDINATE_AXIS_UNKNOWN + assert ax3.up == ufbx.CoordinateAxis.COORDINATE_AXIS_UNKNOWN + assert ax3.front == ufbx.CoordinateAxis.COORDINATE_AXIS_UNKNOWN + + assert "CoordinateAxes" in repr(ax) + assert "right=" in repr(ax) and "up=" in repr(ax) and "front=" in repr(ax) + + +def test_scene_has_axes_property(): + """Scene exposes axes property.""" + assert hasattr(ufbx.Scene, "axes") + + +@pytest.fixture +def fbx_path(): + p = os.path.join(os.path.dirname(__file__), "fixtures", "maya_cube.fbx") + return p if os.path.exists(p) else None + + +def test_scene_axes(fbx_path): + """Scene.axes returns CoordinateAxes with right/up/front as CoordinateAxis.""" + if fbx_path is None: + pytest.skip("maya_cube.fbx not found (see tests/fixtures/README.md)") + + with ufbx.load_file(fbx_path) as scene: + ax = scene.axes + assert ax is not None + assert hasattr(ax, "right") and hasattr(ax, "up") and hasattr(ax, "front") + assert isinstance(ax.right, ufbx.CoordinateAxis) + assert isinstance(ax.up, ufbx.CoordinateAxis) + assert isinstance(ax.front, ufbx.CoordinateAxis) + assert "CoordinateAxes" in repr(ax) + + +def test_scene_axes_after_close(fbx_path): + """Accessing axes after scene.close() raises RuntimeError.""" + if fbx_path is None: + pytest.skip("maya_cube.fbx not found (see tests/fixtures/README.md)") + + scene = ufbx.load_file(fbx_path) + scene.close() + with pytest.raises(RuntimeError, match="closed"): + _ = scene.axes diff --git a/ufbx/__init__.pyi b/ufbx/__init__.pyi index ffa1a8d..8943b5c 100644 --- a/ufbx/__init__.pyi +++ b/ufbx/__init__.pyi @@ -52,9 +52,22 @@ class MirrorAxis(IntEnum): MIRROR_AXIS_Z: int class CoordinateAxis(IntEnum): - COORDINATE_AXIS_X: int - COORDINATE_AXIS_Y: int - COORDINATE_AXIS_Z: int + COORDINATE_AXIS_POSITIVE_X: int + COORDINATE_AXIS_NEGATIVE_X: int + COORDINATE_AXIS_POSITIVE_Y: int + COORDINATE_AXIS_NEGATIVE_Y: int + COORDINATE_AXIS_POSITIVE_Z: int + COORDINATE_AXIS_NEGATIVE_Z: int + COORDINATE_AXIS_UNKNOWN: int + +class CoordinateAxes: + """Scene coordinate axes (right, up, front).""" + + right: CoordinateAxis + up: CoordinateAxis + front: CoordinateAxis + def __init__(self, right: int, up: int, front: int) -> None: ... + def __repr__(self) -> str: ... class SubdivisionDisplayMode(IntEnum): SUBDIVISION_DISPLAY_MODE_OFF: int @@ -203,6 +216,8 @@ class Scene: def materials(self) -> list[Material]: ... @property def root_node(self) -> Node | None: ... + @property + def axes(self) -> CoordinateAxes: ... def find_node(self, name: str) -> Node | None: ... def find_material(self, name: str) -> Material | None: ... @property diff --git a/ufbx/_ufbx.pyx b/ufbx/_ufbx.pyx index 58656bb..14c44ad 100644 --- a/ufbx/_ufbx.pyx +++ b/ufbx/_ufbx.pyx @@ -31,6 +31,9 @@ cdef extern from "ufbx_wrapper.h": size_t ufbx_wrapper_scene_get_num_meshes(const ufbx_scene *scene) size_t ufbx_wrapper_scene_get_num_materials(const ufbx_scene *scene) ufbx_node* ufbx_wrapper_scene_get_root_node(const ufbx_scene *scene) + int ufbx_wrapper_scene_get_axes_right(const ufbx_scene *scene) + int ufbx_wrapper_scene_get_axes_up(const ufbx_scene *scene) + int ufbx_wrapper_scene_get_axes_front(const ufbx_scene *scene) # Node access ufbx_node* ufbx_wrapper_scene_get_node(const ufbx_scene *scene, size_t index) @@ -130,9 +133,28 @@ class MirrorAxis(IntEnum): class CoordinateAxis(IntEnum): - COORDINATE_AXIS_X = 0 - COORDINATE_AXIS_Y = 1 - COORDINATE_AXIS_Z = 2 + COORDINATE_AXIS_POSITIVE_X = 0 + COORDINATE_AXIS_NEGATIVE_X = 1 + COORDINATE_AXIS_POSITIVE_Y = 2 + COORDINATE_AXIS_NEGATIVE_Y = 3 + COORDINATE_AXIS_POSITIVE_Z = 4 + COORDINATE_AXIS_NEGATIVE_Z = 5 + COORDINATE_AXIS_UNKNOWN = 6 + + +class CoordinateAxes: + """Scene coordinate axes (right, up, front). Maps X/Y/Z to world-space directions.""" + + __slots__ = ("right", "up", "front") + + def __init__(self, right: int, up: int, front: int): + unk = CoordinateAxis.COORDINATE_AXIS_UNKNOWN + self.right = CoordinateAxis(right) if 0 <= right <= 6 else unk + self.up = CoordinateAxis(up) if 0 <= up <= 6 else unk + self.front = CoordinateAxis(front) if 0 <= front <= 6 else unk + + def __repr__(self) -> str: + return f"CoordinateAxes(right={self.right!r}, up={self.up!r}, front={self.front!r})" class SubdivisionDisplayMode(IntEnum): @@ -497,6 +519,16 @@ cdef class Scene: return Node._create(self, node) return None + @property + def axes(self): + """Scene coordinate axes (right, up, front). Returns CoordinateAxes with CoordinateAxis members.""" + if self._closed: + raise RuntimeError("Scene is closed") + cdef int r = ufbx_wrapper_scene_get_axes_right(self._scene) + cdef int u = ufbx_wrapper_scene_get_axes_up(self._scene) + cdef int f = ufbx_wrapper_scene_get_axes_front(self._scene) + return CoordinateAxes(r, u, f) + cdef Node _get_node(self, size_t index): """Internal: get node by index""" cdef ufbx_node* node = ufbx_wrapper_scene_get_node(self._scene, index) diff --git a/ufbx/src/ufbx_wrapper.c b/ufbx/src/ufbx_wrapper.c index 8cdbe92..b934b82 100644 --- a/ufbx/src/ufbx_wrapper.c +++ b/ufbx/src/ufbx_wrapper.c @@ -44,6 +44,18 @@ ufbx_node* ufbx_wrapper_scene_get_root_node(const ufbx_scene *scene) { return scene ? scene->root_node : NULL; } +int ufbx_wrapper_scene_get_axes_right(const ufbx_scene *scene) { + return scene ? (int)scene->settings.axes.right : (int)UFBX_COORDINATE_AXIS_UNKNOWN; +} + +int ufbx_wrapper_scene_get_axes_up(const ufbx_scene *scene) { + return scene ? (int)scene->settings.axes.up : (int)UFBX_COORDINATE_AXIS_UNKNOWN; +} + +int ufbx_wrapper_scene_get_axes_front(const ufbx_scene *scene) { + return scene ? (int)scene->settings.axes.front : (int)UFBX_COORDINATE_AXIS_UNKNOWN; +} + // Node access ufbx_node* ufbx_wrapper_scene_get_node(const ufbx_scene *scene, size_t index) { if (!scene || index >= scene->nodes.count) return NULL; diff --git a/ufbx/src/ufbx_wrapper.h b/ufbx/src/ufbx_wrapper.h index 2b9dc79..eede020 100644 --- a/ufbx/src/ufbx_wrapper.h +++ b/ufbx/src/ufbx_wrapper.h @@ -26,6 +26,11 @@ size_t ufbx_wrapper_scene_get_num_meshes(const ufbx_scene *scene); size_t ufbx_wrapper_scene_get_num_materials(const ufbx_scene *scene); ufbx_node* ufbx_wrapper_scene_get_root_node(const ufbx_scene *scene); +// Scene settings.axes (CoordinateAxis: 0-6) +int ufbx_wrapper_scene_get_axes_right(const ufbx_scene *scene); +int ufbx_wrapper_scene_get_axes_up(const ufbx_scene *scene); +int ufbx_wrapper_scene_get_axes_front(const ufbx_scene *scene); + // Node access ufbx_node* ufbx_wrapper_scene_get_node(const ufbx_scene *scene, size_t index); const char* ufbx_wrapper_node_get_name(const ufbx_node *node);