diff --git a/examples/basics/01_load_comp_basic.py b/examples/basics/01_load_comp_basic.py index 83bf1dc..7d3c94c 100644 --- a/examples/basics/01_load_comp_basic.py +++ b/examples/basics/01_load_comp_basic.py @@ -1,50 +1,139 @@ +""" +Improved TouchPy example: Run a .tox component for a fixed number of frames (headless mode) + +This script demonstrates best practices for: +- Error handling with TouchEngine +- Resource cleanup (always unload!) +- Logging instead of raw print +- Configurable parameters +- Context manager pattern + +Requirements: +- Python 3.9+ +- touchpy installed (pip install touchpy) +- TouchDesigner / TouchPlayer 2023+ with paid license +- Windows + Vulkan-capable GPU (NVIDIA for TOPs) + +Usage: + python this_script.py +""" + +import logging +from pathlib import Path import touchpy as tp -# create a class that inherits from tp.Comp -# inheriting from tp.Comp is not required but it is the recommended way to interface -# with a component - -class MyComp (tp.Comp): - def __init__(self): - - # call the parent class constructor to initialize the component - super().__init__() - - # create a frame counter - self.frame = 0 - - # set the on_frame_callback to the on_frame method - self.set_on_frame_callback(self.on_frame, {}) - - # define the on_frame method that will be called on every frame - # the info argument is user data that can be passed to the callback - # in this case it is an empty dictionary - def on_frame(self, info): - - # stop running the component after 1200 frames (20 seconds at 60 fps) - if self.frame == 1200: - self.stop() - return - - # start_next_frame() is called to advance the component to the next frame - self.start_next_frame() - self.frame += 1 - -# create an instance of the MyCompBasic class -comp = MyComp() - -# load the tox file into the component -comp.load('TopChopDatIO.tox') - -# start the component, this will block until self.stop() is called when not running -# the component in async mode -comp.start() - -# unload the component to cleanly free up resources -comp.unload() - - - - - - +# Setup logging (bisa diubah ke file: filename='touchpy_run.log') +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class ControlledComp(tp.Comp): + """ + Custom component wrapper with frame limiting and safe cleanup. + """ + + def __init__(self, tox_path: str | Path, max_frames: int = 1200, fps: float = 60.0): + super().__init__() + + self.tox_path = Path(tox_path).resolve() # Normalize path + self.max_frames = max_frames + self.frame_counter = 0 + self.target_fps = fps + + # Optional: set cook time / FPS target if needed + # self.cook_time = 1.0 / self.target_fps # in seconds, but TouchPy may ignore + + # Set callback + self.set_on_frame_callback(self._on_frame, user_data={}) + + logger.info(f"Initialized ControlledComp for {self.tox_path}") + + def load_tox(self): + """Load the .tox file with error checking""" + if not self.tox_path.exists(): + raise FileNotFoundError(f"TOX file not found: {self.tox_path}") + + try: + self.load(str(self.tox_path)) # TouchPy expects str path + logger.info(f"Successfully loaded TOX: {self.tox_path.name}") + except Exception as e: + logger.error(f"Failed to load TOX: {e}") + raise + + def _on_frame(self, info): + """Called every frame by TouchPy engine""" + self.frame_counter += 1 + + # Example: Access operators inside the .tox and do something useful + # top = self.op('my_top') # if there's a TOP named 'my_top' + # if top: + # top.par.preview = 1 # example: toggle preview + + logger.debug(f"Frame {self.frame_counter}/{self.max_frames}") + + if self.frame_counter >= self.max_frames: + logger.info(f"Reached max frames ({self.max_frames}). Stopping.") + self.stop() + return + + # Advance to next frame + self.start_next_frame() + + def run(self): + """Start the component and block until stopped""" + try: + self.load_tox() + logger.info(f"Starting component for up to {self.max_frames} frames " + f"({self.max_frames / self.target_fps:.1f} seconds)") + self.start() # Blocks until stop() called + except Exception as e: + logger.error(f"Error during execution: {e}") + raise + finally: + self.unload() + logger.info("Component unloaded (cleanup complete)") + + +# Context manager version (recommended for safety) +class SafeCompRunner: + """Context manager to ensure unload even on exceptions""" + + def __init__(self, tox_path: str | Path, max_frames: int = 1200): + self.comp = ControlledComp(tox_path, max_frames=max_frames) + + def __enter__(self): + return self.comp + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.comp: + self.comp.unload() + logger.info("Context exit: component unloaded") + + +# -------------------- +# Main execution +# -------------------- +if __name__ == "__main__": + TOX_FILE = "TopChopDatIO.tox" # Ganti sesuai path kamu + + try: + with SafeCompRunner(TOX_FILE, max_frames=1200) as runner: + runner.run() + + # Alternatif non-blocking (async mode) - uncomment jika perlu + # comp = ControlledComp(TOX_FILE) + # comp.load_tox() + # comp.start(block=False) # Non-blocking + # # Lakukan hal lain... + # while comp.is_running: + # time.sleep(0.1) + # comp.stop() + # comp.unload() + + except FileNotFoundError as e: + logger.error(f"TOX file missing: {e}") + except Exception as e: + logger.exception("Unexpected error during TouchPy run")