Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/basic_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
91 changes: 91 additions & 0 deletions tests/test_axes.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 18 additions & 3 deletions ufbx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +63 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Export CoordinateAxes in the runtime module

This stub declares a top-level CoordinateAxes, but ufbx/__init__.py never imports it from _ufbx, so ufbx.CoordinateAxes will raise AttributeError at runtime even though type checkers accept it. That mismatch is triggered whenever users follow the stub (e.g., for isinstance(x, ufbx.CoordinateAxes) or type annotations) and will fail in regular execution; consider importing and adding it to __all__ to match the stub and Scene.axes return type.

Useful? React with 👍 / 👎.

front: CoordinateAxis
def __init__(self, right: int, up: int, front: int) -> None: ...
def __repr__(self) -> str: ...

class SubdivisionDisplayMode(IntEnum):
SUBDIVISION_DISPLAY_MODE_OFF: int
Expand Down Expand Up @@ -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
Expand Down
38 changes: 35 additions & 3 deletions ufbx/_ufbx.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions ufbx/src/ufbx_wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions ufbx/src/ufbx_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down