diff --git a/src/3d/Viewport3D.cpp b/src/3d/Viewport3D.cpp index 6ab3f12..c275ec6 100644 --- a/src/3d/Viewport3D.cpp +++ b/src/3d/Viewport3D.cpp @@ -7,6 +7,8 @@ #include "PyColor.h" #include "PyPositionHelper.h" #include "McRFPy_Doc.h" +#include "PythonObjectCache.h" +#include "McRFPy_API.h" #include #include @@ -925,15 +927,32 @@ int Viewport3D::init(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { self->data->name = name; } + // Register in Python object cache for scene explorer repr + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); // Cache owns the reference now + } + } + + // Check if this is a Python subclass (for callback method support) + PyObject* viewport3d_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Viewport3D"); + if (viewport3d_type) { + self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != viewport3d_type; + Py_DECREF(viewport3d_type); + } + return 0; } -#undef PyObjectType - } // namespace mcrf -// Methods array (outside namespace) +// Methods array - outside namespace but PyObjectType still in scope via typedef +typedef PyViewport3DObject PyObjectType; + PyMethodDef Viewport3D_methods[] = { - // Add UIDRAWABLE_METHODS when ready + UIDRAWABLE_METHODS, {NULL} // Sentinel }; diff --git a/src/ImGuiSceneExplorer.cpp b/src/ImGuiSceneExplorer.cpp index e255bbf..b11721f 100644 --- a/src/ImGuiSceneExplorer.cpp +++ b/src/ImGuiSceneExplorer.cpp @@ -281,11 +281,15 @@ const char* ImGuiSceneExplorer::getTypeName(UIDrawable* drawable) { if (!drawable) return "null"; switch (drawable->derived_type()) { - case PyObjectsEnum::UIFRAME: return "Frame"; - case PyObjectsEnum::UICAPTION: return "Caption"; - case PyObjectsEnum::UISPRITE: return "Sprite"; - case PyObjectsEnum::UIGRID: return "Grid"; - default: return "Unknown"; + case PyObjectsEnum::UIFRAME: return "Frame"; + case PyObjectsEnum::UICAPTION: return "Caption"; + case PyObjectsEnum::UISPRITE: return "Sprite"; + case PyObjectsEnum::UIGRID: return "Grid"; + case PyObjectsEnum::UILINE: return "Line"; + case PyObjectsEnum::UICIRCLE: return "Circle"; + case PyObjectsEnum::UIARC: return "Arc"; + case PyObjectsEnum::UIVIEWPORT3D: return "Viewport3D"; + default: return "Unknown"; } } diff --git a/tests/demo/viewport3d_demo.py b/tests/demo/viewport3d_demo.py new file mode 100644 index 0000000..745aa36 --- /dev/null +++ b/tests/demo/viewport3d_demo.py @@ -0,0 +1,167 @@ +# viewport3d_demo.py - Visual demo of Viewport3D integration +# Shows the 3D viewport as a UIDrawable alongside 2D elements + +import mcrfpy +import sys +import math + +# Create demo scene +scene = mcrfpy.Scene("viewport3d_demo") + +# Dark background frame +bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(20, 20, 30)) +scene.children.append(bg) + +# Title +title = mcrfpy.Caption(text="Viewport3D Demo - PS1-Style 3D Rendering", pos=(20, 10)) +title.fill_color = mcrfpy.Color(255, 255, 255) +scene.children.append(title) + +# Create the 3D viewport - this is the star of the show! +viewport = mcrfpy.Viewport3D( + pos=(50, 60), + size=(600, 450), + render_resolution=(320, 240), # PS1 resolution for that retro look + fov=60.0, + camera_pos=(5.0, 3.0, 5.0), + camera_target=(0.0, 0.0, 0.0), + bg_color=mcrfpy.Color(25, 25, 50) # Dark blue background +) +scene.children.append(viewport) + +# Info panel on the right +info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450), + fill_color=mcrfpy.Color(30, 30, 40), + outline_color=mcrfpy.Color(80, 80, 100), + outline=2.0) +scene.children.append(info_panel) + +# Panel title +panel_title = mcrfpy.Caption(text="Viewport Properties", pos=(690, 70)) +panel_title.fill_color = mcrfpy.Color(200, 200, 255) +scene.children.append(panel_title) + +# Property labels +props = [ + ("Position:", f"({viewport.x}, {viewport.y})"), + ("Size:", f"{viewport.w}x{viewport.h}"), + ("Render Res:", f"{viewport.render_resolution[0]}x{viewport.render_resolution[1]}"), + ("FOV:", f"{viewport.fov} degrees"), + ("Camera Pos:", f"({viewport.camera_pos[0]:.1f}, {viewport.camera_pos[1]:.1f}, {viewport.camera_pos[2]:.1f})"), + ("Camera Target:", f"({viewport.camera_target[0]:.1f}, {viewport.camera_target[1]:.1f}, {viewport.camera_target[2]:.1f})"), + ("", ""), + ("PS1 Effects:", ""), + (" Vertex Snap:", "ON" if viewport.enable_vertex_snap else "OFF"), + (" Affine Map:", "ON" if viewport.enable_affine else "OFF"), + (" Dithering:", "ON" if viewport.enable_dither else "OFF"), + (" Fog:", "ON" if viewport.enable_fog else "OFF"), + (" Fog Range:", f"{viewport.fog_near} - {viewport.fog_far}"), +] + +y_offset = 100 +for label, value in props: + if label: + cap = mcrfpy.Caption(text=f"{label} {value}", pos=(690, y_offset)) + cap.fill_color = mcrfpy.Color(180, 180, 200) + scene.children.append(cap) + y_offset += 22 + +# Instructions at bottom +instructions = mcrfpy.Caption( + text="[1-4] Toggle PS1 effects | [WASD] Move camera | [Q/E] Camera height | [ESC] Quit", + pos=(20, 530) +) +instructions.fill_color = mcrfpy.Color(150, 150, 150) +scene.children.append(instructions) + +# Status line +status = mcrfpy.Caption(text="Status: Viewport3D rendering PS1-style 3D cube", pos=(20, 555)) +status.fill_color = mcrfpy.Color(100, 200, 100) +scene.children.append(status) + +# Animation state +animation_time = [0.0] +camera_orbit = [True] + +# Camera orbit animation +def update_camera(timer, runtime): + animation_time[0] += runtime / 1000.0 + + if camera_orbit[0]: + # Orbit camera around origin + angle = animation_time[0] * 0.5 # Slow rotation + radius = 7.0 + height = 4.0 + math.sin(animation_time[0] * 0.3) * 2.0 + + x = math.cos(angle) * radius + z = math.sin(angle) * radius + + viewport.camera_pos = (x, height, z) + +# Key handler +def on_key(key, state): + if state != mcrfpy.InputState.PRESSED: + return + + key_name = str(key) + + # Toggle PS1 effects with number keys + if key == mcrfpy.Key.NUM_1: + viewport.enable_vertex_snap = not viewport.enable_vertex_snap + status.text = f"Vertex Snap: {'ON' if viewport.enable_vertex_snap else 'OFF'}" + elif key == mcrfpy.Key.NUM_2: + viewport.enable_affine = not viewport.enable_affine + status.text = f"Affine Mapping: {'ON' if viewport.enable_affine else 'OFF'}" + elif key == mcrfpy.Key.NUM_3: + viewport.enable_dither = not viewport.enable_dither + status.text = f"Dithering: {'ON' if viewport.enable_dither else 'OFF'}" + elif key == mcrfpy.Key.NUM_4: + viewport.enable_fog = not viewport.enable_fog + status.text = f"Fog: {'ON' if viewport.enable_fog else 'OFF'}" + + # Camera controls + elif key == mcrfpy.Key.SPACE: + camera_orbit[0] = not camera_orbit[0] + status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF (manual control)'}" + + elif key == mcrfpy.Key.ESCAPE: + mcrfpy.exit() + + # Manual camera movement (when orbit is off) + if not camera_orbit[0]: + pos = list(viewport.camera_pos) + speed = 0.5 + + if key == mcrfpy.Key.W: + pos[2] -= speed + elif key == mcrfpy.Key.S: + pos[2] += speed + elif key == mcrfpy.Key.A: + pos[0] -= speed + elif key == mcrfpy.Key.D: + pos[0] += speed + elif key == mcrfpy.Key.Q: + pos[1] -= speed + elif key == mcrfpy.Key.E: + pos[1] += speed + + viewport.camera_pos = tuple(pos) + status.text = f"Camera: ({pos[0]:.1f}, {pos[1]:.1f}, {pos[2]:.1f})" + +# Set up scene +scene.on_key = on_key + +# Create timer for camera animation +timer = mcrfpy.Timer("camera_update", update_camera, 16) # ~60fps + +# Activate scene +mcrfpy.current_scene = scene + +print("Viewport3D Demo loaded!") +print("3D rendering enabled - spinning colored cube should be visible.") +print() +print("Controls:") +print(" [1-4] Toggle PS1 effects") +print(" [Space] Toggle camera orbit") +print(" [WASD/QE] Manual camera control (when orbit off)") +print(" [ESC] Quit")