11from contextlib
import contextmanager
12from dataclasses
import dataclass, field, fields
as dataclass_fields
14from pathlib
import Path
15from typing
import List, Union
17from .wrappers
import UWeberPennTreeWrapper
as wpt_wrapper
18from .wrappers.DataTypes
import vec3, RGBcolor
19from .plugins.registry
import get_plugin_registry, graceful_plugin_fallback
20from .validation.plugins
import validate_wpt_parameters
21from .validation.datatypes
import validate_vec3
22from .validation.core
import validate_positive_value
23from .assets
import get_asset_manager
24from .validation.plugin_decorators
import (
25 validate_tree_uuid_params, validate_recursion_params, validate_trunk_segment_params,
26 validate_branch_segment_params, validate_leaf_subdivisions_params, validate_xml_file_params
32 Check if WeberPennTree plugin is available for use.
35 bool: True if WeberPennTree can be used, False otherwise
39 plugin_registry = get_plugin_registry()
40 if not plugin_registry.is_plugin_available(
'weberpenntree'):
44 if not wpt_wrapper._WPT_FUNCTIONS_AVAILABLE:
51from .Context
import Context, check_context_alive
56 Context manager that temporarily changes working directory to where WeberPennTree assets are located.
58 WeberPennTree C++ code uses hardcoded relative paths like "plugins/weberpenntree/xml/WeberPennTreeLibrary.xml"
59 expecting assets relative to working directory. This manager temporarily changes to the build directory
60 where assets are actually located.
63 RuntimeError: If build directory or WeberPennTree assets are not found, indicating a build system error.
67 asset_manager = get_asset_manager()
68 working_dir = asset_manager._get_helios_build_path()
70 if working_dir
and working_dir.exists():
71 weberpenntree_assets = working_dir /
'plugins' /
'weberpenntree'
74 current_dir = Path(__file__).parent
75 packaged_build = current_dir /
'assets' /
'build'
77 if packaged_build.exists():
78 working_dir = packaged_build
79 weberpenntree_assets = working_dir /
'plugins' /
'weberpenntree'
82 repo_root = current_dir.parent
83 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
84 working_dir = build_lib_dir.parent
85 weberpenntree_assets = working_dir /
'plugins' /
'weberpenntree'
87 if not build_lib_dir.exists():
89 f
"PyHelios build directory not found: {build_lib_dir}. "
90 f
"WeberPennTree requires native libraries to be built. "
91 f
"Run: python build_scripts/build_helios.py --plugins weberpenntree"
94 if not weberpenntree_assets.exists():
96 f
"WeberPennTree assets not found: {weberpenntree_assets}. "
97 f
"Build system failed to copy WeberPennTree assets. "
98 f
"Run: python build_scripts/build_helios.py --clean --plugins weberpenntree"
101 xml_file = weberpenntree_assets /
'xml' /
'WeberPennTreeLibrary.xml'
102 if not xml_file.exists():
104 f
"WeberPennTree XML library not found: {xml_file}. "
105 f
"Critical WeberPennTree asset missing from build. "
106 f
"Run: python build_scripts/build_helios.py --clean --plugins weberpenntree"
110 original_cwd = Path.cwd()
112 os.chdir(working_dir)
115 os.chdir(original_cwd)
125 PISTACHIO =
'Pistachio'
128_INT32_MIN, _INT32_MAX = -2**31, 2**31 - 1
129_UINT32_MAX = 2**32 - 1
133_PER_LEVEL_FLOAT_FIELDS = (
134 'nSegSplits',
'nSplitAngle',
'nSplitAngleV',
'nCurve',
'nCurveV',
'nCurveBack',
'nLength',
'nLengthV',
135 'nTaper',
'nDownAngle',
'nDownAngleV',
'nRotate',
'nRotateV',
137_PER_LEVEL_INT_FIELDS = (
'nCurveRes',
'nBranches')
138_SCALAR_FLOAT_FIELDS = (
139 'BaseSize',
'BaseSizeV',
'BaseSplitSize',
'BaseSplitSizeV',
'Scale',
'ScaleV',
'ZScale',
'ZScaleV',
140 'Ratio',
'RatioPower',
'LobeDepth',
'Flare',
'LeafScale',
'LeafScaleX',
142_SCALAR_INT_FIELDS = (
'Shape',
'Lobes',
'Levels',
'Leaves',
'LeafShape')
143_SCALAR_UINT_FIELDS = (
'BaseSplits',
'BaseSplitsV')
144_STRING_FIELDS = (
'label',
'LeafFile',
'WoodFile')
150 Architectural parameters of a Weber-Penn tree, mirroring the C++ ``WeberPennTreeParameters`` struct.
152 Field names and meanings are identical to the C++ struct and to the tags of the XML tree library
153 (see the "Creating Custom XML Files" section of the plugin documentation). Per-level arrays
154 (``nSegSplits`` ... ``nBranches``) are indexed by recursion level (0 = trunk) and must hold at least
155 ``Levels + 1`` entries. ``leafAngleCDF`` is the cumulative leaf inclination distribution computed from
156 the XML ``LeafAngleDist`` tag (empty means uniform). Float values are stored as 32-bit floats natively.
158 The usual way to obtain one is :meth:`WeberPennTree.getTreeParameters`, modify it, and pass it to
159 :meth:`WeberPennTree.setTreeParameters`.
163 BaseSize: float = 0.0
164 BaseSizeV: float = 0.0
167 BaseSplitSize: float = 0.0
168 BaseSplitSizeV: float = 0.0
174 RatioPower: float = 0.0
176 LobeDepth: float = 0.0
179 nSegSplits: List[float] = field(default_factory=list)
180 nSplitAngle: List[float] = field(default_factory=list)
181 nSplitAngleV: List[float] = field(default_factory=list)
182 nCurveRes: List[int] = field(default_factory=list)
183 nCurve: List[float] = field(default_factory=list)
184 nCurveV: List[float] = field(default_factory=list)
185 nCurveBack: List[float] = field(default_factory=list)
186 nLength: List[float] = field(default_factory=list)
187 nLengthV: List[float] = field(default_factory=list)
188 nTaper: List[float] = field(default_factory=list)
189 nDownAngle: List[float] = field(default_factory=list)
190 nDownAngleV: List[float] = field(default_factory=list)
191 nRotate: List[float] = field(default_factory=list)
192 nRotateV: List[float] = field(default_factory=list)
193 nBranches: List[int] = field(default_factory=list)
197 LeafScale: float = 0.0
198 LeafScaleX: float = 0.0
199 WoodColor: RGBcolor = field(default_factory=
lambda:
RGBcolor(0.4, 0.2, 0.05))
200 WoodFile: str =
'none'
201 leafAngleCDF: List[float] = field(default_factory=list)
204 if not isinstance(other, WeberPennTreeParameters):
205 return NotImplemented
206 for f
in dataclass_fields(self):
207 a, b = getattr(self, f.name), getattr(other, f.name)
208 if f.name ==
'WoodColor':
209 if not (isinstance(a, RGBcolor)
and isinstance(b, RGBcolor))
or a.to_list() != b.to_list():
211 elif isinstance(a, (list, tuple))
and isinstance(b, (list, tuple)):
212 if list(a) != list(b):
219 result = {f.name: getattr(self, f.name)
for f
in dataclass_fields(self)}
226 wood = values[
'WoodColor']
228 color.r, color.g, color.b = wood[0], wood[1], wood[2]
229 values[
'WoodColor'] = color
234 Check field types, ranges and per-level array lengths.
237 ValueError: If any field has the wrong type, is out of range, or a per-level array is too short.
240 return isinstance(v, int)
and not isinstance(v, bool)
243 return isinstance(v, (int, float))
and not isinstance(v, bool)
and math.isfinite(v)
245 for name
in _STRING_FIELDS:
246 v = getattr(self, name)
247 if not isinstance(v, str):
248 raise ValueError(f
"WeberPennTreeParameters.{name} must be a str, got {type(v).__name__}")
250 raise ValueError(f
"WeberPennTreeParameters.{name} must not contain NUL characters")
251 for name
in _SCALAR_FLOAT_FIELDS:
252 v = getattr(self, name)
254 raise ValueError(f
"WeberPennTreeParameters.{name} must be a finite number, got {type(v).__name__}: {v!r}")
255 for name
in _SCALAR_INT_FIELDS:
256 v = getattr(self, name)
257 if not is_int(v)
or not (_INT32_MIN <= v <= _INT32_MAX):
258 raise ValueError(f
"WeberPennTreeParameters.{name} must be a 32-bit int, got {type(v).__name__}: {v!r}")
259 for name
in _SCALAR_UINT_FIELDS:
260 v = getattr(self, name)
261 if not is_int(v)
or not (0 <= v <= _UINT32_MAX):
262 raise ValueError(f
"WeberPennTreeParameters.{name} must be a non-negative 32-bit int, got {type(v).__name__}: {v!r}")
263 if not isinstance(self.
WoodColor, RGBcolor):
264 raise ValueError(f
"WeberPennTreeParameters.WoodColor must be an RGBcolor, got {type(self.WoodColor).__name__}")
265 for name
in _PER_LEVEL_FLOAT_FIELDS + (
'leafAngleCDF',):
266 v = getattr(self, name)
267 if not isinstance(v, (list, tuple))
or not all(is_real(x)
for x
in v):
268 raise ValueError(f
"WeberPennTreeParameters.{name} must be a list of finite numbers, got {v!r}")
269 for name
in _PER_LEVEL_INT_FIELDS:
270 v = getattr(self, name)
271 if not isinstance(v, (list, tuple))
or not all(is_int(x)
and _INT32_MIN <= x <= _INT32_MAX
for x
in v):
272 raise ValueError(f
"WeberPennTreeParameters.{name} must be a list of 32-bit ints, got {v!r}")
274 if not (1 <= self.
Levels <= _MAX_LEVELS):
276 f
"WeberPennTreeParameters.Levels is {self.Levels}, but it must be between 1 and {_MAX_LEVELS} "
277 f
"(level 0 is the trunk and leaves are placed at level Levels)."
279 required = self.
Levels + 1
280 for name
in _PER_LEVEL_FLOAT_FIELDS + _PER_LEVEL_INT_FIELDS:
281 n = len(getattr(self, name))
284 f
"WeberPennTreeParameters.{name} has {n} entries, but Levels={self.Levels} requires at least {required}."
291 def __init__(self, context:Context):
292 self.context = context
293 self._plugin_registry = get_plugin_registry()
296 from .wrappers.UWeberPennTreeWrapper
import _WPT_FUNCTIONS_AVAILABLE
297 if not _WPT_FUNCTIONS_AVAILABLE:
298 raise NotImplementedError(
"WeberPennTree functions not available in current Helios library.")
301 if not self._plugin_registry.is_plugin_available(
'weberpenntree'):
302 print(
"Warning: WeberPennTree plugin not detected in current build")
303 print(
"Tree generation functionality may be limited or unavailable")
306 asset_manager = get_asset_manager()
307 build_dir = asset_manager._get_helios_build_path()
309 if not build_dir
or not build_dir.exists():
311 current_dir = Path(__file__).parent
312 packaged_build = current_dir /
'assets' /
'build'
314 if packaged_build.exists()
and (packaged_build /
'plugins' /
'weberpenntree').exists():
315 build_dir = packaged_build
318 repo_root = current_dir.parent
319 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
320 build_dir = build_lib_dir.parent
322 if not build_dir.exists():
324 f
"PyHelios build directory not found: {build_dir}. "
325 f
"WeberPennTree requires native libraries to be built. "
326 f
"Run: python build_scripts/build_helios.py --plugins weberpenntree"
331 original_cwd = Path.cwd()
334 self.
wpt = wpt_wrapper.createWeberPennTreeWithBuildPluginRootDirectory(
335 context.getNativePtr(), str(build_dir)
338 os.chdir(original_cwd)
341 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
342 check_context_alive(getattr(self,
"context",
None),
"WeberPennTree")
347 def __exit__(self, exc_type, exc_value, traceback):
348 if self.
wpt is not None:
350 wpt_wrapper.destroyWeberPennTree(self.
wpt)
355 """Destructor to ensure C++ resources freed even without 'with' statement."""
356 if hasattr(self,
'wpt')
and self.
wpt is not None:
358 wpt_wrapper.destroyWeberPennTree(self.
wpt)
360 except Exception
as e:
362 warnings.warn(f
"Error in WeberPennTree.__del__: {e}")
368 def buildTree(self, wpt_type, origin:vec3=
vec3(0, 0, 0), scale:float=1) -> int:
370 Build a tree using either a built-in tree type or custom species name.
373 wpt_type: Either WPTType enum for built-in types, or string for custom species loaded via loadXML()
374 origin: Tree origin position (default: vec3(0, 0, 0))
375 scale: Tree scale factor (default: 1.0)
378 Tree ID for querying tree components
381 validate_vec3(origin,
"origin",
"buildTree")
382 validate_positive_value(scale,
"scale",
"buildTree")
384 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
386 f
"WeberPennTree is not properly initialized. "
387 f
"This may indicate that the weberpenntree plugin is not available. "
388 f
"Check plugin status with context.print_plugin_status()"
392 if isinstance(wpt_type, WPTType):
393 tree_name = wpt_type.value
394 elif isinstance(wpt_type, str):
397 raise TypeError(f
"wpt_type must be WPTType enum or string, got {type(wpt_type).__name__}")
404 return wpt_wrapper.buildTreeWithScale(self.
wpt, tree_name, origin.to_list(), scale)
407 return wpt_wrapper.buildTree(self.
wpt, tree_name, origin.to_list())
409 @validate_tree_uuid_params
411 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
412 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
414 return wpt_wrapper.getTrunkUUIDs(self.
wpt, tree_id)
416 @validate_tree_uuid_params
418 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
419 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
421 return wpt_wrapper.getBranchUUIDs(self.
wpt, tree_id)
423 @validate_tree_uuid_params
425 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
426 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
428 return wpt_wrapper.getLeafUUIDs(self.
wpt, tree_id)
430 @validate_tree_uuid_params
432 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
433 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
435 return wpt_wrapper.getAllUUIDs(self.
wpt, tree_id)
437 @validate_recursion_params
439 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
440 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
442 wpt_wrapper.setBranchRecursionLevel(self.
wpt, level)
444 @validate_trunk_segment_params
446 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
447 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
449 wpt_wrapper.setTrunkSegmentResolution(self.
wpt, trunk_segs)
451 @validate_branch_segment_params
453 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
454 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
456 wpt_wrapper.setBranchSegmentResolution(self.
wpt, branch_segs)
458 @validate_leaf_subdivisions_params
460 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
461 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
463 wpt_wrapper.setLeafSubdivisions(self.
wpt, leaf_segs_x, leaf_segs_y)
465 @validate_xml_file_params
466 def loadXML(self, filename: str, silent: bool =
False) ->
None:
468 Load custom tree species from XML file.
470 Loads tree species definitions from an XML file into the WeberPennTree library.
471 After loading, trees can be built using buildTree() with the custom species names
472 defined in the XML file.
475 filename: Path to XML file containing tree species definitions.
476 Can be absolute or relative to current working directory.
477 silent: If True, suppress console output during loading. Default False.
480 ValueError: If filename is invalid or empty
481 FileNotFoundError: If XML file does not exist
482 HeliosRuntimeError: If XML file is malformed or cannot be parsed
485 >>> wpt = WeberPennTree(context)
486 >>> wpt.loadXML("my_custom_trees.xml")
487 >>> tree_id = wpt.buildTree("CustomOak") # Use custom species name
490 XML file must follow WeberPennTree XML schema. See "Creating Custom XML Files" in the
491 WeberPennTree plugin documentation for the format.
493 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
494 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
497 xml_path = Path(filename)
498 if not xml_path.is_absolute():
499 xml_path = xml_path.resolve()
504 wpt_wrapper.loadXML(self.
wpt, str(xml_path), silent)
507 def _tree_name(tree_name: Union[WPTType, str], method: str) -> str:
508 if isinstance(tree_name, WPTType):
509 return tree_name.value
510 if isinstance(tree_name, str):
511 if not tree_name.strip():
512 raise ValueError(f
"{method}: tree_name must be a non-empty string")
513 if '\x00' in tree_name:
514 raise ValueError(f
"{method}: tree_name must not contain NUL characters")
516 raise ValueError(f
"{method}: tree_name must be a WPTType or str, got {type(tree_name).__name__}")
518 def getTreeParameters(self, tree_name: Union[WPTType, str]) -> WeberPennTreeParameters:
520 Get the architectural parameters of a tree in the library.
523 tree_name: WPTType for a built-in species, or the name of a species loaded with loadXML()
524 or defined with setTreeParameters().
527 WeberPennTreeParameters with every field of the C++ struct.
530 ValueError: If tree_name is not a WPTType or non-empty string.
531 HeliosRuntimeError: If no tree with that name exists in the library.
533 name = self.
_tree_name(tree_name,
"getTreeParameters")
534 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
535 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
537 return WeberPennTreeParameters._from_wrapper_fields(wpt_wrapper.getTreeParameters(self.
wpt, name))
539 def setTreeParameters(self, tree_name: Union[WPTType, str], parameters: WeberPennTreeParameters) ->
None:
541 Set the architectural parameters of a tree in the library.
543 If ``tree_name`` is not yet in the library, a new species with that name is added, and
544 ``buildTree(tree_name)`` will then build it. Parameters are validated in Python (types, Levels
545 range, per-level array lengths) and again natively (e.g. Scale > 0, nCurveRes >= 1,
546 0 <= BaseSize < 1); an invalid set leaves the library unchanged.
549 tree_name: WPTType or species name.
550 parameters: WeberPennTreeParameters, typically obtained from getTreeParameters() and modified.
553 ValueError: If tree_name or parameters are invalid.
554 HeliosRuntimeError: If the native validation rejects the parameters.
556 name = self.
_tree_name(tree_name,
"setTreeParameters")
557 if not isinstance(parameters, WeberPennTreeParameters):
559 f
"setTreeParameters: parameters must be a WeberPennTreeParameters, got {type(parameters).__name__}"
561 parameters.validate()
562 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
563 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
565 wpt_wrapper.setTreeParameters(self.
wpt, name, parameters._to_wrapper_fields())
569 Seed the WeberPennTree random number generator to make tree geometry reproducible.
571 WeberPennTree has its own generator, independent of Context.seedRandomGenerator(); seeding
572 the Context does not make trees repeatable. The generator advances with each buildTree() call,
573 so re-seed before each build that should be reproduced.
576 seed: Non-negative integer below 2**32.
579 ValueError: If seed is not an int in [0, 2**32).
581 if not isinstance(seed, int)
or isinstance(seed, bool)
or not (0 <= seed <= _UINT32_MAX):
582 raise ValueError(f
"seedRandomGenerator: seed must be an int in [0, 2**32), got {seed!r}")
583 if not self.
wpt or not isinstance(self.
wpt, ctypes._Pointer):
584 raise RuntimeError(
"WeberPennTree is not properly initialized. Check plugin availability.")
586 wpt_wrapper.seedRandomGenerator(self.
wpt, seed)
Architectural parameters of a Weber-Penn tree, mirroring the C++ WeberPennTreeParameters struct.
dict _to_wrapper_fields(self)
'WeberPennTreeParameters' _from_wrapper_fields(cls, dict raw)
None validate(self)
Check field types, ranges and per-level array lengths.
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).
str _tree_name(Union[WPTType, str] tree_name, str method)
None seedRandomGenerator(self, int seed)
Seed the WeberPennTree random number generator to make tree geometry reproducible.
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)
WeberPennTreeParameters getTreeParameters(self, Union[WPTType, str] tree_name)
Get the architectural parameters of a tree in the library.
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
None setBranchRecursionLevel(self, int level)
None setTreeParameters(self, Union[WPTType, str] tree_name, WeberPennTreeParameters parameters)
Set the architectural parameters of a tree in the library.
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.