0.1.26
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, check_context_alive
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 _check_context_alive(self):
179 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
180 check_context_alive(getattr(self, "context", None), "WeberPennTree")
181
182 def __enter__(self):
183 return self
184
185 def __exit__(self, exc_type, exc_value, traceback):
186 if self.wpt is not None:
187 try:
188 wpt_wrapper.destroyWeberPennTree(self.wpt)
189 finally:
190 self.wpt = None # Prevent double deletion
191
192 def __del__(self):
193 """Destructor to ensure C++ resources freed even without 'with' statement."""
194 if hasattr(self, 'wpt') and self.wpt is not None:
195 try:
196 wpt_wrapper.destroyWeberPennTree(self.wpt)
197 self.wpt = None
198 except Exception as e:
199 import warnings
200 warnings.warn(f"Error in WeberPennTree.__del__: {e}")
201
202 def getNativePtr(self):
203 return self.wpt
204
205
206 def buildTree(self, wpt_type, origin:vec3=vec3(0, 0, 0), scale:float=1) -> int:
207 """
208 Build a tree using either a built-in tree type or custom species name.
209
210 Args:
211 wpt_type: Either WPTType enum for built-in types, or string for custom species loaded via loadXML()
212 origin: Tree origin position (default: vec3(0, 0, 0))
213 scale: Tree scale factor (default: 1.0)
214
215 Returns:
216 Tree ID for querying tree components
217 """
218 # Validate inputs
219 validate_vec3(origin, "origin", "buildTree")
220 validate_positive_value(scale, "scale", "buildTree")
222 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
223 raise RuntimeError(
224 f"WeberPennTree is not properly initialized. "
225 f"This may indicate that the weberpenntree plugin is not available. "
226 f"Check plugin status with context.print_plugin_status()"
227 )
228
229 # Convert wpt_type to string (handle both WPTType enum and string)
230 if isinstance(wpt_type, WPTType):
231 tree_name = wpt_type.value
232 elif isinstance(wpt_type, str):
233 tree_name = wpt_type
234 else:
235 raise TypeError(f"wpt_type must be WPTType enum or string, got {type(wpt_type).__name__}")
236
237 # Use working directory context manager during tree building to access assets
239 # Use scale-aware function if scale is not 1.0, otherwise use regular function
240 if scale != 1.0:
242 return wpt_wrapper.buildTreeWithScale(self.wpt, tree_name, origin.to_list(), scale)
243 else:
245 return wpt_wrapper.buildTree(self.wpt, tree_name, origin.to_list())
246
247 @validate_tree_uuid_params
248 def getTrunkUUIDs(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.")
252 return wpt_wrapper.getTrunkUUIDs(self.wpt, tree_id)
253
254 @validate_tree_uuid_params
255 def getBranchUUIDs(self, tree_id:int) -> List[int]:
256 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
257 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
259 return wpt_wrapper.getBranchUUIDs(self.wpt, tree_id)
260
261 @validate_tree_uuid_params
262 def getLeafUUIDs(self, tree_id:int) -> List[int]:
263 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
264 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
266 return wpt_wrapper.getLeafUUIDs(self.wpt, tree_id)
267
268 @validate_tree_uuid_params
269 def getAllUUIDs(self, tree_id:int) -> List[int]:
270 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
271 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
273 return wpt_wrapper.getAllUUIDs(self.wpt, tree_id)
274
275 @validate_recursion_params
276 def setBranchRecursionLevel(self, level:int) -> None:
277 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
278 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
280 wpt_wrapper.setBranchRecursionLevel(self.wpt, level)
281
282 @validate_trunk_segment_params
283 def setTrunkSegmentResolution(self, trunk_segs:int) -> None:
284 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
285 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
287 wpt_wrapper.setTrunkSegmentResolution(self.wpt, trunk_segs)
288
289 @validate_branch_segment_params
290 def setBranchSegmentResolution(self, branch_segs:int) -> None:
291 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
292 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
294 wpt_wrapper.setBranchSegmentResolution(self.wpt, branch_segs)
295
296 @validate_leaf_subdivisions_params
297 def setLeafSubdivisions(self, leaf_segs_x:int, leaf_segs_y:int) -> None:
298 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
299 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
301 wpt_wrapper.setLeafSubdivisions(self.wpt, leaf_segs_x, leaf_segs_y)
302
303 @validate_xml_file_params
304 def loadXML(self, filename: str, silent: bool = False) -> None:
305 """
306 Load custom tree species from XML file.
307
308 Loads tree species definitions from an XML file into the WeberPennTree library.
309 After loading, trees can be built using buildTree() with the custom species names
310 defined in the XML file.
311
312 Args:
313 filename: Path to XML file containing tree species definitions.
314 Can be absolute or relative to current working directory.
315 silent: If True, suppress console output during loading. Default False.
316
317 Raises:
318 ValueError: If filename is invalid or empty
319 FileNotFoundError: If XML file does not exist
320 HeliosRuntimeError: If XML file is malformed or cannot be parsed
321
322 Example:
323 >>> wpt = WeberPennTree(context)
324 >>> wpt.loadXML("my_custom_trees.xml")
325 >>> tree_id = wpt.buildTree("CustomOak") # Use custom species name
326
327 Note:
328 XML file must follow WeberPennTree XML schema. See WeberPennTreeLibrary.xml
329 in helios-core/plugins/weberpenntree/xml/ for format examples.
330 """
331 if not self.wpt or not isinstance(self.wpt, ctypes._Pointer):
332 raise RuntimeError("WeberPennTree is not properly initialized. Check plugin availability.")
333
334 # Convert relative path to absolute BEFORE changing working directory
335 xml_path = Path(filename)
336 if not xml_path.is_absolute():
337 xml_path = xml_path.resolve()
338
339 # Use working directory context manager for C++ asset access
342 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)
_check_context_alive(self)
Raise if the owning Context has been destroyed (see Context.check_context_alive).
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.