PyHelios 0.1.11
Loading...
Searching...
No Matches
WeberPennTree.py
Go to the documentation of this file.
1# import ctypes
2# from pyhelios import Context
3# from wrappers import UWeberPennTreeWrapper as wpt_wrapper
4# from enum import Enum
5# from typing import List
6# from wrappers import Vec3
7
8import ctypes
9import os
10from contextlib import contextmanager
11from enum import Enum
12from pathlib import Path
13from typing import List
14
15from .wrappers import UWeberPennTreeWrapper as wpt_wrapper
16from .wrappers.DataTypes import vec3
17from .plugins.registry import get_plugin_registry, graceful_plugin_fallback
18from .validation.plugins import validate_wpt_parameters
19from .validation.datatypes import validate_vec3
20from .validation.core import validate_positive_value
21from .assets import get_asset_manager
22from .validation.plugin_decorators import (
23 validate_tree_uuid_params, validate_recursion_params, validate_trunk_segment_params,
24 validate_branch_segment_params, validate_leaf_subdivisions_params, validate_xml_file_params
25)
26
27
29 """
30 Check if WeberPennTree plugin is available for use.
31
32 Returns:
33 bool: True if WeberPennTree can be used, False otherwise
34 """
35 try:
36 # Check plugin registry
37 plugin_registry = get_plugin_registry()
38 if not plugin_registry.is_plugin_available('weberpenntree'):
39 return False
40
41 # Check if wrapper functions are available
42 if not wpt_wrapper._WPT_FUNCTIONS_AVAILABLE:
43 return False
44
45 return True
46 except Exception:
47 return False
48
49from .Context import Context
50
51@contextmanager
53 """
54 Context manager that temporarily changes working directory to where WeberPennTree assets are located.
55
56 WeberPennTree C++ code uses hardcoded relative paths like "plugins/weberpenntree/xml/WeberPennTreeLibrary.xml"
57 expecting assets relative to working directory. This manager temporarily changes to the build directory
58 where assets are actually located.
59
60 Raises:
61 RuntimeError: If build directory or WeberPennTree assets are not found, indicating a build system error.
62 """
63 # Find the build directory containing WeberPennTree assets
64 # Try asset manager first (works for both development and wheel installations)
65 asset_manager = get_asset_manager()
66 working_dir = asset_manager._get_helios_build_path()
67
68 if working_dir and working_dir.exists():
69 weberpenntree_assets = working_dir / 'plugins' / 'weberpenntree'
70 else:
71 # For wheel installations, check packaged assets
72 current_dir = Path(__file__).parent
73 packaged_build = current_dir / 'assets' / 'build'
74
75 if packaged_build.exists():
76 working_dir = packaged_build
77 weberpenntree_assets = working_dir / 'plugins' / 'weberpenntree'
78 else:
79 # Fallback to development paths
80 repo_root = current_dir.parent
81 build_lib_dir = repo_root / 'pyhelios_build' / 'build' / 'lib'
82 working_dir = build_lib_dir.parent
83 weberpenntree_assets = working_dir / 'plugins' / 'weberpenntree'
84
85 if not build_lib_dir.exists():
86 raise RuntimeError(
87 f"PyHelios build directory not found: {build_lib_dir}. "
88 f"WeberPennTree requires native libraries to be built. "
89 f"Run: python build_scripts/build_helios.py --plugins weberpenntree"
90 )
91
92 if not weberpenntree_assets.exists():
93 raise RuntimeError(
94 f"WeberPennTree assets not found: {weberpenntree_assets}. "
95 f"Build system failed to copy WeberPennTree assets. "
96 f"Run: python build_scripts/build_helios.py --clean --plugins weberpenntree"
97 )
98
99 xml_file = weberpenntree_assets / 'xml' / 'WeberPennTreeLibrary.xml'
100 if not xml_file.exists():
101 raise RuntimeError(
102 f"WeberPennTree XML library not found: {xml_file}. "
103 f"Critical WeberPennTree asset missing from build. "
104 f"Run: python build_scripts/build_helios.py --clean --plugins weberpenntree"
105 )
106
107 # Change to build directory where assets are located
108 original_cwd = Path.cwd()
109 try:
110 os.chdir(working_dir)
111 yield
112 finally:
113 os.chdir(original_cwd)
114
115class WPTType(Enum):
116 ALMOND = 'Almond'
117 APPLE = 'Apple'
118 AVOCADO = 'Avocado'
119 LEMON = 'Lemon'
120 OLIVE = 'Olive'
121 ORANGE = 'Orange'
122 PEACH = 'Peach'
123 PISTACHIO = 'Pistachio'
124 WALNUT = 'Walnut'
127 WPTType = WPTType # Make WPTType accessible as class attribute
129 def __init__(self, context:Context):
130 self.context = context
131 self._plugin_registry = get_plugin_registry()
133 # Check if weberpenntree functions are available at the wrapper level first
134 from .wrappers.UWeberPennTreeWrapper import _WPT_FUNCTIONS_AVAILABLE
135 if not _WPT_FUNCTIONS_AVAILABLE:
136 raise NotImplementedError("WeberPennTree functions not available in current Helios library.")
137
138 # Check if weberpenntree plugin is available in registry
139 if not self._plugin_registry.is_plugin_available('weberpenntree'):
140 print("Warning: WeberPennTree plugin not detected in current build")
141 print("Tree generation functionality may be limited or unavailable")
142
143 # Find build directory for asset loading using asset manager
144 asset_manager = get_asset_manager()
145 build_dir = asset_manager._get_helios_build_path()
146
147 if not build_dir or not build_dir.exists():
148 # In wheel installations, try packaged assets location
149 current_dir = Path(__file__).parent
150 packaged_build = current_dir / 'assets' / 'build'
151
152 if packaged_build.exists() and (packaged_build / 'plugins' / 'weberpenntree').exists():
153 build_dir = packaged_build
154 else:
155 # Fallback to development build directory
156 repo_root = current_dir.parent
157 build_lib_dir = repo_root / 'pyhelios_build' / 'build' / 'lib'
158 build_dir = build_lib_dir.parent
159
160 if not build_dir.exists():
161 raise RuntimeError(
162 f"PyHelios build directory not found: {build_dir}. "
163 f"WeberPennTree requires native libraries to be built. "
164 f"Run: python build_scripts/build_helios.py --plugins weberpenntree"
165 )
166
167 # Use the same build_dir for working directory as we use for C++ interface
168 # This ensures consistency between Python working directory and C++ asset paths
169 original_cwd = Path.cwd()
170 try:
171 os.chdir(build_dir)
172 self.wpt = wpt_wrapper.createWeberPennTreeWithBuildPluginRootDirectory(
173 context.getNativePtr(), str(build_dir)
175 finally:
176 os.chdir(original_cwd)
177
178 def __enter__(self):
179 return self
181 def __exit__(self, exc_type, exc_value, traceback):
182 if self.wpt is not None:
183 try:
184 wpt_wrapper.destroyWeberPennTree(self.wpt)
185 finally:
186 self.wpt = None # Prevent double deletion
187
188 def __del__(self):
189 """Destructor to ensure C++ resources freed even without 'with' statement."""
190 if hasattr(self, 'wpt') and self.wpt is not None:
191 try:
192 wpt_wrapper.destroyWeberPennTree(self.wpt)
193 self.wpt = None
194 except Exception as e:
195 import warnings
196 warnings.warn(f"Error in WeberPennTree.__del__: {e}")
197
198 def getNativePtr(self):
199 return self.wpt
201
202 def buildTree(self, wpt_type, origin:vec3=vec3(0, 0, 0), scale:float=1) -> int:
203 """
204 Build a tree using either a built-in tree type or custom species name.
205
206 Args:
207 wpt_type: Either WPTType enum for built-in types, or string for custom species loaded via loadXML()
208 origin: Tree origin position (default: vec3(0, 0, 0))
209 scale: Tree scale factor (default: 1.0)
210
211 Returns:
212 Tree ID for querying tree components
213 """
214 # Validate inputs
215 validate_vec3(origin, "origin", "buildTree")
216 validate_positive_value(scale, "scale", "buildTree")
217
218 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
219 raise RuntimeError(
220 f"WeberPennTree is not properly initialized. "
221 f"This may indicate that the weberpenntree plugin is not available. "
222 f"Check plugin status with context.print_plugin_status()"
223 )
224
225 # Convert wpt_type to string (handle both WPTType enum and string)
226 if isinstance(wpt_type, WPTType):
227 tree_name = wpt_type.value
228 elif isinstance(wpt_type, str):
229 tree_name = wpt_type
230 else:
231 raise TypeError(f"wpt_type must be WPTType enum or string, got {type(wpt_type).__name__}")
232
233 # Use working directory context manager during tree building to access assets
235 # Use scale-aware function if scale is not 1.0, otherwise use regular function
236 if scale != 1.0:
237 return wpt_wrapper.buildTreeWithScale(self.wpt, tree_name, origin.to_list(), scale)
238 else:
239 return wpt_wrapper.buildTree(self.wpt, tree_name, origin.to_list())
240
241 @validate_tree_uuid_params
242 def getTrunkUUIDs(self, tree_id:int) -> List[int]:
243 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
244 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
245 return wpt_wrapper.getTrunkUUIDs(self.wpt, tree_id)
246
247 @validate_tree_uuid_params
248 def getBranchUUIDs(self, tree_id:int) -> List[int]:
249 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
250 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
251 return wpt_wrapper.getBranchUUIDs(self.wpt, tree_id)
252
253 @validate_tree_uuid_params
254 def getLeafUUIDs(self, tree_id:int) -> List[int]:
255 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
256 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
257 return wpt_wrapper.getLeafUUIDs(self.wpt, tree_id)
258
259 @validate_tree_uuid_params
260 def getAllUUIDs(self, tree_id:int) -> List[int]:
261 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
262 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
263 return wpt_wrapper.getAllUUIDs(self.wpt, tree_id)
264
265 @validate_recursion_params
266 def setBranchRecursionLevel(self, level:int) -> None:
267 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
268 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
269 wpt_wrapper.setBranchRecursionLevel(self.wpt, level)
270
271 @validate_trunk_segment_params
272 def setTrunkSegmentResolution(self, trunk_segs:int) -> None:
273 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
274 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
275 wpt_wrapper.setTrunkSegmentResolution(self.wpt, trunk_segs)
276
277 @validate_branch_segment_params
278 def setBranchSegmentResolution(self, branch_segs:int) -> None:
279 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
280 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
281 wpt_wrapper.setBranchSegmentResolution(self.wpt, branch_segs)
282
283 @validate_leaf_subdivisions_params
284 def setLeafSubdivisions(self, leaf_segs_x:int, leaf_segs_y:int) -> None:
285 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
286 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
287 wpt_wrapper.setLeafSubdivisions(self.wpt, leaf_segs_x, leaf_segs_y)
288
289 @validate_xml_file_params
290 def loadXML(self, filename: str, silent: bool = False) -> None:
291 """
292 Load custom tree species from XML file.
293
294 Loads tree species definitions from an XML file into the WeberPennTree library.
295 After loading, trees can be built using buildTree() with the custom species names
296 defined in the XML file.
297
298 Args:
299 filename: Path to XML file containing tree species definitions.
300 Can be absolute or relative to current working directory.
301 silent: If True, suppress console output during loading. Default False.
302
303 Raises:
304 ValueError: If filename is invalid or empty
305 FileNotFoundError: If XML file does not exist
306 HeliosRuntimeError: If XML file is malformed or cannot be parsed
307
308 Example:
309 >>> wpt = WeberPennTree(context)
310 >>> wpt.loadXML("my_custom_trees.xml")
311 >>> tree_id = wpt.buildTree("CustomOak") # Use custom species name
312
313 Note:
314 XML file must follow WeberPennTree XML schema. See WeberPennTreeLibrary.xml
315 in helios-core/plugins/weberpenntree/xml/ for format examples.
316 """
317 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
318 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
319
320 # Convert relative path to absolute BEFORE changing working directory
321 xml_path = Path(filename)
322 if not xml_path.is_absolute():
323 xml_path = xml_path.resolve()
324
325 # Use working directory context manager for C++ asset access
327 wpt_wrapper.loadXML(self.wpt, str(xml_path), silent)
List[int] getAllUUIDs(self, int tree_id)
None setBranchSegmentResolution(self, int branch_segs)
None loadXML(self, str filename, bool silent=False)
Load custom tree species from XML file.
List[int] getLeafUUIDs(self, int tree_id)
None setTrunkSegmentResolution(self, int trunk_segs)
List[int] getBranchUUIDs(self, int tree_id)
int buildTree(self, wpt_type, vec3 origin=vec3(0, 0, 0), float scale=1)
Build a tree using either a built-in tree type or custom species name.
List[int] getTrunkUUIDs(self, int tree_id)
None setLeafSubdivisions(self, int leaf_segs_x, int leaf_segs_y)
__exit__(self, exc_type, exc_value, traceback)
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
None setBranchRecursionLevel(self, int level)
is_weberpenntree_available()
Check if WeberPennTree plugin is available for use.
_weberpenntree_working_directory()
Context manager that temporarily changes working directory to where WeberPennTree assets are located.