1.3.64
 
Loading...
Searching...
No Matches
PlantArchitecture.cpp
Go to the documentation of this file.
1
16#include "PlantArchitecture.h"
17#include "CollisionDetection.h"
18
19#include <unordered_set>
20#include <utility>
21
22using namespace helios;
23
24// Minimum thresholds for creating tube geometry to avoid malformed triangles
25static const float MIN_TUBE_RADIUS_FOR_GEOMETRY = 1e-5f;
26static const float MIN_TUBE_LENGTH_FOR_GEOMETRY = 1e-4f;
27
28static float clampOffset(int count_per_axis, float offset) {
29 if (count_per_axis > 2) {
30 float denom = 0.5f * float(count_per_axis) - 1.f;
31 if (offset * denom > 1.f) {
32 offset = 1.f / denom;
33 }
34 }
35 return offset;
36}
37
38float PlantArchitecture::interpolateTube(const std::vector<float> &P, const float frac) {
39 assert(frac >= 0 && frac <= 1);
40 assert(!P.empty());
41
42 float dl = 1.f / float(P.size() - 1);
43
44 float f = 0;
45 for (int i = 0; i < P.size() - 1; i++) {
46 float fplus = f + dl;
47
48 if (fplus >= 1.f) {
49 fplus = 1.f + 1e-3;
50 }
51
52 if (frac >= f && (frac <= fplus || std::abs(frac - fplus) < 0.0001)) {
53 float V = P.at(i) + (frac - f) / (fplus - f) * (P.at(i + 1) - P.at(i));
54
55 return V;
56 }
57
58 f = fplus;
59 }
60
61 return P.front();
62}
63
64vec3 PlantArchitecture::interpolateTube(const std::vector<vec3> &P, const float frac) {
65 assert(frac >= 0 && frac <= 1);
66 assert(!P.empty());
67
68 float dl = 0.f;
69 for (int i = 0; i < P.size() - 1; i++) {
70 dl += (P.at(i + 1) - P.at(i)).magnitude();
71 }
72
73 float f = 0;
74 for (int i = 0; i < P.size() - 1; i++) {
75 float dseg = (P.at(i + 1) - P.at(i)).magnitude();
76
77 float fplus = f + dseg / dl;
78
79 if (fplus >= 1.f) {
80 fplus = 1.f + 1e-3;
81 }
82
83 if (frac >= f && (frac <= fplus || fabs(frac - fplus) < 0.0001)) {
84 vec3 V = P.at(i) + (frac - f) / (fplus - f) * (P.at(i + 1) - P.at(i));
85
86 return V;
87 }
88
89 f = fplus;
90 }
91
92 return P.front();
93}
94
95PlantArchitecture::PlantArchitecture(helios::Context *context_ptr) : context_ptr(context_ptr) {
96 generator = context_ptr->getRandomGenerator();
97
98
99 // Initialize plant model registrations
100 initializePlantModelRegistrations();
101
102 output_object_data["age"] = false;
103 output_object_data["rank"] = false;
104 output_object_data["plantID"] = false;
105 output_object_data["plant_name"] = false;
106 output_object_data["plant_height"] = false;
107 output_object_data["plant_type"] = false;
108 output_object_data["phenology_stage"] = false;
109 output_object_data["leafID"] = false;
110 output_object_data["peduncleID"] = false;
111 output_object_data["closedflowerID"] = false;
112 output_object_data["openflowerID"] = false;
113 output_object_data["fruitID"] = false;
114 output_object_data["carbohydrate_concentration"] = false;
115}
116
118 // Clean up owned CollisionDetection instance
119 if (collision_detection_ptr != nullptr && owns_collision_detection) {
120 delete collision_detection_ptr;
121 collision_detection_ptr = nullptr;
122 owns_collision_detection = false;
123 }
124}
125
126std::string PlantArchitecture::resolveTextureFile(const std::string &texture_file) {
127 // Empty path returns empty string
128 if (texture_file.empty()) {
129 return "";
130 }
131
132 std::filesystem::path filepath(texture_file);
133
134 // Absolute paths that exist are returned as-is
135 if (filepath.is_absolute() && std::filesystem::exists(filepath)) {
136 return texture_file;
137 }
138
139 // Try resolving as a general file path (handles already-resolved paths and paths relative to cwd)
140 std::filesystem::path resolved_path = helios::tryResolveFilePath(texture_file);
141 if (!resolved_path.empty()) {
142 return resolved_path.string();
143 }
144
145 // Try resolving as a plugin asset path
146 resolved_path = helios::tryResolvePluginAsset("plantarchitecture", texture_file);
147 if (!resolved_path.empty()) {
148 return resolved_path.string();
149 }
150
151 // If path doesn't have "assets/" prefix, try appropriate asset subdirectory based on file extension
152 if (texture_file.find("assets/") != 0) {
153 std::string filename = std::filesystem::path(texture_file).filename().string();
154 std::string extension = std::filesystem::path(texture_file).extension().string();
155
156 // Determine correct asset subdirectory based on file extension
157 std::string subdirectory = (extension == ".obj" || extension == ".mtl") ? "assets/obj/" : "assets/textures/";
158 std::string assets_path = subdirectory + filename;
159
160 resolved_path = helios::tryResolvePluginAsset("plantarchitecture", assets_path);
161 if (!resolved_path.empty()) {
162 return resolved_path.string();
163 }
164 }
165
166 // None of the resolution strategies worked
167 helios_runtime_error("ERROR (PlantArchitecture): Could not resolve asset file: " + texture_file + ". Tried: direct path, plugin asset path, and assets/ subdirectory prefix.");
168 return ""; // Never reached
169}
170
171LeafPrototype::LeafPrototype(std::minstd_rand0 *generator) : generator(generator) {
172 leaf_aspect_ratio.initialize(1.f, generator);
173 midrib_fold_fraction.initialize(0.f, generator);
174 longitudinal_curvature.initialize(0.f, generator);
175 lateral_curvature.initialize(0.f, generator);
176 petiole_roll.initialize(0.f, generator);
177 wave_period.initialize(0.f, generator);
178 wave_amplitude.initialize(0.f, generator);
179 leaf_buckle_length.initialize(0.f, generator);
180 leaf_buckle_angle.initialize(0.f, generator);
181 subdivisions = 1;
183 leaf_offset = make_vec3(0, 0, 0);
184 prototype_function = GenericLeafPrototype;
185 build_petiolule = false;
186 if (generator != nullptr) {
187 sampleIdentifier();
188 }
189}
190
193
194PhytomerParameters::PhytomerParameters(std::minstd_rand0 *generator) {
195 //--- internode ---//
196 internode.pitch.initialize(20, generator);
197 internode.phyllotactic_angle.initialize(137.5, generator);
198 internode.radius_initial.initialize(0.001, generator);
199 internode.color = RGB::forestgreen;
200 internode.length_segments = 1;
201 internode.radial_subdivisions = 7;
202
203 //--- petiole ---//
204 petiole.petioles_per_internode = 1;
205 petiole.pitch.initialize(90, generator);
206 petiole.radius.initialize(0.001, generator);
207 petiole.length.initialize(0.05, generator);
208 petiole.curvature.initialize(0, generator);
209 petiole.taper.initialize(0, generator);
210 petiole.color = RGB::forestgreen;
211 petiole.length_segments = 1;
212 petiole.radial_subdivisions = 7;
213
214 //--- leaf ---//
215 leaf.leaves_per_petiole.initialize(1, generator);
216 leaf.pitch.initialize(0, generator);
217 leaf.yaw.initialize(0, generator);
218 leaf.roll.initialize(0, generator);
219 leaf.leaflet_offset.initialize(0, generator);
220 leaf.leaflet_scale = 1;
221 leaf.prototype_scale.initialize(0.05, generator);
222 leaf.prototype = LeafPrototype(generator);
223
224 //--- peduncle ---//
225 peduncle.length.initialize(0.05, generator);
226 peduncle.radius.initialize(0.001, generator);
227 peduncle.pitch.initialize(0, generator);
228 peduncle.roll.initialize(0, generator);
229 peduncle.curvature.initialize(0, generator);
230 petiole.color = RGB::forestgreen;
231 peduncle.length_segments = 3;
232 peduncle.radial_subdivisions = 7;
233
234 //--- inflorescence ---//
235 inflorescence.flowers_per_peduncle.initialize(1, generator);
236 inflorescence.flower_offset.initialize(0, generator);
237 inflorescence.pitch.initialize(0, generator);
238 inflorescence.roll.initialize(0, generator);
239 inflorescence.flower_prototype_scale.initialize(0.0075, generator);
240 inflorescence.fruit_prototype_scale.initialize(0.0075, generator);
241 inflorescence.fruit_gravity_factor_fraction.initialize(0, generator);
242 inflorescence.unique_prototypes = 1;
243}
244
247
248ShootParameters::ShootParameters(std::minstd_rand0 *generator) {
249 // ---- Geometric Parameters ---- //
250
251 max_nodes.initialize(10, generator);
252
253 max_nodes_per_season.initialize(9999, generator);
254
255 insertion_angle_tip.initialize(20, generator);
256 insertion_angle_decay_rate.initialize(0, generator);
257
258 internode_length_max.initialize(0.02, generator);
259 internode_length_min.initialize(0.002, generator);
260 internode_length_decay_rate.initialize(0, generator);
261
262 base_roll.initialize(0, generator);
263 base_yaw.initialize(0, generator);
264
265 gravitropic_curvature.initialize(0, generator);
266 tortuosity.initialize(0, generator);
267
268 // ---- Growth Parameters ---- //
269
270 phyllochron_min.initialize(2, generator);
271
272 elongation_rate_max.initialize(0.2, generator);
273 girth_area_factor.initialize(0, generator);
274
275 vegetative_bud_break_time.initialize(5, generator);
276 vegetative_bud_break_probability_min.initialize(0, generator);
277 vegetative_bud_break_probability_max.initialize(1.0, generator);
278 vegetative_bud_break_probability_decay_rate.initialize(-0.5, generator);
279 max_terminal_floral_buds.initialize(0, generator);
280 flower_bud_break_probability.initialize(0, generator);
281 fruit_set_probability.initialize(0, generator);
282
285
287}
288
289void ShootParameters::defineChildShootTypes(const std::vector<std::string> &a_child_shoot_type_labels, const std::vector<float> &a_child_shoot_type_probabilities) {
290 if (a_child_shoot_type_labels.size() != a_child_shoot_type_probabilities.size()) {
291 helios_runtime_error("ERROR (ShootParameters::defineChildShootTypes): Child shoot type labels and probabilities must be the same size.");
292 } else if (a_child_shoot_type_labels.empty()) {
293 helios_runtime_error("ERROR (ShootParameters::defineChildShootTypes): Input argument vectors were empty.");
294 } else if (sum(a_child_shoot_type_probabilities) != 1.f) {
295 helios_runtime_error("ERROR (ShootParameters::defineChildShootTypes): Child shoot type probabilities must sum to 1.");
296 }
297
298 this->child_shoot_type_labels = a_child_shoot_type_labels;
299 this->child_shoot_type_probabilities = a_child_shoot_type_probabilities;
300}
301
302std::vector<uint> PlantArchitecture::buildPlantCanopyFromLibrary(const helios::vec3 &canopy_center_position, const helios::vec2 &plant_spacing_xy, const helios::int2 &plant_count_xy, const float age, const float germination_rate,
303 const std::map<std::string, float> &build_parameters) {
304 if (plant_count_xy.x <= 0 || plant_count_xy.y <= 0) {
305 helios_runtime_error("ERROR (PlantArchitecture::buildPlantCanopyFromLibrary): Plant count must be greater than zero.");
306 }
307
308 vec2 canopy_extent(plant_spacing_xy.x * float(plant_count_xy.x - 1), plant_spacing_xy.y * float(plant_count_xy.y - 1));
309
310 std::vector<uint> plantIDs;
311 plantIDs.reserve(plant_count_xy.x * plant_count_xy.y);
312 for (int j = 0; j < plant_count_xy.y; j++) {
313 for (int i = 0; i < plant_count_xy.x; i++) {
314 if (context_ptr->randu() < germination_rate) {
315 plantIDs.push_back(buildPlantInstanceFromLibrary(canopy_center_position + make_vec3(-0.5f * canopy_extent.x + float(i) * plant_spacing_xy.x, -0.5f * canopy_extent.y + float(j) * plant_spacing_xy.y, 0), 0, build_parameters));
316 }
317 }
318 }
319
320 if (age > 0) {
321 advanceTime(plantIDs, age);
322 }
323
324 return plantIDs;
325}
326
327std::vector<uint> PlantArchitecture::buildPlantCanopyFromLibrary(const helios::vec3 &canopy_center_position, const helios::vec2 &canopy_extent_xy, const uint plant_count, const float age, const std::map<std::string, float> &build_parameters) {
328 std::vector<uint> plantIDs;
329 plantIDs.reserve(plant_count);
330 for (int i = 0; i < plant_count; i++) {
331 vec3 plant_origin = canopy_center_position + make_vec3((-0.5f + context_ptr->randu()) * canopy_extent_xy.x, (-0.5f + context_ptr->randu()) * canopy_extent_xy.y, 0);
332 plantIDs.push_back(buildPlantInstanceFromLibrary(plant_origin, age, build_parameters));
333 }
334
335 return plantIDs;
336}
337
338
339void PlantArchitecture::defineShootType(const std::string &shoot_type_label, const ShootParameters &shoot_params) {
340 if (this->shoot_types.find(shoot_type_label) != this->shoot_types.end()) {
341 // shoot type already exists
342 this->shoot_types.at(shoot_type_label) = shoot_params;
343 } else {
344 this->shoot_types.emplace(shoot_type_label, shoot_params);
345 }
346}
347
348std::vector<helios::vec3> Phytomer::getInternodeNodePositions() const {
349 std::vector<vec3> nodes = parent_shoot_ptr->shoot_internode_vertices.at(shoot_index.x);
350 if (shoot_index.x > 0) {
351 int p_minus = shoot_index.x - 1;
352 int s_minus = parent_shoot_ptr->shoot_internode_vertices.at(p_minus).size() - 1;
353 nodes.insert(nodes.begin(), parent_shoot_ptr->shoot_internode_vertices.at(p_minus).at(s_minus));
354 }
355 return nodes;
356}
357
358std::vector<float> Phytomer::getInternodeNodeRadii() const {
359 std::vector<float> node_radii = parent_shoot_ptr->shoot_internode_radii.at(shoot_index.x);
360 if (shoot_index.x > 0) {
361 int p_minus = shoot_index.x - 1;
362 int s_minus = parent_shoot_ptr->shoot_internode_radii.at(p_minus).size() - 1;
363 node_radii.insert(node_radii.begin(), parent_shoot_ptr->shoot_internode_radii.at(p_minus).at(s_minus));
364 }
365 return node_radii;
366}
367
368helios::vec3 Phytomer::getInternodeAxisVector(const float stem_fraction) const {
369 return getAxisVector(stem_fraction, getInternodeNodePositions());
370}
371
372helios::vec3 Phytomer::getPetioleAxisVector(const float stem_fraction, const uint petiole_index) const {
373 if (petiole_index >= this->petiole_vertices.size()) {
374 helios_runtime_error("ERROR (Phytomer::getPetioleAxisVector): Petiole index out of range.");
375 }
376 return getAxisVector(stem_fraction, this->petiole_vertices.at(petiole_index));
377}
378
379helios::vec3 Phytomer::getPeduncleAxisVector(const float stem_fraction, const uint petiole_index, const uint bud_index) const {
380 if (petiole_index >= this->peduncle_vertices.size()) {
381 helios_runtime_error("ERROR (Phytomer::getPeduncleAxisVector): Petiole index out of range.");
382 }
383 if (bud_index >= this->peduncle_vertices.at(petiole_index).size()) {
384 helios_runtime_error("ERROR (Phytomer::getPeduncleAxisVector): Floral bud index out of range.");
385 }
386 return getAxisVector(stem_fraction, this->peduncle_vertices.at(petiole_index).at(bud_index));
387}
388
389helios::vec3 Phytomer::getAxisVector(const float stem_fraction, const std::vector<helios::vec3> &axis_vertices) {
390 assert(stem_fraction >= 0 && stem_fraction <= 1);
391
392 float df = 0.1f;
393 float frac_plus, frac_minus;
394 if (stem_fraction + df <= 1) {
395 frac_minus = stem_fraction;
396 frac_plus = stem_fraction + df;
397 } else {
398 frac_minus = stem_fraction - df;
399 frac_plus = stem_fraction;
400 }
401
402 const vec3 node_minus = PlantArchitecture::interpolateTube(axis_vertices, frac_minus);
403 const vec3 node_plus = PlantArchitecture::interpolateTube(axis_vertices, frac_plus);
404
405 vec3 norm = node_plus - node_minus;
406 norm.normalize();
407
408 return norm;
409}
410
412 return parent_shoot_ptr->shoot_internode_radii.at(shoot_index.x).front();
413}
414
416 std::vector<vec3> node_vertices = this->getInternodeNodePositions();
417 float length = 0;
418 for (int i = 0; i < node_vertices.size() - 1; i++) {
419 length += (node_vertices.at(i + 1) - node_vertices.at(i)).magnitude();
420 }
421 return length;
422}
423
425 // \todo
426 return 0;
427}
428
429float Phytomer::getInternodeRadius(const float stem_fraction) const {
430 return PlantArchitecture::interpolateTube(parent_shoot_ptr->shoot_internode_radii.at(shoot_index.x), stem_fraction);
431}
432
434 float leaf_area = 0;
435 uint p = 0;
436 for (auto &petiole: leaf_objIDs) {
437 for (auto &leaf_objID: petiole) {
438 if (context_ptr->doesObjectExist(leaf_objID)) {
439 float obj_area = context_ptr->getObjectArea(leaf_objID);
440 float scale_factor = current_leaf_scale_factor.at(p);
441 float scaled_area = obj_area / powi(scale_factor, 2);
442 leaf_area += scaled_area;
443 }
444 }
445 p++;
446 }
447 return leaf_area;
448}
449
450helios::vec3 Phytomer::getLeafBasePosition(const uint petiole_index, const uint leaf_index) const {
451#ifdef HELIOS_DEBUG
452 if (petiole_index >= leaf_bases.size()) {
453 helios_runtime_error("ERROR (Phytomer::getLeafBasePosition): Petiole index out of range.");
454 } else if (leaf_index >= leaf_bases.at(petiole_index).size()) {
455 helios_runtime_error("ERROR (Phytomer::getLeafBasePosition): Leaf index out of range.");
456 }
457#endif
458 return leaf_bases.at(petiole_index).at(leaf_index);
459}
460
462 for (auto &petiole: axillary_vegetative_buds) {
463 for (auto &bud: petiole) {
464 bud.state = state;
465 }
466 }
467}
468
469void Phytomer::setVegetativeBudState(BudState state, uint petiole_index, uint bud_index) {
470 if (petiole_index >= axillary_vegetative_buds.size()) {
471 helios_runtime_error("ERROR (Phytomer::setVegetativeBudState): Petiole index out of range.");
472 }
473 if (bud_index >= axillary_vegetative_buds.at(petiole_index).size()) {
474 helios_runtime_error("ERROR (Phytomer::setVegetativeBudState): Bud index out of range.");
475 }
476 setVegetativeBudState(state, axillary_vegetative_buds.at(petiole_index).at(bud_index));
477}
478
480 vbud.state = state;
481}
482
483void Phytomer::setFloralBudState(BudState state) {
484 for (auto &petiole: floral_buds) {
485 for (auto &fbud: petiole) {
486 if (!fbud.isterminal) {
487 setFloralBudState(state, fbud);
488 }
489 }
490 }
491}
492
493void Phytomer::setFloralBudState(BudState state, uint petiole_index, uint bud_index) {
494 if (petiole_index >= floral_buds.size()) {
495 helios_runtime_error("ERROR (Phytomer::setFloralBudState): Petiole index out of range.");
496 }
497 if (bud_index >= floral_buds.at(petiole_index).size()) {
498 helios_runtime_error("ERROR (Phytomer::setFloralBudState): Bud index out of range.");
499 }
500 setFloralBudState(state, floral_buds.at(petiole_index).at(bud_index));
501}
502
503void Phytomer::setFloralBudState(BudState state, FloralBud &fbud) {
504 // If state is already at the desired state, do nothing
505 if (fbud.state == state) {
506 return;
507 } else if (state == BUD_DORMANT || state == BUD_ACTIVE) {
508 fbud.state = state;
509 return;
510 }
511
512 // Calculate carbon cost
513 if (plantarchitecture_ptr->carbon_model_enabled) {
514 if (state == BUD_FLOWER_CLOSED || (fbud.state == BUD_ACTIVE && state == BUD_FLOWER_OPEN)) {
515 // state went from active to closed flower or open flower
516 float flower_cost = calculateFlowerConstructionCosts(fbud);
517 plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_tree.at(this->parent_shoot_ID)->carbohydrate_pool_molC -= flower_cost;
518 } else if (state == BUD_FRUITING) {
519 // adding a fruit
520 float fruit_cost = calculateFruitConstructionCosts(fbud);
521 fbud.previous_fruit_scale_factor = fbud.current_fruit_scale_factor;
522 if (plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_tree.at(this->parent_shoot_ID)->carbohydrate_pool_molC > fruit_cost) {
523 plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_tree.at(this->parent_shoot_ID)->carbohydrate_pool_molC -= fruit_cost;
524 } else {
525 setFloralBudState(BUD_DEAD, fbud);
526 }
527 }
528 }
529
530 // Delete geometry from previous reproductive state (if present)
531 context_ptr->deleteObject(fbud.inflorescence_objIDs);
532 fbud.inflorescence_objIDs.resize(0);
533 fbud.inflorescence_bases.resize(0);
534 fbud.inflorescence_rotation.resize(0);
535 fbud.inflorescence_base_scales.resize(0);
536
537 if (plantarchitecture_ptr->build_context_geometry_peduncle) {
538 context_ptr->deleteObject(fbud.peduncle_objIDs);
539 fbud.peduncle_objIDs.resize(0);
540 }
541
542 fbud.state = state;
543
544 if (state != BUD_DEAD) {
545 // add new reproductive organs
546
547 updateInflorescence(fbud);
548 fbud.time_counter = 0;
549 if (fbud.state == BUD_FRUITING) {
551 }
552 }
553}
554
555helios::vec3 Phytomer::calculateCollisionAvoidanceDirection(const helios::vec3 &internode_base_origin, const helios::vec3 &internode_axis, bool &collision_detection_active) const {
556 vec3 collision_optimal_direction;
557 collision_detection_active = false;
558
559 if (plantarchitecture_ptr->collision_detection_enabled && plantarchitecture_ptr->collision_detection_ptr != nullptr) {
560
561 // BVH should already be built at timestep level - just use it
562 if (!plantarchitecture_ptr->bvh_cached_for_current_growth) {
563 if (plantarchitecture_ptr->printmessages) {
564 std::cout << "WARNING: BVH not cached - this indicates rebuildBVHForTimestep() was not called" << std::endl;
565 }
566 return collision_optimal_direction; // Skip collision avoidance if BVH not ready
567 }
568
569 // Apply cone-aware culling based on actual collision detection geometry
570 std::vector<uint> filtered_geometry;
571
572 // Calculate spherical sector culling distance
573 // The "cone" is actually a spherical sector with radius = look-ahead distance
574 float look_ahead_distance = plantarchitecture_ptr->collision_cone_height;
575
576 // Only obstacles within the look-ahead distance can be detected by collision rays
577 // Add small buffer for obstacles at sector boundary
578 float max_relevant_distance = look_ahead_distance * 1.1f; // 10% buffer
579
580
581 // Always apply cone-aware culling for performance (no arbitrary thresholds)
582 filtered_geometry = plantarchitecture_ptr->collision_detection_ptr->filterGeometryByDistance(internode_base_origin, max_relevant_distance, plantarchitecture_ptr->cached_target_geometry);
583
584
585 // Update cached filtered geometry for this specific collision check
586 plantarchitecture_ptr->cached_filtered_geometry = filtered_geometry;
587
588 if (plantarchitecture_ptr->bvh_cached_for_current_growth && !plantarchitecture_ptr->cached_filtered_geometry.empty()) {
589 // Set up cone parameters for optimal path finding
590 vec3 apex = internode_base_origin;
591 vec3 central_axis = internode_axis;
592 central_axis.normalize();
593 float height = plantarchitecture_ptr->collision_cone_height;
594 float half_angle = plantarchitecture_ptr->collision_cone_half_angle_rad;
595 int samples = plantarchitecture_ptr->collision_sample_count;
596
597 // Find optimal cone path using gap detection (inertia blending handled later in PlantArchitecture)
598 auto optimal_result = plantarchitecture_ptr->collision_detection_ptr->findOptimalConePath(apex, central_axis, half_angle, height, samples);
599
600 // Store the optimal direction for later blending
601 if (optimal_result.confidence > 0.0f) {
602 collision_optimal_direction = optimal_result.direction;
603 collision_optimal_direction.normalize();
604 collision_detection_active = true;
605 }
606 }
607 }
608 return collision_optimal_direction;
609}
610
611helios::vec3 Phytomer::calculatePetioleCollisionAvoidanceDirection(const helios::vec3 &petiole_base_origin, const helios::vec3 &proposed_petiole_axis, bool &collision_detection_active) const {
612 vec3 collision_optimal_direction;
613 collision_detection_active = false;
614
615 if (plantarchitecture_ptr->collision_detection_enabled && plantarchitecture_ptr->collision_detection_ptr != nullptr) {
616 // Build restricted BVH with target geometry only
617 std::vector<uint> target_geometry;
618 if (!plantarchitecture_ptr->collision_target_UUIDs.empty()) {
619 target_geometry = plantarchitecture_ptr->collision_target_UUIDs;
620 } else if (!plantarchitecture_ptr->collision_target_object_IDs.empty()) {
621 for (uint objID: plantarchitecture_ptr->collision_target_object_IDs) {
622 std::vector<uint> obj_primitives = context_ptr->getObjectPrimitiveUUIDs(objID);
623 target_geometry.insert(target_geometry.end(), obj_primitives.begin(), obj_primitives.end());
624 }
625 } else {
626 // If no specific targets provided, use ALL geometry in Context for collision avoidance
627 target_geometry = context_ptr->getAllUUIDs();
628 }
629
630 // Use cached BVH if available (same cache as internode collision avoidance)
631 if (plantarchitecture_ptr->bvh_cached_for_current_growth && !plantarchitecture_ptr->cached_filtered_geometry.empty()) {
632 // Set up cone parameters for optimal path finding using petiole-specific parameters
633 vec3 apex = petiole_base_origin;
634 vec3 central_axis = proposed_petiole_axis;
635 central_axis.normalize();
636 float height = plantarchitecture_ptr->collision_cone_height;
637 float half_angle = plantarchitecture_ptr->collision_cone_half_angle_rad;
638 int samples = plantarchitecture_ptr->collision_sample_count;
639
640 // Find optimal cone path using gap detection for petiole direction
641 auto optimal_result = plantarchitecture_ptr->collision_detection_ptr->findOptimalConePath(apex, central_axis, half_angle, height, samples);
642
643 // Store the optimal direction for later blending
644 if (optimal_result.confidence > 0.0f) {
645 collision_optimal_direction = optimal_result.direction;
646 collision_optimal_direction.normalize();
647 collision_detection_active = true;
648 }
649 }
650 }
651 return collision_optimal_direction;
652}
653
654helios::vec3 Phytomer::calculateFruitCollisionAvoidanceDirection(const helios::vec3 &fruit_base_origin, const helios::vec3 &proposed_fruit_axis, bool &collision_detection_active) const {
655 vec3 collision_optimal_direction;
656 collision_detection_active = false;
657
658
659 if (plantarchitecture_ptr->collision_detection_enabled && plantarchitecture_ptr->collision_detection_ptr != nullptr) {
660 // Build restricted BVH with target geometry only
661 std::vector<uint> target_geometry;
662 if (!plantarchitecture_ptr->collision_target_UUIDs.empty()) {
663 target_geometry = plantarchitecture_ptr->collision_target_UUIDs;
664 } else if (!plantarchitecture_ptr->collision_target_object_IDs.empty()) {
665 for (uint objID: plantarchitecture_ptr->collision_target_object_IDs) {
666 std::vector<uint> obj_primitives = context_ptr->getObjectPrimitiveUUIDs(objID);
667 target_geometry.insert(target_geometry.end(), obj_primitives.begin(), obj_primitives.end());
668 }
669 } else {
670 // If no specific targets provided, use ALL geometry in Context for collision avoidance
671 target_geometry = context_ptr->getAllUUIDs();
672 }
673
674 // Use cached BVH if available (same cache as internode collision avoidance)
675 if (plantarchitecture_ptr->bvh_cached_for_current_growth && !plantarchitecture_ptr->cached_filtered_geometry.empty()) {
676 // Set up cone parameters for optimal path finding using fruit-specific parameters
677 vec3 apex = fruit_base_origin;
678 vec3 central_axis = proposed_fruit_axis;
679 central_axis.normalize();
680 float height = plantarchitecture_ptr->collision_cone_height;
681 float half_angle = plantarchitecture_ptr->collision_cone_half_angle_rad;
682 int samples = plantarchitecture_ptr->collision_sample_count;
683
684 // Find optimal cone path using gap detection for fruit direction
685 auto optimal_result = plantarchitecture_ptr->collision_detection_ptr->findOptimalConePath(apex, central_axis, half_angle, height, samples);
686
687 // Store the optimal direction for later blending
688 if (optimal_result.confidence > 0.0f) {
689 collision_optimal_direction = optimal_result.direction;
690 collision_optimal_direction.normalize();
691 collision_detection_active = true;
692 }
693
694 // Debug: track when collision detection doesn't find anything
695 static int no_collision_count = 0;
696 if (optimal_result.confidence <= 0.0f) {
697 no_collision_count++;
698 }
699 } else {
700 static int no_bvh_count = 0;
701 no_bvh_count++;
702 }
703 }
704 return collision_optimal_direction;
705}
706
707bool Phytomer::applySolidObstacleAvoidance(const helios::vec3 &current_position, helios::vec3 &internode_axis) const {
708 if (!plantarchitecture_ptr->solid_obstacle_avoidance_enabled || plantarchitecture_ptr->solid_obstacle_UUIDs.empty()) {
709 return false;
710 }
711
712 // Ignore solid obstacles for the first several nodes of the base stem to prevent U-turn growth
713 // when plants start slightly below ground surface
714 if (rank == 0 && (shoot_index.x < 3 || parent_shoot_ptr->calculateShootLength() < 0.05f)) {
715 return false; // Skip solid obstacle avoidance for first 3 nodes OR if shoot length < 5cm
716 }
717
718 vec3 growth_direction = internode_axis;
719 growth_direction.normalize();
720
721 // Check for obstacles using cone-based detection
722 float nearest_obstacle_distance;
723 vec3 nearest_obstacle_direction;
724
725 // Use smaller cone angle for hard obstacle detection
726 float hard_detection_cone_angle = deg2rad(20.0f);
727 float detection_distance = plantarchitecture_ptr->solid_obstacle_avoidance_distance;
728
729 if (plantarchitecture_ptr->collision_detection_ptr != nullptr && plantarchitecture_ptr->collision_detection_ptr->findNearestSolidObstacleInCone(current_position, growth_direction, hard_detection_cone_angle, detection_distance,
730 plantarchitecture_ptr->solid_obstacle_UUIDs, nearest_obstacle_distance, nearest_obstacle_direction)) {
731
732 // Define buffer distance as 5% of detection distance (cone length)
733 float buffer_distance = detection_distance * 0.05f;
734
735 // Normalize distance by detection distance for smooth calculations
736 float normalized_distance = nearest_obstacle_distance / detection_distance;
737 float buffer_threshold = buffer_distance / detection_distance; // Normalized buffer threshold
738
739 vec3 avoidance_direction;
740 float rotation_fraction;
741
742 if (nearest_obstacle_distance <= buffer_distance) {
743 // CRITICAL: Within buffer zone - use strong directional avoidance
744 // Calculate direction that points directly away from the obstacle surface
745 avoidance_direction = current_position - (current_position + nearest_obstacle_direction * nearest_obstacle_distance);
746 if (avoidance_direction.magnitude() < 0.001f) {
747 // Fallback if we can't determine clear avoidance direction
748 avoidance_direction = cross(growth_direction, nearest_obstacle_direction);
749 if (avoidance_direction.magnitude() < 0.001f) {
750 avoidance_direction = make_vec3(0, 0, 1); // Fallback to upward growth
751 }
752 }
753 avoidance_direction.normalize();
754
755 // Strong avoidance when in buffer zone
756 rotation_fraction = 1.0f;
757
758 // Blend growth direction away from obstacle to maintain buffer
759 float buffer_blend_factor = 0.8f; // Strong influence to get out of buffer
760 internode_axis = (1.0f - buffer_blend_factor) * growth_direction + buffer_blend_factor * avoidance_direction;
761 internode_axis.normalize();
762
763 } else {
764 // NORMAL: Outside buffer zone - use smooth rotational avoidance
765
766 // Calculate the angle between growth direction and obstacle direction
767 float dot_with_obstacle = normalize(growth_direction) * normalize(nearest_obstacle_direction);
768 float angle_deficit = asin_safe(fabs(dot_with_obstacle));
769
770 // Calculate perpendicular direction to avoid obstacle
771 vec3 rotation_axis = cross(growth_direction, -nearest_obstacle_direction);
772
773 if (rotation_axis.magnitude() > 0.001f) {
774 rotation_axis.normalize();
775 } else {
776 angle_deficit = 0.f;
777 }
778
779 if (rotation_axis.magnitude() > 0.001f) {
780
781 // Use smooth, normalized distance-based approach
782 // Use increasing function that reaches 1.0 at 20% of the surface distance
783 float surface_threshold_fraction = 0.2f; // Function reaches max strength at 20% of detection distance
784
785 if (normalized_distance <= surface_threshold_fraction) {
786 // Maximum avoidance strength (1.0) when very close to surface
787 rotation_fraction = 1.0f;
788 } else {
789 // Smooth decay from 1.0 to minimum strength as distance increases
790 float remaining_distance = normalized_distance - surface_threshold_fraction;
791 float max_remaining_distance = 1.0f - surface_threshold_fraction;
792
793 // Exponential decay for smoother transitions
794 float distance_factor = remaining_distance / max_remaining_distance; // 0.0 to 1.0
795 float min_rotation_fraction = 0.05f; // Minimum background avoidance strength
796
797 // Exponential decay: strong avoidance close to threshold, gentle far away
798 rotation_fraction = min_rotation_fraction + (1.0f - min_rotation_fraction) * exp(-3.0f * distance_factor);
799 }
800
801 // Apply fraction of the total angle deficit
802 float rotation_this_step = angle_deficit * rotation_fraction;
803
804 // Apply the rotation
805 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, rotation_axis, rotation_this_step);
806 internode_axis.normalize();
807 }
808 }
809
810 return true; // Obstacle found and avoidance applied
811 }
812
813 return false; // No obstacle found
814}
815
816helios::vec3 Phytomer::calculateAttractionPointDirection(const helios::vec3 &internode_base_origin, const helios::vec3 &internode_axis, bool &attraction_active) const {
817 vec3 attraction_direction;
818 attraction_active = false;
819
820 // First check if this plant has plant-specific attraction points enabled
821 if (plantarchitecture_ptr->plant_instances.find(plantID) != plantarchitecture_ptr->plant_instances.end()) {
822 const auto &plant = plantarchitecture_ptr->plant_instances.at(plantID);
823 if (plant.attraction_points_enabled && !plant.attraction_points.empty()) {
824 // Use plant-specific attraction points
825 vec3 look_direction = internode_axis;
826 look_direction.normalize();
827 float half_angle_degrees = rad2deg(plant.attraction_cone_half_angle_rad);
828 float look_ahead_distance = plant.attraction_cone_height;
829
830 vec3 direction_to_closest;
831 if (plantarchitecture_ptr->detectAttractionPointsInCone(plant.attraction_points, internode_base_origin, look_direction, look_ahead_distance, half_angle_degrees, direction_to_closest)) {
832 attraction_direction = direction_to_closest;
833 attraction_direction.normalize();
834 attraction_active = true;
835 }
836 return attraction_direction;
837 }
838 }
839
840 // Fall back to global attraction points for backward compatibility
841 if (!plantarchitecture_ptr->attraction_points_enabled || plantarchitecture_ptr->attraction_points.empty()) {
842 return attraction_direction;
843 }
844
845 // Use the native attraction points detection method from PlantArchitecture (no collision detection required)
846 vec3 look_direction = internode_axis;
847 look_direction.normalize();
848 float half_angle_degrees = rad2deg(plantarchitecture_ptr->attraction_cone_half_angle_rad);
849 float look_ahead_distance = plantarchitecture_ptr->attraction_cone_height;
850
851 vec3 direction_to_closest;
852 if (plantarchitecture_ptr->detectAttractionPointsInCone(plantarchitecture_ptr->attraction_points, internode_base_origin, look_direction, look_ahead_distance, half_angle_degrees, direction_to_closest)) {
853 attraction_direction = direction_to_closest;
854 attraction_direction.normalize();
855 attraction_active = true;
856 }
857
858 return attraction_direction;
859}
860
861bool PlantArchitecture::detectAttractionPointsInCone(const helios::vec3 &vertex, const helios::vec3 &look_direction, float look_ahead_distance, float half_angle_degrees, helios::vec3 &direction_to_closest) const {
862
863 // Validate input parameters
864 if (attraction_points.empty()) {
865 return false;
866 }
867
868 if (look_ahead_distance <= 0.0f) {
869 if (printmessages) {
870 }
871 return false;
872 }
873
874 if (half_angle_degrees <= 0.0f || half_angle_degrees >= 180.0f) {
875 if (printmessages) {
876 }
877 return false;
878 }
879
880 // Convert half-angle to radians
881 float half_angle_rad = half_angle_degrees * M_PI / 180.0f;
882
883 // Normalize look direction
884 vec3 axis = look_direction;
885 axis.normalize();
886
887 // Variables to track the closest attraction point
888 bool found_any = false;
889 float min_angular_distance = std::numeric_limits<float>::max();
890 vec3 closest_point;
891
892 // Check each attraction point
893 for (const vec3 &point: attraction_points) {
894 // Calculate vector from vertex to attraction point
895 vec3 to_point = point - vertex;
896 float distance_to_point = to_point.magnitude();
897
898 // Skip if point is at the vertex or beyond look-ahead distance
899 if (distance_to_point < 1e-6f || distance_to_point > look_ahead_distance) {
900 continue;
901 }
902
903 // Normalize the direction to the point
904 vec3 direction_to_point = to_point;
905 direction_to_point.normalize();
906
907 // Calculate angle between look direction and direction to point
908 float cos_angle = axis * direction_to_point;
909
910 // Clamp to handle numerical precision issues
911 cos_angle = std::max(-1.0f, std::min(1.0f, cos_angle));
912
913 float angle = std::acos(cos_angle);
914
915 // Check if point is within the perception cone
916 if (angle <= half_angle_rad) {
917 found_any = true;
918
919 // Check if this is the closest to the centerline
920 if (angle < min_angular_distance) {
921 min_angular_distance = angle;
922 closest_point = point;
923 }
924 }
925 }
926
927 // If we found any attraction points, calculate the direction to the closest one
928 if (found_any) {
929 direction_to_closest = closest_point - vertex;
930 direction_to_closest.normalize();
931 return true;
932 }
933
934 return false;
935}
936
937bool PlantArchitecture::detectAttractionPointsInCone(const std::vector<helios::vec3> &attraction_points_input, const helios::vec3 &vertex, const helios::vec3 &look_direction, float look_ahead_distance, float half_angle_degrees,
938 helios::vec3 &direction_to_closest) const {
939
940 // Validate input parameters
941 if (attraction_points_input.empty()) {
942 return false;
943 }
944
945 if (look_ahead_distance <= 0.0f) {
946 if (printmessages) {
947 }
948 return false;
949 }
950
951 if (half_angle_degrees <= 0.0f || half_angle_degrees >= 180.0f) {
952 if (printmessages) {
953 }
954 return false;
955 }
956
957 // Convert half-angle to radians
958 float half_angle_rad = half_angle_degrees * M_PI / 180.0f;
959
960 // Normalize look direction
961 vec3 axis = look_direction;
962 axis.normalize();
963
964 // Variables to track the closest attraction point
965 bool found_any = false;
966 float min_angular_distance = std::numeric_limits<float>::max();
967 vec3 closest_point;
968
969 // Check each attraction point
970 for (const vec3 &point: attraction_points_input) {
971 // Calculate vector from vertex to attraction point
972 vec3 to_point = point - vertex;
973 float distance_to_point = to_point.magnitude();
974
975 // Skip if point is at the vertex or beyond look-ahead distance
976 if (distance_to_point <= 1e-6 || distance_to_point > look_ahead_distance) {
977 continue;
978 }
979
980 // Normalize the direction to the point
981 vec3 direction_to_point = to_point;
982 direction_to_point.normalize();
983
984 // Calculate angle between look direction and direction to point
985 float cos_angle = axis * direction_to_point;
986
987 // Clamp to handle numerical precision issues
988 cos_angle = std::max(-1.0f, std::min(1.0f, cos_angle));
989
990 float angle = std::acos(cos_angle);
991
992 // Check if point is within the perception cone
993 if (angle <= half_angle_rad) {
994 found_any = true;
995
996 // Check if this is the closest to the centerline
997 if (angle < min_angular_distance) {
998 min_angular_distance = angle;
999 closest_point = point;
1000 }
1001 }
1002 }
1003
1004 // If we found any attraction points, calculate the direction to the closest one
1005 if (found_any) {
1006 direction_to_closest = closest_point - vertex;
1007 direction_to_closest.normalize();
1008 return true;
1009 }
1010
1011 return false;
1012}
1013
1014int Shoot::appendPhytomer(float internode_radius, float internode_length_max, float internode_length_scale_factor_fraction, float leaf_scale_factor_fraction, const PhytomerParameters &phytomer_parameters) {
1015 auto shoot_tree_ptr = &plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree;
1016
1017 // Determine the parent internode and petiole axes for rotation of the new phytomer
1018 vec3 parent_internode_axis;
1019 vec3 parent_petiole_axis;
1020 vec3 internode_base_position;
1021 if (phytomers.empty()) {
1022 // very first phytomer on shoot
1023 if (parent_shoot_ID == -1) {
1024 // very first shoot of the plant
1025 parent_internode_axis = make_vec3(0, 0, 1);
1026 parent_petiole_axis = make_vec3(0, -1, 0);
1027 } else {
1028 // first phytomer of a new shoot
1029 assert(parent_shoot_ID < shoot_tree_ptr->size() && parent_node_index < shoot_tree_ptr->at(parent_shoot_ID)->phytomers.size());
1030 parent_internode_axis = shoot_tree_ptr->at(parent_shoot_ID)->phytomers.at(parent_node_index)->getInternodeAxisVector(1.f);
1031 // If the parent phytomer has no petioles, create a ghost petiole perpendicular to the internode
1032 if (shoot_tree_ptr->at(parent_shoot_ID)->phytomers.at(parent_node_index)->petiole_vertices.empty()) {
1033 parent_petiole_axis = cross(parent_internode_axis, make_vec3(0, 0, 1));
1034 if (parent_petiole_axis.magnitude() < 0.01f) {
1035 // Internode is nearly vertical
1036 parent_petiole_axis = make_vec3(0, 1, 0);
1037 }
1038 parent_petiole_axis.normalize();
1039 // Rotate ghost petiole by cumulative phyllotactic angle to match phyllotactic patterning
1040 float phyllotactic_angle = shoot_tree_ptr->at(parent_shoot_ID)->phytomers.at(parent_node_index)->internode_phyllotactic_angle;
1041 float cumulative_rotation = float(parent_node_index) * phyllotactic_angle;
1042 parent_petiole_axis = rotatePointAboutLine(parent_petiole_axis, make_vec3(0, 0, 0), parent_internode_axis, cumulative_rotation);
1043 } else {
1044 parent_petiole_axis = shoot_tree_ptr->at(parent_shoot_ID)->phytomers.at(parent_node_index)->getPetioleAxisVector(0.f, parent_petiole_index);
1045 }
1046 }
1047 internode_base_position = base_position;
1048 } else {
1049 // additional phytomer being added to an existing shoot
1050 parent_internode_axis = phytomers.back()->getInternodeAxisVector(1.f);
1051 // If the parent phytomer has no petioles, create a ghost petiole perpendicular to the internode
1052 if (phytomers.back()->petiole_vertices.empty()) {
1053 parent_petiole_axis = cross(parent_internode_axis, make_vec3(0, 0, 1));
1054 if (parent_petiole_axis.magnitude() < 0.01f) {
1055 // Internode is nearly vertical
1056 parent_petiole_axis = make_vec3(0, 1, 0);
1057 }
1058 parent_petiole_axis.normalize();
1059 // Rotate ghost petiole by cumulative phyllotactic angle to match phyllotactic patterning
1060 uint prev_phytomer_index = phytomers.size() - 1;
1061 float phyllotactic_angle = phytomers.back()->internode_phyllotactic_angle;
1062 float cumulative_rotation = float(prev_phytomer_index) * phyllotactic_angle;
1063 parent_petiole_axis = rotatePointAboutLine(parent_petiole_axis, make_vec3(0, 0, 0), parent_internode_axis, cumulative_rotation);
1064 } else {
1065 parent_petiole_axis = phytomers.back()->getPetioleAxisVector(0.f, 0);
1066 }
1067 internode_base_position = shoot_internode_vertices.back().back();
1068 }
1069
1070 std::shared_ptr<Phytomer> phytomer = std::make_shared<Phytomer>(phytomer_parameters, this, static_cast<uint>(phytomers.size()), parent_internode_axis, parent_petiole_axis, internode_base_position, this->base_rotation, internode_radius,
1071 internode_length_max, internode_length_scale_factor_fraction, leaf_scale_factor_fraction, rank, plantarchitecture_ptr, context_ptr);
1072 shoot_tree_ptr->at(ID)->phytomers.push_back(phytomer);
1073 phytomer = shoot_tree_ptr->at(ID)->phytomers.back(); // change to point to phytomer stored in shoot
1074
1075 // Initialize phytomer vegetative bud types and state
1076 for (auto &petiole: phytomer->axillary_vegetative_buds) {
1077 // sample the bud shoot type
1078 std::string child_shoot_type_label = sampleChildShootType();
1079 for (auto &vbud: petiole) {
1080 phytomer->setVegetativeBudState(BUD_DORMANT, vbud);
1081 vbud.shoot_type_label = child_shoot_type_label;
1082
1083 // if the shoot type does not require dormancy, bud should be set to active
1084 if (!shoot_parameters.growth_requires_dormancy) {
1085 if (plantarchitecture_ptr->carbon_model_enabled) {
1086 if (sampleVegetativeBudBreak_carb(phytomer->shoot_index.x)) {
1087 // randomly sample bud
1088 phytomer->setVegetativeBudState(BUD_ACTIVE, vbud);
1089 } else {
1090 phytomer->setVegetativeBudState(BUD_DEAD, vbud);
1091 }
1092 } else {
1093 if (sampleVegetativeBudBreak(phytomer->shoot_index.x)) {
1094 // randomly sample bud
1095 phytomer->setVegetativeBudState(BUD_ACTIVE, vbud);
1096 } else {
1097 phytomer->setVegetativeBudState(BUD_DEAD, vbud);
1098 }
1099 }
1100 }
1101 }
1102 }
1103
1104 // Initialize phytomer floral bud types and state
1105 uint petiole_index = 0;
1106 for (auto &petiole: phytomer->floral_buds) {
1107 uint bud_index = 0;
1108 for (auto &fbud: petiole) {
1109 // Set state of phytomer buds
1110 phytomer->setFloralBudState(BUD_DORMANT, fbud);
1111
1112 // if the shoot type does not require dormancy, bud should be set to active
1113 if (!shoot_parameters.flowers_require_dormancy && fbud.state != BUD_DEAD) {
1114 phytomer->setFloralBudState(BUD_ACTIVE, fbud);
1115 }
1116
1117 fbud.parent_index = petiole_index;
1118 fbud.bud_index = bud_index;
1119
1120 bud_index++;
1121 }
1122 petiole_index++;
1123 }
1124
1125 // Update the downstream leaf area for all upstream phytomers
1126 propagateDownstreamLeafArea(this, phytomer->shoot_index.x, phytomer->getLeafArea());
1127
1128 // Set output object data 'age'
1129 phytomer->age = 0;
1130 if (plantarchitecture_ptr->build_context_geometry_internode && context_ptr->doesObjectExist(internode_tube_objID)) {
1131 //\todo This really only needs to be done once when the shoot is first created.
1132 if (plantarchitecture_ptr->output_object_data.at("age")) {
1133 context_ptr->setObjectData(internode_tube_objID, "age", phytomer->age);
1134 }
1135 if (plantarchitecture_ptr->output_object_data.at("rank")) {
1136 context_ptr->setObjectData(internode_tube_objID, "rank", rank);
1137 }
1138 if (plantarchitecture_ptr->output_object_data.at("plantID")) {
1139 context_ptr->setObjectData(internode_tube_objID, "plantID", (int) plantID);
1140 }
1141 if (plantarchitecture_ptr->output_object_data.at("plant_name")) {
1142 context_ptr->setObjectData(internode_tube_objID, "plant_name", plantarchitecture_ptr->plant_instances.at(plantID).plant_name);
1143 }
1144 }
1145 if (plantarchitecture_ptr->build_context_geometry_petiole) {
1146 if (plantarchitecture_ptr->output_object_data.at("age")) {
1147 context_ptr->setObjectData(phytomer->petiole_objIDs, "age", phytomer->age);
1148 }
1149 if (plantarchitecture_ptr->output_object_data.at("rank")) {
1150 context_ptr->setObjectData(phytomer->petiole_objIDs, "rank", phytomer->rank);
1151 }
1152 if (plantarchitecture_ptr->output_object_data.at("plantID")) {
1153 context_ptr->setObjectData(phytomer->petiole_objIDs, "plantID", (int) plantID);
1154 }
1155 if (plantarchitecture_ptr->output_object_data.at("plant_name")) {
1156 context_ptr->setObjectData(phytomer->petiole_objIDs, "plant_name", plantarchitecture_ptr->plant_instances.at(plantID).plant_name);
1157 }
1158 }
1159 if (plantarchitecture_ptr->output_object_data.at("age")) {
1160 context_ptr->setObjectData(phytomer->leaf_objIDs, "age", phytomer->age);
1161 }
1162 if (plantarchitecture_ptr->output_object_data.at("rank")) {
1163 context_ptr->setObjectData(phytomer->leaf_objIDs, "rank", phytomer->rank);
1164 }
1165 if (plantarchitecture_ptr->output_object_data.at("plantID")) {
1166 context_ptr->setObjectData(phytomer->leaf_objIDs, "plantID", (int) plantID);
1167 }
1168 if (plantarchitecture_ptr->output_object_data.at("plant_name")) {
1169 context_ptr->setObjectData(phytomer->leaf_objIDs, "plant_name", plantarchitecture_ptr->plant_instances.at(plantID).plant_name);
1170 }
1171
1172 if (plantarchitecture_ptr->output_object_data.at("leafID")) {
1173 for (auto &petiole: phytomer->leaf_objIDs) {
1174 for (uint objID: petiole) {
1175 context_ptr->setObjectData(objID, "leafID", (int) objID);
1176 }
1177 }
1178 }
1179
1180 if (phytomer_parameters.phytomer_creation_function != nullptr) {
1181 phytomer_parameters.phytomer_creation_function(phytomer, current_node_number, this->parent_node_index, shoot_parameters.max_nodes.val(), plantarchitecture_ptr->plant_instances.at(plantID).current_age);
1182 }
1183
1184 // calculate fully expanded/elongated carbon costs
1185 if (plantarchitecture_ptr->carbon_model_enabled) {
1186 this->carbohydrate_pool_molC -= phytomer->calculatePhytomerConstructionCosts();
1187 }
1188
1189 return (int) phytomers.size() - 1;
1190}
1191
1192void Shoot::breakDormancy() {
1193 isdormant = false;
1194
1195 int phytomer_ind = 0;
1196 for (auto &phytomer: phytomers) {
1197 for (auto &petiole: phytomer->floral_buds) {
1198 for (auto &fbud: petiole) {
1199 if (fbud.state != BUD_DEAD) {
1200 phytomer->setFloralBudState(BUD_ACTIVE, fbud);
1201 }
1202 if (meristem_is_alive && fbud.isterminal) {
1203 phytomer->setFloralBudState(BUD_ACTIVE, fbud);
1204 }
1205 fbud.time_counter = 0;
1206 }
1207 }
1208 for (auto &petiole: phytomer->axillary_vegetative_buds) {
1209 for (auto &vbud: petiole) {
1210 if (vbud.state != BUD_DEAD) {
1211 if (plantarchitecture_ptr->carbon_model_enabled) {
1212 if (sampleVegetativeBudBreak_carb(phytomer_ind)) {
1213 // randomly sample bud
1214 phytomer->setVegetativeBudState(BUD_ACTIVE, vbud);
1215 } else {
1216 phytomer->setVegetativeBudState(BUD_DEAD, vbud);
1217 }
1218 } else {
1219 if (sampleVegetativeBudBreak(phytomer_ind)) {
1220 // randomly sample bud
1221 phytomer->setVegetativeBudState(BUD_ACTIVE, vbud);
1222 } else {
1223 phytomer->setVegetativeBudState(BUD_DEAD, vbud);
1224 }
1225 }
1226 }
1227 }
1228 }
1229
1230 phytomer->isdormant = false;
1231 phytomer_ind++;
1232 }
1233}
1234
1235void Shoot::makeDormant() {
1236 isdormant = true;
1237 dormancy_cycles++;
1238 nodes_this_season = 0;
1239
1240 for (auto &phytomer: phytomers) {
1241 for (auto &petiole: phytomer->floral_buds) {
1242 // all currently active lateral buds die at dormancy
1243 for (auto &fbud: petiole) {
1244 if (fbud.state != BUD_DORMANT) {
1245 phytomer->setFloralBudState(BUD_DEAD, fbud);
1246 }
1247 }
1248 }
1249 for (auto &petiole: phytomer->axillary_vegetative_buds) {
1250 for (auto &vbud: petiole) {
1251 if (vbud.state != BUD_DORMANT) {
1252 phytomer->setVegetativeBudState(BUD_DEAD, vbud);
1253 }
1254 }
1255 }
1256 if (!plantarchitecture_ptr->plant_instances.at(plantID).is_evergreen) {
1257 phytomer->removeLeaf();
1258 }
1259 phytomer->isdormant = true;
1260 }
1261
1262 if (meristem_is_alive && shoot_parameters.flowers_require_dormancy && shoot_parameters.max_terminal_floral_buds.val() > 0) {
1264 }
1265}
1266
1268 this->meristem_is_alive = false;
1269 this->phyllochron_counter = 0;
1270}
1271
1273 for (auto &phytomer: phytomers) {
1274 for (auto &petiole: phytomer->axillary_vegetative_buds) {
1275 for (auto &vbud: petiole) {
1276 phytomer->setVegetativeBudState(BUD_DEAD, vbud);
1277 }
1278 }
1279 }
1280}
1281
1283 int Nbuds = shoot_parameters.max_terminal_floral_buds.val();
1284 for (int bud = 0; bud < Nbuds; bud++) {
1285 FloralBud bud_new;
1286 bud_new.isterminal = true;
1287 bud_new.parent_index = 0;
1288 bud_new.bud_index = bud;
1289 bud_new.base_position = shoot_internode_vertices.back().back();
1290 float pitch_adjustment = 0;
1291 if (Nbuds > 1) {
1292 pitch_adjustment = deg2rad(30);
1293 }
1294 float yaw_adjustment = static_cast<float>(bud_new.bud_index) * 2.f * PI_F / float(Nbuds);
1295 //-0.25f * PI_F + bud_new.bud_index * 0.5f * PI_F / float(Nbuds);
1296 bud_new.base_rotation = make_AxisRotation(pitch_adjustment, yaw_adjustment, 0);
1297 bud_new.bending_axis = make_vec3(1, 0, 0);
1298
1299 phytomers.back()->floral_buds.push_back({bud_new});
1300 }
1301
1302 shoot_parameters.max_terminal_floral_buds.resample();
1303}
1304
1306 float shoot_volume = 0;
1307 for (const auto &phytomer: phytomers) {
1308 if (context_ptr->doesObjectExist(internode_tube_objID)) {
1309 shoot_volume += context_ptr->getTubeObjectVolume(internode_tube_objID);
1310 }
1311 }
1312 return shoot_volume;
1313}
1314
1316 float shoot_length = 0;
1317 for (const auto &phytomer: phytomers) {
1318 shoot_length += phytomer->getInternodeLength();
1319 }
1320 return shoot_length;
1321}
1322
1323void Shoot::updateShootNodes(bool update_context_geometry) {
1324 // make shoot origin consistent with parent shoot node position
1325 if (parent_shoot_ID >= 0) {
1326 // only if not the base shoot
1327
1328 auto parent_shoot = plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree.at(parent_shoot_ID);
1329
1330 const vec3 current_origin = shoot_internode_vertices.front().front();
1331 const vec3 updated_origin = parent_shoot->shoot_internode_vertices.at(this->parent_node_index).back();
1332 vec3 shift = updated_origin - current_origin;
1333
1334 // shift shoot based outward by the radius of the parent internode
1335 // shift += radial_outward_axis * parent_shoot->shoot_internode_radii.at(this->parent_node_index).back();
1336
1337 if (shift != nullorigin) {
1338 for (auto &phytomer: shoot_internode_vertices) {
1339 for (vec3 &node: phytomer) {
1340 node += shift;
1341 }
1342 }
1343 }
1344 }
1345
1346 if (update_context_geometry && plantarchitecture_ptr->build_context_geometry_internode && context_ptr->doesObjectExist(internode_tube_objID)) {
1347 context_ptr->setTubeRadii(internode_tube_objID, flatten(shoot_internode_radii));
1348 context_ptr->setTubeNodes(internode_tube_objID, flatten(shoot_internode_vertices));
1349 }
1350
1351 // update petiole/leaf positions
1352 for (int p = 0; p < phytomers.size(); p++) {
1353 vec3 petiole_base = shoot_internode_vertices.at(p).back();
1354 if (parent_shoot_ID >= 0) {
1355 // shift petiole base outward by the parent internode radius
1356 auto parent_shoot = plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree.at(parent_shoot_ID);
1357 // petiole_base += radial_outward_axis * parent_shoot->shoot_internode_radii.at(this->parent_node_index).back();
1358 }
1359 phytomers.at(p)->setPetioleBase(petiole_base);
1360 }
1361
1362 // update child shoot origins
1363 for (const auto &node: childIDs) {
1364 for (int child_shoot_ID: node.second) {
1365 plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree.at(child_shoot_ID)->updateShootNodes(update_context_geometry);
1366 }
1367 }
1368}
1369
1370helios::vec3 Shoot::getShootAxisVector(float shoot_fraction) const {
1371 uint phytomer_count = this->phytomers.size();
1372
1373 uint phytomer_index = 0;
1374 if (shoot_fraction > 0) {
1375 phytomer_index = std::ceil(shoot_fraction * float(phytomer_count)) - 1;
1376 }
1377
1378 assert(phytomer_index < phytomer_count);
1379
1380 return this->phytomers.at(phytomer_index)->getInternodeAxisVector(0.5);
1381}
1382
1383void Shoot::propagateDownstreamLeafArea(const Shoot *shoot, uint node_index, float leaf_area) {
1384 for (int i = node_index; i >= 0; i--) {
1385 shoot->phytomers.at(i)->downstream_leaf_area += leaf_area;
1386 shoot->phytomers.at(i)->downstream_leaf_area = std::max(0.f, shoot->phytomers.at(i)->downstream_leaf_area);
1387 }
1388
1389 if (shoot->parent_shoot_ID >= 0) {
1390 Shoot *parent_shoot = plantarchitecture_ptr->plant_instances.at(shoot->plantID).shoot_tree.at(shoot->parent_shoot_ID).get();
1391 propagateDownstreamLeafArea(parent_shoot, shoot->parent_node_index, leaf_area);
1392 }
1393}
1394
1395
1396float Shoot::sumShootLeafArea(uint start_node_index) const {
1397 if (start_node_index >= phytomers.size()) {
1398 helios_runtime_error("ERROR (Shoot::sumShootLeafArea): Start node index out of range.");
1399 }
1400
1401 float area = 0;
1402
1403 for (uint p = start_node_index; p < phytomers.size(); p++) {
1404 // sum up leaves directly connected to this shoot
1405 auto phytomer = phytomers.at(p);
1406 for (auto &petiole: phytomer->leaf_objIDs) {
1407 for (uint objID: petiole) {
1408 if (context_ptr->doesObjectExist(objID)) {
1409 area += context_ptr->getObjectArea(objID);
1410 }
1411 }
1412 }
1413
1414 // call recursively for child shoots
1415 if (childIDs.find(p) != childIDs.end()) {
1416 for (int child_shoot_ID: childIDs.at(p)) {
1417 area += plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree.at(child_shoot_ID)->sumShootLeafArea(0);
1418 }
1419 }
1420 }
1421
1422 return area;
1423}
1424
1425
1426float Shoot::sumChildVolume(uint start_node_index) const {
1427 if (start_node_index >= phytomers.size()) {
1428 helios_runtime_error("ERROR (Shoot::sumChildVolume): Start node index out of range.");
1429 }
1430
1431 float volume = 0;
1432
1433 for (uint p = start_node_index; p < phytomers.size(); p++) {
1434 // call recursively for child shoots
1435 if (childIDs.find(p) != childIDs.end()) {
1436 for (int child_shoot_ID: childIDs.at(p)) {
1437 volume += plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree.at(child_shoot_ID)->calculateShootInternodeVolume();
1438 }
1439 }
1440 }
1441
1442 return volume;
1443}
1444
1445Phytomer::Phytomer(const PhytomerParameters &params, Shoot *parent_shoot, uint phytomer_index, const helios::vec3 &parent_internode_axis, const helios::vec3 &parent_petiole_axis, helios::vec3 internode_base_origin,
1446 const AxisRotation &shoot_base_rotation, float internode_radius, float internode_length_max, float internode_length_scale_factor_fraction, float leaf_scale_factor_fraction, uint rank, PlantArchitecture *plantarchitecture_ptr,
1447 helios::Context *context_ptr) : rank(rank), context_ptr(context_ptr), plantarchitecture_ptr(plantarchitecture_ptr) {
1448 this->phytomer_parameters = params;
1449 // note this needs to be an assignment operation not a copy in order to re-randomize all the parameters
1450
1451 ShootParameters parent_shoot_parameters = parent_shoot->shoot_parameters;
1452
1453 this->internode_radius_initial = internode_radius;
1454 this->internode_length_max = internode_length_max;
1455 this->shoot_index = make_int3(phytomer_index, parent_shoot->current_node_number, parent_shoot_parameters.max_nodes.val());
1456 //.x is the index of the phytomer along the shoot, .y is the current number of phytomers on the parent shoot, .z is the maximum number of phytomers on the parent shoot.
1457 this->rank = parent_shoot->rank;
1458 this->plantID = parent_shoot->plantID;
1459 this->parent_shoot_ID = parent_shoot->ID;
1460 this->parent_shoot_ptr = parent_shoot;
1461
1462 bool build_context_geometry_internode = plantarchitecture_ptr->build_context_geometry_internode;
1463 bool build_context_geometry_petiole = plantarchitecture_ptr->build_context_geometry_petiole;
1464 bool build_context_geometry_peduncle = plantarchitecture_ptr->build_context_geometry_peduncle;
1465
1466 // if( internode_radius==0.f || internode_length_max==0.f || parent_shoot_parameters.internode_radius_max.val()==0.f ){
1467 // build_context_geometry_internode = false;
1468 // }
1469
1470 // Number of longitudinal segments for internode and petiole
1471 // if Ndiv=0, use Ndiv=1 (but don't add any primitives to Context)
1472 uint Ndiv_internode_length = std::max(uint(1), phytomer_parameters.internode.length_segments);
1473 uint Ndiv_internode_radius = std::max(uint(3), phytomer_parameters.internode.radial_subdivisions);
1474 uint Ndiv_petiole_length = std::max(uint(1), phytomer_parameters.petiole.length_segments);
1475 uint Ndiv_petiole_radius = std::max(uint(3), phytomer_parameters.petiole.radial_subdivisions);
1476
1477 // Flags to determine whether internode geometry should be built in the Context. Not building all geometry can save memory and computation time.
1478 if (phytomer_parameters.internode.length_segments == 0 || phytomer_parameters.internode.radial_subdivisions < 3) {
1479 build_context_geometry_internode = false;
1480 }
1481 if (phytomer_parameters.petiole.length_segments == 0 || phytomer_parameters.petiole.radial_subdivisions < 3) {
1482 build_context_geometry_petiole = false;
1483 }
1484
1485 if (phytomer_parameters.petiole.petioles_per_internode == 0) {
1486 // Allow 0 petioles per internode, but ensure no leaves are created
1487 build_context_geometry_petiole = false;
1488 phytomer_parameters.leaf.leaves_per_petiole = 0;
1489 }
1490
1491 if (phytomer_parameters.petiole.petioles_per_internode < 0) {
1492 helios_runtime_error("ERROR (PlantArchitecture::Phytomer): Number of petioles per internode cannot be negative.");
1493 }
1494
1495 current_internode_scale_factor = internode_length_scale_factor_fraction;
1496 current_leaf_scale_factor.resize(phytomer_parameters.petiole.petioles_per_internode);
1497 std::fill(current_leaf_scale_factor.begin(), current_leaf_scale_factor.end(), leaf_scale_factor_fraction);
1498
1499 if (internode_radius == 0.f) {
1500 internode_radius = MIN_TUBE_RADIUS_FOR_GEOMETRY;
1501 }
1502
1503 // Initialize internode variables
1504 float internode_length = internode_length_scale_factor_fraction * internode_length_max;
1505 float dr_internode = internode_length / float(phytomer_parameters.internode.length_segments);
1506 float dr_internode_max = internode_length_max / float(phytomer_parameters.internode.length_segments);
1507 std::vector<vec3> phytomer_internode_vertices;
1508 std::vector<float> phytomer_internode_radii;
1509 phytomer_internode_vertices.resize(Ndiv_internode_length + 1);
1510 phytomer_internode_vertices.at(0) = internode_base_origin;
1511 phytomer_internode_radii.resize(Ndiv_internode_length + 1);
1512 phytomer_internode_radii.at(0) = internode_radius;
1513 internode_pitch = deg2rad(phytomer_parameters.internode.pitch.val());
1514 phytomer_parameters.internode.pitch.resample();
1515 internode_phyllotactic_angle = deg2rad(phytomer_parameters.internode.phyllotactic_angle.val());
1516 phytomer_parameters.internode.phyllotactic_angle.resample();
1517
1518 // initialize petiole variables
1519 petiole_length.resize(phytomer_parameters.petiole.petioles_per_internode);
1520 petiole_vertices.resize(phytomer_parameters.petiole.petioles_per_internode);
1521 petiole_radii.resize(phytomer_parameters.petiole.petioles_per_internode);
1522
1523 // initialize peduncle vertices and radii storage (will be resized when floral buds are added)
1524 peduncle_vertices.resize(phytomer_parameters.petiole.petioles_per_internode);
1525 peduncle_radii.resize(phytomer_parameters.petiole.petioles_per_internode);
1526 peduncle_length.resize(phytomer_parameters.petiole.petioles_per_internode);
1527 peduncle_radius.resize(phytomer_parameters.petiole.petioles_per_internode);
1528 peduncle_pitch.resize(phytomer_parameters.petiole.petioles_per_internode);
1529 peduncle_curvature.resize(phytomer_parameters.petiole.petioles_per_internode);
1530 petiole_pitch.resize(phytomer_parameters.petiole.petioles_per_internode);
1531 petiole_curvature.resize(phytomer_parameters.petiole.petioles_per_internode);
1532 petiole_taper.resize(phytomer_parameters.petiole.petioles_per_internode);
1533 petiole_axis_initial.resize(phytomer_parameters.petiole.petioles_per_internode);
1534 petiole_rotation_axis.resize(phytomer_parameters.petiole.petioles_per_internode);
1535 std::vector<float> dr_petiole(phytomer_parameters.petiole.petioles_per_internode);
1536 std::vector<float> dr_petiole_max(phytomer_parameters.petiole.petioles_per_internode);
1537 for (int p = 0; p < phytomer_parameters.petiole.petioles_per_internode; p++) {
1538 petiole_vertices.at(p).resize(Ndiv_petiole_length + 1);
1539 petiole_radii.at(p).resize(Ndiv_petiole_length + 1);
1540
1541 petiole_length.at(p) = leaf_scale_factor_fraction * phytomer_parameters.petiole.length.val();
1542 if (petiole_length.at(p) <= 0.f) {
1543 petiole_length.at(p) = MIN_TUBE_LENGTH_FOR_GEOMETRY;
1544 }
1545 dr_petiole.at(p) = petiole_length.at(p) / float(phytomer_parameters.petiole.length_segments);
1546 dr_petiole_max.at(p) = phytomer_parameters.petiole.length.val() / float(phytomer_parameters.petiole.length_segments);
1547
1548 petiole_radii.at(p).at(0) = leaf_scale_factor_fraction * phytomer_parameters.petiole.radius.val();
1549 if (petiole_radii.at(p).at(0) <= 0.f) {
1550 petiole_radii.at(p).at(0) = MIN_TUBE_RADIUS_FOR_GEOMETRY;
1551 }
1552 }
1553 phytomer_parameters.petiole.length.resample();
1554 // Always initialize petiole_objIDs vector for potential lazy creation later
1555 petiole_objIDs.resize(phytomer_parameters.petiole.petioles_per_internode);
1556
1557 // initialize leaf variables
1558 leaf_bases.resize(phytomer_parameters.petiole.petioles_per_internode);
1559 leaf_objIDs.resize(phytomer_parameters.petiole.petioles_per_internode);
1560 leaf_size_max.resize(phytomer_parameters.petiole.petioles_per_internode);
1561 leaf_rotation.resize(phytomer_parameters.petiole.petioles_per_internode);
1562 int leaves_per_petiole = phytomer_parameters.leaf.leaves_per_petiole.val();
1563 float leaflet_offset_val = clampOffset(leaves_per_petiole, phytomer_parameters.leaf.leaflet_offset.val());
1564 phytomer_parameters.leaf.leaves_per_petiole.resample();
1565 for (uint petiole = 0; petiole < phytomer_parameters.petiole.petioles_per_internode; petiole++) {
1566 leaf_size_max.at(petiole).resize(leaves_per_petiole);
1567 leaf_rotation.at(petiole).resize(leaves_per_petiole);
1568 }
1569
1570 internode_colors.resize(Ndiv_internode_length + 1);
1571 internode_colors.at(0) = phytomer_parameters.internode.color;
1572 petiole_colors.resize(Ndiv_petiole_length + 1);
1573 petiole_colors.at(0) = phytomer_parameters.petiole.color;
1574
1575 vec3 internode_axis = parent_internode_axis;
1576
1577 vec3 petiole_rotation_axis = cross(parent_internode_axis, parent_petiole_axis);
1578 if (petiole_rotation_axis == make_vec3(0, 0, 0)) {
1579 petiole_rotation_axis = make_vec3(1, 0, 0);
1580 }
1581
1582 // Debug output for first phytomer construction
1583 if (phytomer_index == 0) {
1584 }
1585
1586 if (phytomer_index == 0) { // if this is the first phytomer along a shoot, apply the origin rotation about the parent axis
1587
1588 // internode pitch rotation for phytomer base
1589 if (internode_pitch != 0.f) {
1590 if (phytomer_index == 0) {
1591 }
1592 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, 0.5f * internode_pitch);
1593 if (phytomer_index == 0) {
1594 }
1595 }
1596
1597 float roll_nudge = 0.f;
1598 //\todo Not clear if this is still needed. It causes problems when you want to plant base roll to be exactly 0.
1599 // if( shoot_base_rotation.roll/180.f == floor(shoot_base_rotation.roll/180.f) ) {
1600 // roll_nudge = 0.2;
1601 // }
1602
1603 if (phytomer_index == 0) {
1604 }
1605
1606 if (shoot_base_rotation.roll != 0.f || roll_nudge != 0.f) {
1607 if (phytomer_index == 0) {
1608 }
1609 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll + roll_nudge);
1610 // small additional rotation is to make sure the petiole is not exactly vertical
1611 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll + roll_nudge);
1612 if (phytomer_index == 0) {
1613 }
1614 }
1615
1616 vec3 base_pitch_axis = -1 * cross(parent_internode_axis, parent_petiole_axis);
1617
1618 // internode pitch rotation for shoot base rotation
1619 if (shoot_base_rotation.pitch != 0.f) {
1620 if (phytomer_index == 0) {
1621 }
1622 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1623 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1624 if (phytomer_index == 0) {
1625 }
1626 }
1627
1628 // internode yaw rotation for shoot base rotation
1629 if (shoot_base_rotation.yaw != 0) {
1630 if (phytomer_index == 0) {
1631 }
1632 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1633 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1634 if (phytomer_index == 0) {
1635 }
1636 }
1637
1638 parent_shoot->radial_outward_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, 0.5f * PI_F);
1639
1640 // if( parent_shoot->parent_shoot_ID>=0 ) { //if this is not the first shoot on the plant (i.e. it has a parent shoot
1641 // auto parent_of_parent_shoot = plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree.at(parent_shoot->parent_shoot_ID);
1642 // phytomer_internode_vertices.at(0) += parent_shoot->radial_outward_axis * parent_of_parent_shoot->shoot_internode_radii.at(parent_shoot->parent_node_index).back();
1643 // }
1644 } else {
1645 // internode pitch rotation for phytomer base
1646 if (internode_pitch != 0) {
1647 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, -1.25f * internode_pitch);
1648 }
1649 }
1650
1651 vec3 shoot_bending_axis = cross(internode_axis, make_vec3(0, 0, 1));
1652
1653 internode_axis.normalize();
1654 if (internode_axis == make_vec3(0, 0, 1)) {
1655 shoot_bending_axis = make_vec3(0, 1, 0);
1656 }
1657
1658 // Store collision detection and attraction points parameters for later use (after all natural rotations)
1659 vec3 collision_optimal_direction;
1660 bool collision_detection_active = false;
1661 vec3 attraction_direction;
1662 bool attraction_active = false;
1663 bool obstacle_found = false;
1664
1665 // Calculate collision avoidance direction if collision detection is enabled
1666 collision_optimal_direction = calculateCollisionAvoidanceDirection(internode_base_origin, internode_axis, collision_detection_active);
1667
1668 // Calculate attraction point direction if attraction points are enabled
1669 attraction_direction = calculateAttractionPointDirection(internode_base_origin, internode_axis, attraction_active);
1670
1671 // Solid obstacle avoidance is now handled inside the segment creation loop
1672
1673 // Resize perturbation vectors to capture stochastic state for XML reconstruction
1674 internode_curvature_perturbations.resize(Ndiv_internode_length);
1675 internode_yaw_perturbations.resize(Ndiv_internode_length);
1676
1677 // create internode tube
1678 for (int inode_segment = 1; inode_segment <= Ndiv_internode_length; inode_segment++) {
1679 // apply curvature and tortuosity
1680 if ((fabs(parent_shoot->gravitropic_curvature) > 0 || parent_shoot_parameters.tortuosity.val() > 0) && shoot_index.x > 0) {
1681 // note: curvature is not applied to the first phytomer because if scaling is performed in the phytomer creation function it messes things up
1682
1683 float current_curvature_fact = 0.5f - internode_axis.z / 2.f;
1684 if (internode_axis.z < 0) {
1685 current_curvature_fact *= 2.f;
1686 }
1687
1688 float dt = dr_internode_max / float(Ndiv_internode_length);
1689
1690 parent_shoot->curvature_perturbation += -0.5f * parent_shoot->curvature_perturbation * dt + parent_shoot_parameters.tortuosity.val() * context_ptr->randn() * sqrt(dt);
1691 internode_curvature_perturbations[inode_segment - 1] = parent_shoot->curvature_perturbation;
1692 float curvature_angle = deg2rad((parent_shoot->gravitropic_curvature * current_curvature_fact * dr_internode_max + parent_shoot->curvature_perturbation));
1693 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, shoot_bending_axis, curvature_angle);
1694
1695 parent_shoot->yaw_perturbation += -0.5f * parent_shoot->yaw_perturbation * dt + parent_shoot_parameters.tortuosity.val() * context_ptr->randn() * sqrt(dt);
1696 internode_yaw_perturbations[inode_segment - 1] = parent_shoot->yaw_perturbation;
1697 float yaw_angle = deg2rad((parent_shoot->yaw_perturbation));
1698 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, make_vec3(0, 0, 1), yaw_angle);
1699 }
1700
1701 // Apply solid obstacle avoidance after natural rotations but before soft collision avoidance
1702 vec3 current_position = phytomer_internode_vertices.at(inode_segment - 1);
1703 obstacle_found = applySolidObstacleAvoidance(current_position, internode_axis);
1704
1705 // Apply direction guidance after all natural rotations are complete
1706 // New approach: Blend hard obstacle avoidance with attraction to maintain surface attraction
1707
1708 vec3 final_direction = internode_axis; // Start with current direction (includes hard obstacle avoidance if applied)
1709
1710 if (attraction_active) {
1711 // Always apply attraction points if they're found
1712 float attraction_weight = plantarchitecture_ptr->attraction_weight;
1713
1714 if (obstacle_found) {
1715 // When hard obstacles are present, reduce attraction influence to allow obstacle avoidance
1716 // but maintain some attraction to keep plant near surface
1717 attraction_weight *= plantarchitecture_ptr->attraction_obstacle_reduction_factor; // Reduce attraction when avoiding hard obstacles
1718 }
1719
1720 // Blend current direction (which may include obstacle avoidance) with attraction direction
1721 final_direction = (1.0f - attraction_weight) * final_direction + attraction_weight * attraction_direction;
1722 final_direction.normalize();
1723
1724 // Mark that attraction guidance was applied
1725 plantarchitecture_ptr->collision_avoidance_applied = true;
1726
1727 } else if (collision_detection_active && !obstacle_found) {
1728 // No attraction points found and no hard obstacles - fall back to soft collision avoidance
1729 float inertia_weight = plantarchitecture_ptr->collision_inertia_weight;
1730
1731 // Blend natural direction with optimal collision avoidance direction
1732 final_direction = inertia_weight * final_direction + (1.0f - inertia_weight) * collision_optimal_direction;
1733 final_direction.normalize();
1734
1735 // Mark that collision avoidance was applied this timestep
1736 plantarchitecture_ptr->collision_avoidance_applied = true;
1737 }
1738
1739 if (obstacle_found) {
1740 // Mark that hard obstacle avoidance was applied
1741 plantarchitecture_ptr->collision_avoidance_applied = true;
1742 }
1743
1744 // Update the internode axis with the final blended direction
1745 internode_axis = final_direction;
1746
1747 // vec3 displacement = dr_internode * internode_axis;
1748 // // Ensure minimum coordinate-wise displacement to avoid floating-point precision issues
1749 // if (fabs(displacement.x) < 1e-5f && fabs(displacement.y) < 1e-5f) {
1750 // // If both x and y displacements are tiny, add small perturbation to avoid degenerate geometry
1751 // if (fabs(internode_axis.z) > 0.9f) {
1752 // // Nearly vertical - add horizontal perturbation
1753 // displacement.x = (internode_axis.x >= 0) ? 1e-5f : -1e-5f;
1754 // } else {
1755 // // Not vertical - add z perturbation
1756 // displacement.z = (internode_axis.z >= 0) ? 1e-5f : -1e-5f;
1757 // }
1758 // }
1759 // phytomer_internode_vertices.at(inode_segment) = phytomer_internode_vertices.at(inode_segment - 1) + displacement;
1760
1761 phytomer_internode_vertices.at(inode_segment) = phytomer_internode_vertices.at(inode_segment - 1) + dr_internode * internode_axis;
1762
1763 phytomer_internode_radii.at(inode_segment) = internode_radius;
1764 internode_colors.at(inode_segment) = phytomer_parameters.internode.color;
1765 }
1766
1767 if (shoot_index.x == 0) {
1768 // first phytomer on shoot
1769 parent_shoot_ptr->shoot_internode_vertices.push_back(phytomer_internode_vertices);
1770 parent_shoot_ptr->shoot_internode_radii.push_back(phytomer_internode_radii);
1771 } else {
1772 // if not the first phytomer on shoot, don't insert the first node because it's already defined on the previous phytomer
1773 parent_shoot_ptr->shoot_internode_vertices.emplace_back(phytomer_internode_vertices.begin() + 1, phytomer_internode_vertices.end());
1774 parent_shoot_ptr->shoot_internode_radii.emplace_back(phytomer_internode_radii.begin() + 1, phytomer_internode_radii.end());
1775 }
1776
1777 // build internode context geometry
1778 if (build_context_geometry_internode) {
1779 // calculate texture coordinates
1780 float texture_repeat_length = 0.25f; // meters
1781 float length = 0; // shoot length prior to this phytomer
1782 for (auto &phytomer: parent_shoot_ptr->phytomers) {
1783 length += phytomer->internode_length_max;
1784 }
1785 std::vector<float> uv_y(phytomer_internode_vertices.size());
1786 float dy = internode_length_max / float(uv_y.size() - 1);
1787 for (int j = 0; j < uv_y.size(); j++) {
1788 uv_y.at(j) = (length + j * dy) / texture_repeat_length - std::floor((length + j * dy) / texture_repeat_length);
1789 }
1790
1791 // Resolve internode texture path (allows users to specify simple paths like "OliveBark.jpg")
1792 std::string resolved_internode_texture = PlantArchitecture::resolveTextureFile(phytomer_parameters.internode.image_texture);
1793
1794 if (!context_ptr->doesObjectExist(parent_shoot->internode_tube_objID)) {
1795 // first internode on shoot
1796 if (!resolved_internode_texture.empty()) {
1797 parent_shoot->internode_tube_objID = context_ptr->addTubeObject(Ndiv_internode_radius, phytomer_internode_vertices, phytomer_internode_radii, resolved_internode_texture.c_str(), uv_y);
1798 } else {
1799 parent_shoot->internode_tube_objID = context_ptr->addTubeObject(Ndiv_internode_radius, phytomer_internode_vertices, phytomer_internode_radii, internode_colors);
1800 }
1801 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(parent_shoot->internode_tube_objID), "object_label", "shoot");
1802 } else {
1803 // appending internode to shoot
1804 for (int inode_segment = 1; inode_segment <= Ndiv_internode_length; inode_segment++) {
1805 if (!resolved_internode_texture.empty()) {
1806 context_ptr->appendTubeSegment(parent_shoot->internode_tube_objID, phytomer_internode_vertices.at(inode_segment), phytomer_internode_radii.at(inode_segment), resolved_internode_texture.c_str(),
1807 {uv_y.at(inode_segment - 1), uv_y.at(inode_segment)});
1808 } else {
1809 context_ptr->appendTubeSegment(parent_shoot->internode_tube_objID, phytomer_internode_vertices.at(inode_segment), phytomer_internode_radii.at(inode_segment), internode_colors.at(inode_segment));
1810 }
1811 }
1812 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(parent_shoot->internode_tube_objID), "object_label", "shoot");
1813 }
1814 }
1815
1816 //--- create petiole ---//
1817
1818 for (int petiole = 0; petiole < phytomer_parameters.petiole.petioles_per_internode; petiole++) {
1819 // looping over petioles
1820
1821 vec3 petiole_axis = internode_axis;
1822
1823 // petiole pitch rotation
1824 // Check if this is the last phytomer on a shoot that has reached max_nodes
1825 if (shoot_index.y + 1 == shoot_index.z) {
1826 // Last phytomer on shoot - set petiole pitch to 0
1827 petiole_pitch.at(petiole) = 0.0f;
1828 } else {
1829 // Normal phytomer - use standard pitch calculation
1830 petiole_pitch.at(petiole) = deg2rad(phytomer_parameters.petiole.pitch.val());
1831 phytomer_parameters.petiole.pitch.resample();
1832 if (fabs(petiole_pitch.at(petiole)) < deg2rad(5.f)) {
1833 petiole_pitch.at(petiole) = deg2rad(5.f);
1834 }
1835 }
1836 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, petiole_rotation_axis, std::abs(petiole_pitch.at(petiole)));
1837
1838 // petiole yaw rotation
1839 if (phytomer_index != 0 && internode_phyllotactic_angle != 0) {
1840 // not first phytomer along shoot
1841 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, internode_axis, internode_phyllotactic_angle);
1842 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, internode_axis, internode_phyllotactic_angle);
1843 }
1844
1845 // petiole curvature
1846 petiole_curvature.at(petiole) = phytomer_parameters.petiole.curvature.val();
1847 phytomer_parameters.petiole.curvature.resample();
1848
1849 vec3 petiole_rotation_axis_actual = petiole_rotation_axis;
1850 vec3 petiole_axis_actual = petiole_axis;
1851
1852 if (petiole > 0) {
1853 float budrot = float(petiole) * 2.f * PI_F / float(phytomer_parameters.petiole.petioles_per_internode);
1854 petiole_axis_actual = rotatePointAboutLine(petiole_axis_actual, nullorigin, internode_axis, budrot);
1855 petiole_rotation_axis_actual = rotatePointAboutLine(petiole_rotation_axis_actual, nullorigin, internode_axis, budrot);
1856 }
1857
1858 // Store true initial vectors before collision avoidance and curvature application
1859 this->petiole_axis_initial.at(petiole) = petiole_axis_actual;
1860 this->petiole_rotation_axis.at(petiole) = petiole_rotation_axis_actual;
1861
1862 // Apply collision avoidance for petiole direction (if enabled)
1863 vec3 collision_optimal_petiole_direction;
1864 bool petiole_collision_active = false;
1865
1866 if (plantarchitecture_ptr->petiole_collision_detection_enabled) {
1867 collision_optimal_petiole_direction = calculatePetioleCollisionAvoidanceDirection(phytomer_internode_vertices.back(), // petiole base position
1868 petiole_axis_actual, petiole_collision_active);
1869 }
1870
1871 if (petiole_collision_active) {
1872 float inertia_weight = plantarchitecture_ptr->collision_inertia_weight;
1873 vec3 natural_petiole_direction = petiole_axis_actual;
1874
1875 // Blend natural petiole direction with optimal direction
1876 // inertia = 1.0: use natural direction (no collision avoidance)
1877 // inertia = 0.0: use optimal direction (full collision avoidance)
1878 petiole_axis_actual = inertia_weight * natural_petiole_direction + (1.0f - inertia_weight) * collision_optimal_petiole_direction;
1879 petiole_axis_actual.normalize();
1880
1881 // Adjust petiole curvature to bend toward optimal direction
1882 // Calculate desired bending direction perpendicular to natural petiole axis
1883 vec3 bending_direction = collision_optimal_petiole_direction - (collision_optimal_petiole_direction * natural_petiole_direction) * natural_petiole_direction;
1884
1885 if (bending_direction.magnitude() > 1e-6f) {
1886 bending_direction.normalize();
1887
1888 // Project bending direction onto petiole rotation plane to determine curvature adjustment
1889 // The rotation axis is perpendicular to both natural direction and bending direction
1890 vec3 curvature_axis = cross(natural_petiole_direction, bending_direction);
1891
1892 if (curvature_axis.magnitude() > 1e-6f) {
1893 curvature_axis.normalize();
1894
1895 // Calculate desired curvature angle based on angular deviation
1896 float angular_deviation = acosf(std::max(-1.0f, std::min(1.0f, collision_optimal_petiole_direction * natural_petiole_direction)));
1897
1898 // Convert to degrees and scale by collision strength
1899 float desired_curvature_deg = rad2deg(angular_deviation) * (1.0f - inertia_weight);
1900
1901 // Determine if curvature should be positive or negative based on rotation axis alignment
1902 float curvature_sign = (curvature_axis * petiole_rotation_axis_actual > 0) ? 1.0f : -1.0f;
1903
1904 // Apply additional curvature for collision avoidance
1905 petiole_curvature.at(petiole) += curvature_sign * desired_curvature_deg * 0.5f; // scale factor to prevent excessive bending
1906 }
1907 }
1908 }
1909
1910 // Sample taper once and store (avoids resampling each iteration which was a bug)
1911 petiole_taper.at(petiole) = phytomer_parameters.petiole.taper.val();
1912 phytomer_parameters.petiole.taper.resample();
1913
1914 petiole_vertices.at(petiole).at(0) = phytomer_internode_vertices.back();
1915
1916 for (int j = 1; j <= Ndiv_petiole_length; j++) {
1917 if (fabs(petiole_curvature.at(petiole)) > 0) {
1918 petiole_axis_actual = rotatePointAboutLine(petiole_axis_actual, nullorigin, petiole_rotation_axis_actual, -deg2rad(petiole_curvature.at(petiole) * dr_petiole_max.at(petiole)));
1919 }
1920
1921 petiole_vertices.at(petiole).at(j) = petiole_vertices.at(petiole).at(j - 1) + dr_petiole.at(petiole) * petiole_axis_actual;
1922
1923 petiole_radii.at(petiole).at(j) = leaf_scale_factor_fraction * phytomer_parameters.petiole.radius.val() * (1.f - petiole_taper.at(petiole) / float(Ndiv_petiole_length) * float(j));
1924 petiole_colors.at(j) = phytomer_parameters.petiole.color;
1925
1926 assert(!std::isnan(petiole_vertices.at(petiole).at(j).x) && std::isfinite(petiole_vertices.at(petiole).at(j).x));
1927 assert(!std::isnan(petiole_radii.at(petiole).at(j)) && std::isfinite(petiole_radii.at(petiole).at(j)));
1928 }
1929
1930 if (build_context_geometry_petiole) {
1931 petiole_objIDs.at(petiole) = makeTubeFromCones(Ndiv_petiole_radius, petiole_vertices.at(petiole), petiole_radii.at(petiole), petiole_colors, context_ptr);
1932 if (!petiole_objIDs.at(petiole).empty()) {
1933 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(petiole_objIDs.at(petiole)), "object_label", "petiole");
1934 }
1935 }
1936
1937 //--- create buds ---//
1938
1939 std::vector<VegetativeBud> vegetative_buds_new;
1940 vegetative_buds_new.resize(phytomer_parameters.internode.max_vegetative_buds_per_petiole.val());
1941 phytomer_parameters.internode.max_vegetative_buds_per_petiole.resample();
1942
1943 axillary_vegetative_buds.push_back(vegetative_buds_new);
1944
1945 std::vector<FloralBud> floral_buds_new;
1946 floral_buds_new.resize(phytomer_parameters.internode.max_floral_buds_per_petiole.val());
1947 phytomer_parameters.internode.max_floral_buds_per_petiole.resample();
1948
1949 uint index = 0;
1950 for (auto &fbud: floral_buds_new) {
1951 fbud.bud_index = index;
1952 fbud.parent_index = petiole;
1953 float pitch_adjustment = fbud.bud_index * 0.1f * PI_F / float(axillary_vegetative_buds.size());
1954 float yaw_adjustment = -0.25f * PI_F + fbud.bud_index * 0.5f * PI_F / float(axillary_vegetative_buds.size());
1955 fbud.base_rotation = make_AxisRotation(pitch_adjustment, yaw_adjustment, 0);
1956 fbud.base_position = phytomer_internode_vertices.back();
1957 fbud.bending_axis = shoot_bending_axis;
1958 index++;
1959 }
1960
1961 floral_buds.push_back(floral_buds_new);
1962
1963 //--- create leaves ---//
1964
1965 if (phytomer_parameters.leaf.prototype.prototype_function == nullptr) {
1966 helios_runtime_error("ERROR (PlantArchitecture::Phytomer): Leaf prototype function was not defined for shoot type " + parent_shoot->shoot_type_label + ".");
1967 }
1968
1969 vec3 petiole_tip_axis = getPetioleAxisVector(1.f, petiole);
1970
1971 // Create unique leaf prototypes for each shoot type so we can simply copy them for each leaf
1972 assert(phytomer_parameters.leaf.prototype.unique_prototype_identifier != 0);
1973 if (phytomer_parameters.leaf.prototype.unique_prototypes > 0 &&
1974 plantarchitecture_ptr->unique_leaf_prototype_objIDs.find(phytomer_parameters.leaf.prototype.unique_prototype_identifier) == plantarchitecture_ptr->unique_leaf_prototype_objIDs.end()) {
1975 plantarchitecture_ptr->unique_leaf_prototype_objIDs[phytomer_parameters.leaf.prototype.unique_prototype_identifier].resize(phytomer_parameters.leaf.prototype.unique_prototypes);
1976 for (int prototype = 0; prototype < phytomer_parameters.leaf.prototype.unique_prototypes; prototype++) {
1977 for (int leaf = 0; leaf < leaves_per_petiole; leaf++) {
1978 float ind_from_tip = float(leaf) - float(leaves_per_petiole - 1) / 2.f;
1979 uint objID_leaf = phytomer_parameters.leaf.prototype.prototype_function(context_ptr, &phytomer_parameters.leaf.prototype, ind_from_tip);
1980 if (phytomer_parameters.leaf.prototype.prototype_function == GenericLeafPrototype) {
1981 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(objID_leaf), "object_label", "leaf");
1982 }
1983 plantarchitecture_ptr->unique_leaf_prototype_objIDs.at(phytomer_parameters.leaf.prototype.unique_prototype_identifier).at(prototype).push_back(objID_leaf);
1984 std::vector<uint> petiolule_UUIDs = context_ptr->filterPrimitivesByData(context_ptr->getObjectPrimitiveUUIDs(objID_leaf), "object_label", "petiolule");
1985 context_ptr->setPrimitiveColor(petiolule_UUIDs, phytomer_parameters.petiole.color);
1986 context_ptr->hideObject(objID_leaf);
1987 }
1988 }
1989 }
1990
1991 for (int leaf = 0; leaf < leaves_per_petiole; leaf++) {
1992 float ind_from_tip = float(leaf) - float(leaves_per_petiole - 1) / 2.f;
1993
1994 uint objID_leaf;
1995 if (phytomer_parameters.leaf.prototype.unique_prototypes > 0) {
1996 // copy the existing prototype
1997 int prototype = context_ptr->randu(0, phytomer_parameters.leaf.prototype.unique_prototypes - 1);
1998 assert(plantarchitecture_ptr->unique_leaf_prototype_objIDs.find(phytomer_parameters.leaf.prototype.unique_prototype_identifier) != plantarchitecture_ptr->unique_leaf_prototype_objIDs.end());
1999 assert(plantarchitecture_ptr->unique_leaf_prototype_objIDs.at(phytomer_parameters.leaf.prototype.unique_prototype_identifier).size() > prototype);
2000 assert(plantarchitecture_ptr->unique_leaf_prototype_objIDs.at(phytomer_parameters.leaf.prototype.unique_prototype_identifier).at(prototype).size() > leaf);
2001 objID_leaf = context_ptr->copyObject(plantarchitecture_ptr->unique_leaf_prototype_objIDs.at(phytomer_parameters.leaf.prototype.unique_prototype_identifier).at(prototype).at(leaf));
2002 } else {
2003 // load a new prototype
2004 objID_leaf = phytomer_parameters.leaf.prototype.prototype_function(context_ptr, &phytomer_parameters.leaf.prototype, ind_from_tip);
2005 }
2006
2007 // -- leaf scaling -- //
2008
2009 if (leaves_per_petiole > 0 && phytomer_parameters.leaf.leaflet_scale.val() != 1.f && ind_from_tip != 0) {
2010 leaf_size_max.at(petiole).at(leaf) = powf(phytomer_parameters.leaf.leaflet_scale.val(), fabs(ind_from_tip)) * phytomer_parameters.leaf.prototype_scale.val();
2011 } else {
2012 leaf_size_max.at(petiole).at(leaf) = phytomer_parameters.leaf.prototype_scale.val();
2013 }
2014 vec3 leaf_scale = leaf_scale_factor_fraction * leaf_size_max.at(petiole).at(leaf) * make_vec3(1, 1, 1);
2015
2016 context_ptr->scaleObject(objID_leaf, leaf_scale);
2017
2018 float compound_rotation = 0;
2019 if (leaves_per_petiole > 1) {
2020 if (leaflet_offset_val == 0) {
2021 float dphi = PI_F / (floor(0.5 * float(leaves_per_petiole - 1)) + 1);
2022 compound_rotation = -float(PI_F) + dphi * (leaf + 0.5f);
2023 } else {
2024 if (leaf == float(leaves_per_petiole - 1) / 2.f) {
2025 // tip leaf
2026 compound_rotation = 0;
2027 } else if (leaf < float(leaves_per_petiole - 1) / 2.f) {
2028 compound_rotation = -0.5 * PI_F;
2029 } else {
2030 compound_rotation = 0.5 * PI_F;
2031 }
2032 }
2033 }
2034
2035 // -- leaf rotations -- //
2036
2037 // leaf roll rotation
2038 float roll_rot = 0;
2039 if (leaves_per_petiole == 1) {
2040 int sign = (shoot_index.x % 2 == 0) ? 1 : -1;
2041 roll_rot = (acos_safe(internode_axis.z) - deg2rad(phytomer_parameters.leaf.roll.val())) * sign;
2042 } else if (ind_from_tip != 0) {
2043 roll_rot = (asin_safe(petiole_tip_axis.z) + deg2rad(phytomer_parameters.leaf.roll.val())) * compound_rotation / std::fabs(compound_rotation);
2044 }
2045 leaf_rotation.at(petiole).at(leaf).roll = deg2rad(phytomer_parameters.leaf.roll.val());
2046 phytomer_parameters.leaf.roll.resample();
2047 context_ptr->rotateObject(objID_leaf, roll_rot, "x");
2048
2049 // leaf pitch rotation
2050 leaf_rotation.at(petiole).at(leaf).pitch = deg2rad(phytomer_parameters.leaf.pitch.val());
2051 float pitch_rot = leaf_rotation.at(petiole).at(leaf).pitch;
2052 phytomer_parameters.leaf.pitch.resample();
2053 if (ind_from_tip == 0) {
2054 pitch_rot += asin_safe(petiole_tip_axis.z);
2055 }
2056 context_ptr->rotateObject(objID_leaf, -pitch_rot, "y");
2057
2058 // leaf yaw rotation
2059 if (ind_from_tip != 0) {
2060 float sign = -compound_rotation / fabs(compound_rotation);
2061 leaf_rotation.at(petiole).at(leaf).yaw = sign * deg2rad(phytomer_parameters.leaf.yaw.val());
2062 float yaw_rot = leaf_rotation.at(petiole).at(leaf).yaw;
2063 phytomer_parameters.leaf.yaw.resample();
2064 context_ptr->rotateObject(objID_leaf, yaw_rot, "z");
2065 } else {
2066 leaf_rotation.at(petiole).at(leaf).yaw = 0;
2067 }
2068
2069 // rotate leaf to azimuth of petiole
2070 context_ptr->rotateObject(objID_leaf, -std::atan2(petiole_tip_axis.y, petiole_tip_axis.x) + compound_rotation, "z");
2071
2072
2073 // -- leaf translation -- //
2074
2075 vec3 leaf_base = petiole_vertices.at(petiole).back();
2076 if (leaves_per_petiole > 1 && leaflet_offset_val > 0) {
2077 if (ind_from_tip != 0) {
2078 float offset = (fabs(ind_from_tip) - 0.5f) * leaflet_offset_val * phytomer_parameters.petiole.length.val();
2079 leaf_base = PlantArchitecture::interpolateTube(petiole_vertices.at(petiole), 1.f - offset / phytomer_parameters.petiole.length.val());
2080 }
2081 }
2082
2083 context_ptr->translateObject(objID_leaf, leaf_base);
2084
2085 leaf_objIDs.at(petiole).push_back(objID_leaf);
2086 leaf_bases.at(petiole).push_back(leaf_base);
2087 }
2088 phytomer_parameters.leaf.prototype_scale.resample();
2089
2090 inflorescence_bending_axis = cross(parent_internode_axis, petiole_axis_actual);
2091 if (inflorescence_bending_axis == make_vec3(0, 0, 0)) {
2092 inflorescence_bending_axis = make_vec3(1, 0, 0);
2093 }
2094 }
2095
2096 // Special case: if there are no petioles, still create vegetative buds directly on the internode
2097 if (phytomer_parameters.petiole.petioles_per_internode == 0) {
2098 std::vector<VegetativeBud> vegetative_buds_new;
2099 vegetative_buds_new.resize(phytomer_parameters.internode.max_vegetative_buds_per_petiole.val());
2100 phytomer_parameters.internode.max_vegetative_buds_per_petiole.resample();
2101 axillary_vegetative_buds.push_back(vegetative_buds_new);
2102
2103 std::vector<FloralBud> floral_buds_new;
2104 floral_buds_new.resize(phytomer_parameters.internode.max_floral_buds_per_petiole.val());
2105 phytomer_parameters.internode.max_floral_buds_per_petiole.resample();
2106 floral_buds.push_back(floral_buds_new);
2107 }
2108}
2109
2110float Phytomer::calculatePhytomerVolume(uint node_number) const {
2111 // Get the radii of this phytomer from the parent shoot
2112 const auto &segment = parent_shoot_ptr->shoot_internode_radii.at(node_number);
2113
2114 // Find the average radius
2115 float avg_radius = 0.0f;
2116 for (float radius: segment) {
2117 avg_radius += radius;
2118 }
2119 avg_radius /= scast<float>(segment.size());
2120
2121 // Get the length of the phytomer
2122 float length = getInternodeLength();
2123
2124 // Calculate the volume of the cylinder
2125 float volume = PI_F * avg_radius * avg_radius * length;
2126
2127 return volume;
2128}
2129
2130void Phytomer::createInflorescenceGeometry(FloralBud &fbud, const helios::vec3 &fruit_base, const helios::vec3 &peduncle_axis, float pitch, float roll, float azimuth, float yaw_compound, float scale_factor, bool is_open_flower) {
2131
2132 // Step 1: Create flower/fruit prototype based on current bud state
2133 uint objID_fruit;
2134 if (fbud.state == BUD_FRUITING) {
2135 if (phytomer_parameters.inflorescence.unique_prototypes > 0) {
2136 // Copy existing prototype
2137 int prototype = context_ptr->randu(0, int(phytomer_parameters.inflorescence.unique_prototypes - 1));
2138 objID_fruit = context_ptr->copyObject(plantarchitecture_ptr->unique_fruit_prototype_objIDs.at(phytomer_parameters.inflorescence.fruit_prototype_function).at(prototype));
2139 } else {
2140 // Load new prototype
2141 objID_fruit = phytomer_parameters.inflorescence.fruit_prototype_function(context_ptr, 1);
2142 }
2143 } else {
2144 // Flower (open or closed)
2145 if (phytomer_parameters.inflorescence.unique_prototypes > 0) {
2146 // Copy existing prototype
2147 int prototype = context_ptr->randu(0, int(phytomer_parameters.inflorescence.unique_prototypes - 1));
2148 if (is_open_flower) {
2149 objID_fruit = context_ptr->copyObject(plantarchitecture_ptr->unique_open_flower_prototype_objIDs.at(phytomer_parameters.inflorescence.flower_prototype_function).at(prototype));
2150 } else {
2151 objID_fruit = context_ptr->copyObject(plantarchitecture_ptr->unique_closed_flower_prototype_objIDs.at(phytomer_parameters.inflorescence.flower_prototype_function).at(prototype));
2152 }
2153 } else {
2154 // Load new prototype
2155 objID_fruit = phytomer_parameters.inflorescence.flower_prototype_function(context_ptr, 1, is_open_flower);
2156 }
2157 }
2158
2159 // Step 2: Scale the flower/fruit
2160 vec3 fruit_scale = scale_factor * make_vec3(1, 1, 1);
2161 context_ptr->scaleObject(objID_fruit, fruit_scale);
2162
2163 // Step 3: Apply rotations at origin in correct order (roll, pitch, azimuth)
2164 if (std::abs(roll) > 1e-6) {
2165 context_ptr->rotateObject(objID_fruit, roll, "x");
2166 }
2167 if (std::abs(pitch) > 1e-6) {
2168 context_ptr->rotateObject(objID_fruit, pitch, "y");
2169 }
2170 if (std::abs(azimuth) > 1e-6) {
2171 context_ptr->rotateObject(objID_fruit, azimuth, "z");
2172 }
2173
2174 // Step 4: Translate to position on peduncle
2175 context_ptr->translateObject(objID_fruit, fruit_base);
2176
2177 // Step 5: Apply compound rotation about peduncle axis (or vertical axis for fruit with gravity)
2178 if (std::abs(yaw_compound) > 1e-6) {
2179 context_ptr->rotateObject(objID_fruit, yaw_compound, fruit_base, peduncle_axis);
2180 }
2181
2182 // Step 6: Store in floral bud data structures
2183 fbud.inflorescence_objIDs.push_back(objID_fruit);
2184 fbud.inflorescence_bases.push_back(fruit_base);
2185
2186 AxisRotation flower_rotation;
2187 flower_rotation.pitch = pitch;
2188 flower_rotation.yaw = yaw_compound;
2189 flower_rotation.roll = roll;
2190 flower_rotation.azimuth = azimuth;
2191 flower_rotation.peduncle_axis = peduncle_axis;
2192 fbud.inflorescence_rotation.push_back(flower_rotation);
2193
2194 fbud.inflorescence_base_scales.push_back(scale_factor);
2195
2196 assert(fbud.inflorescence_objIDs.size() == fbud.inflorescence_bases.size());
2197 assert(fbud.inflorescence_bases.size() == fbud.inflorescence_rotation.size());
2198 assert(fbud.inflorescence_rotation.size() == fbud.inflorescence_base_scales.size());
2199}
2200
2201void PlantArchitecture::ensureInflorescencePrototypesInitialized(const PhytomerParameters &params) {
2202 if (params.inflorescence.unique_prototypes > 0) {
2203 // Initialize closed flower prototypes
2204 if (params.inflorescence.flower_prototype_function != nullptr && unique_closed_flower_prototype_objIDs.find(params.inflorescence.flower_prototype_function) == unique_closed_flower_prototype_objIDs.end()) {
2205 unique_closed_flower_prototype_objIDs[params.inflorescence.flower_prototype_function].resize(params.inflorescence.unique_prototypes);
2206 for (int prototype = 0; prototype < params.inflorescence.unique_prototypes; prototype++) {
2207 uint objID_flower = params.inflorescence.flower_prototype_function(context_ptr, 1, false);
2208 unique_closed_flower_prototype_objIDs.at(params.inflorescence.flower_prototype_function).at(prototype) = objID_flower;
2209 context_ptr->hideObject(objID_flower);
2210 }
2211 }
2212 // Initialize open flower prototypes
2213 if (params.inflorescence.flower_prototype_function != nullptr && unique_open_flower_prototype_objIDs.find(params.inflorescence.flower_prototype_function) == unique_open_flower_prototype_objIDs.end()) {
2214 unique_open_flower_prototype_objIDs[params.inflorescence.flower_prototype_function].resize(params.inflorescence.unique_prototypes);
2215 for (int prototype = 0; prototype < params.inflorescence.unique_prototypes; prototype++) {
2216 uint objID_flower = params.inflorescence.flower_prototype_function(context_ptr, 1, true);
2217 unique_open_flower_prototype_objIDs.at(params.inflorescence.flower_prototype_function).at(prototype) = objID_flower;
2218 context_ptr->hideObject(objID_flower);
2219 }
2220 }
2221 // Initialize fruit prototypes
2222 if (params.inflorescence.fruit_prototype_function != nullptr && unique_fruit_prototype_objIDs.find(params.inflorescence.fruit_prototype_function) == unique_fruit_prototype_objIDs.end()) {
2223 unique_fruit_prototype_objIDs[params.inflorescence.fruit_prototype_function].resize(params.inflorescence.unique_prototypes);
2224 for (int prototype = 0; prototype < params.inflorescence.unique_prototypes; prototype++) {
2225 uint objID_fruit = params.inflorescence.fruit_prototype_function(context_ptr, 1);
2226 unique_fruit_prototype_objIDs.at(params.inflorescence.fruit_prototype_function).at(prototype) = objID_fruit;
2227 context_ptr->hideObject(objID_fruit);
2228 }
2229 }
2230 }
2231}
2232
2233void Phytomer::updateInflorescence(FloralBud &fbud) {
2234 bool build_context_geometry_peduncle = plantarchitecture_ptr->build_context_geometry_peduncle;
2235
2236 uint Ndiv_peduncle_length = std::max(uint(1), phytomer_parameters.peduncle.length_segments);
2237 uint Ndiv_peduncle_radius = std::max(uint(3), phytomer_parameters.peduncle.radial_subdivisions);
2238 if (phytomer_parameters.peduncle.length_segments == 0 || phytomer_parameters.peduncle.radial_subdivisions < 3) {
2239 build_context_geometry_peduncle = false;
2240 }
2241
2242 // Sample length once before calculating dr (same fix as petioles - don't resample until after geometry is created)
2243 float peduncle_length = phytomer_parameters.peduncle.length.val();
2244 float dr_peduncle = peduncle_length / float(Ndiv_peduncle_length);
2245
2246 std::vector<vec3> peduncle_vertices(phytomer_parameters.peduncle.length_segments + 1);
2247 peduncle_vertices.at(0) = fbud.base_position;
2248 std::vector<float> peduncle_radii(phytomer_parameters.peduncle.length_segments + 1);
2249 peduncle_radii.at(0) = phytomer_parameters.peduncle.radius.val();
2250 std::vector<RGBcolor> peduncle_colors(phytomer_parameters.peduncle.length_segments + 1);
2251 peduncle_colors.at(0) = phytomer_parameters.peduncle.color;
2252
2253 vec3 peduncle_axis = getAxisVector(1.f, getInternodeNodePositions());
2254
2255 // Create local copy of inflorescence_bending_axis that will be rotated with the peduncle
2256 vec3 inflorescence_bending_axis_actual = inflorescence_bending_axis;
2257
2258 // peduncle pitch rotation
2259 if (phytomer_parameters.peduncle.pitch.val() != 0.f || fbud.base_rotation.pitch != 0.f) {
2260 peduncle_axis = rotatePointAboutLine(peduncle_axis, nullorigin, inflorescence_bending_axis_actual, deg2rad(phytomer_parameters.peduncle.pitch.val()) + fbud.base_rotation.pitch);
2261 }
2262
2263 // rotate peduncle to azimuth of petiole and apply peduncle base yaw rotation
2264 vec3 internode_axis = getAxisVector(1.f, getInternodeNodePositions());
2265 vec3 parent_petiole_base_axis;
2266 if (petiole_vertices.empty()) {
2267 // No petioles - use internode axis instead
2268 parent_petiole_base_axis = internode_axis;
2269 } else {
2270 parent_petiole_base_axis = getPetioleAxisVector(0.f, fbud.parent_index);
2271 }
2272 float parent_petiole_azimuth = -std::atan2(parent_petiole_base_axis.y, parent_petiole_base_axis.x);
2273 float current_peduncle_azimuth = -std::atan2(peduncle_axis.y, peduncle_axis.x);
2274 float azimuthal_rotation = current_peduncle_azimuth - parent_petiole_azimuth;
2275 peduncle_axis = rotatePointAboutLine(peduncle_axis, nullorigin, internode_axis, azimuthal_rotation);
2276 // Rotate the bending axis by the same azimuthal angle to keep it perpendicular to the peduncle
2277 inflorescence_bending_axis_actual = rotatePointAboutLine(inflorescence_bending_axis_actual, nullorigin, internode_axis, azimuthal_rotation);
2278
2279
2280 float theta_base = fabs(cart2sphere(peduncle_axis).zenith);
2281
2282 // Apply collision avoidance for peduncle direction (if enabled) - following petiole pattern
2283 vec3 collision_optimal_peduncle_direction;
2284 bool peduncle_collision_active = false;
2285
2286 if (plantarchitecture_ptr->fruit_collision_detection_enabled) {
2287 collision_optimal_peduncle_direction = calculateFruitCollisionAvoidanceDirection(fbud.base_position, peduncle_axis, peduncle_collision_active);
2288 }
2289
2290 if (peduncle_collision_active) {
2291 float inertia_weight = plantarchitecture_ptr->collision_inertia_weight;
2292 vec3 natural_peduncle_direction = peduncle_axis;
2293
2294 // Blend natural peduncle direction with optimal direction
2295 // inertia = 1.0: use natural direction (no collision avoidance)
2296 // inertia = 0.0: use optimal direction (full collision avoidance)
2297 peduncle_axis = inertia_weight * natural_peduncle_direction + (1.0f - inertia_weight) * collision_optimal_peduncle_direction;
2298 peduncle_axis.normalize();
2299 }
2300
2301 // Sample curvature once and store (avoids resampling each iteration which was a bug - same fix as petioles)
2302 float peduncle_curvature = phytomer_parameters.peduncle.curvature.val();
2303 phytomer_parameters.peduncle.curvature.resample();
2304
2305 // Store actual sampled peduncle parameters for XML reconstruction
2306 uint petiole_idx = fbud.parent_index;
2307 uint bud_idx = fbud.bud_index;
2308 if (petiole_idx < this->peduncle_length.size()) {
2309 if (this->peduncle_length.at(petiole_idx).size() <= bud_idx) {
2310 this->peduncle_length.at(petiole_idx).resize(bud_idx + 1);
2311 this->peduncle_radius.at(petiole_idx).resize(bud_idx + 1);
2312 this->peduncle_pitch.at(petiole_idx).resize(bud_idx + 1);
2313 this->peduncle_curvature.at(petiole_idx).resize(bud_idx + 1);
2314 }
2315 this->peduncle_length.at(petiole_idx).at(bud_idx) = peduncle_length;
2316 this->peduncle_radius.at(petiole_idx).at(bud_idx) = phytomer_parameters.peduncle.radius.val();
2317 this->peduncle_pitch.at(petiole_idx).at(bud_idx) = phytomer_parameters.peduncle.pitch.val();
2318 this->peduncle_curvature.at(petiole_idx).at(bud_idx) = peduncle_curvature;
2319 }
2320
2321 for (int i = 1; i <= phytomer_parameters.peduncle.length_segments; i++) {
2322 if (peduncle_curvature != 0.f) {
2323 float curvature_value = peduncle_curvature;
2324
2325 // Calculate horizontal bending axis perpendicular to current peduncle direction
2326 // This ensures bending is purely upward or downward
2327 vec3 horizontal_bending_axis = cross(peduncle_axis, make_vec3(0, 0, 1));
2328 float axis_magnitude = horizontal_bending_axis.magnitude();
2329
2330 // Check if peduncle is nearly vertical (axis magnitude near zero)
2331 if (axis_magnitude > 0.001f) {
2332 horizontal_bending_axis = horizontal_bending_axis / axis_magnitude; // normalize
2333
2334 // Calculate current angle from target vertical direction
2335 float theta_curvature = deg2rad(curvature_value * dr_peduncle);
2336 float theta_from_target;
2337
2338 if (curvature_value > 0) {
2339 // Positive curvature: target is upward (0, 0, 1)
2340 // Current angle from target = acos(peduncle_axis.z)
2341 theta_from_target = std::acos(std::min(1.0f, std::max(-1.0f, peduncle_axis.z)));
2342 } else {
2343 // Negative curvature: target is downward (0, 0, -1)
2344 // Current angle from target = acos(-peduncle_axis.z)
2345 theta_from_target = std::acos(std::min(1.0f, std::max(-1.0f, -peduncle_axis.z)));
2346 }
2347
2348 // Clamp rotation to not overshoot vertical
2349 if (fabs(theta_curvature) >= theta_from_target) {
2350 // Would overshoot - snap to exact vertical
2351 if (curvature_value > 0) {
2352 peduncle_axis = make_vec3(0, 0, 1);
2353 } else {
2354 peduncle_axis = make_vec3(0, 0, -1);
2355 }
2356 } else {
2357 // Won't overshoot - apply rotation
2358 peduncle_axis = rotatePointAboutLine(peduncle_axis, nullorigin, horizontal_bending_axis, theta_curvature);
2359 peduncle_axis.normalize();
2360 }
2361 } else {
2362 // Already vertical - snap to correct vertical direction based on curvature sign
2363 if (curvature_value > 0) {
2364 peduncle_axis = make_vec3(0, 0, 1); // upward
2365 } else {
2366 peduncle_axis = make_vec3(0, 0, -1); // downward
2367 }
2368 }
2369 }
2370
2371 peduncle_vertices.at(i) = peduncle_vertices.at(i - 1) + dr_peduncle * peduncle_axis;
2372
2373 peduncle_radii.at(i) = phytomer_parameters.peduncle.radius.val();
2374 peduncle_colors.at(i) = phytomer_parameters.peduncle.color;
2375 }
2376
2377 if (build_context_geometry_peduncle) {
2378 fbud.peduncle_objIDs.push_back(context_ptr->addTubeObject(Ndiv_peduncle_radius, peduncle_vertices, peduncle_radii, peduncle_colors));
2379 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(fbud.peduncle_objIDs.back()), "object_label", "peduncle");
2380 }
2381
2382 // Store peduncle vertices for later axis vector calculations
2383 // Use the parent_index to determine which petiole this floral bud belongs to (petiole_idx already defined above)
2384
2385 // Ensure the peduncle_vertices storage has the right size for this floral bud
2386 if (petiole_idx < this->peduncle_vertices.size()) {
2387 if (this->peduncle_vertices.at(petiole_idx).size() <= fbud.bud_index) {
2388 this->peduncle_vertices.at(petiole_idx).resize(fbud.bud_index + 1);
2389 }
2390 this->peduncle_vertices.at(petiole_idx).at(fbud.bud_index) = peduncle_vertices;
2391 }
2392
2393 // Store peduncle radii alongside vertices for exact geometry reconstruction
2394 if (petiole_idx < this->peduncle_radii.size()) {
2395 if (this->peduncle_radii.at(petiole_idx).size() <= fbud.bud_index) {
2396 this->peduncle_radii.at(petiole_idx).resize(fbud.bud_index + 1);
2397 }
2398 this->peduncle_radii.at(petiole_idx).at(fbud.bud_index) = peduncle_radii;
2399 }
2400
2401 // Resample parameters after geometry is created (same pattern as petioles - avoids mismatch between saved values and geometry)
2402 phytomer_parameters.peduncle.length.resample();
2403 phytomer_parameters.peduncle.radius.resample();
2404 phytomer_parameters.peduncle.pitch.resample();
2405
2406 // Create unique inflorescence prototypes for each shoot type so we can simply copy them for each leaf
2407 plantarchitecture_ptr->ensureInflorescencePrototypesInitialized(phytomer_parameters);
2408
2409 int flowers_per_peduncle = phytomer_parameters.inflorescence.flowers_per_peduncle.val();
2410 float flower_offset_val = clampOffset(flowers_per_peduncle, phytomer_parameters.inflorescence.flower_offset.val());
2411 for (int fruit = 0; fruit < flowers_per_peduncle; fruit++) {
2412 // Determine scale factor based on bud state
2413 float scale_factor;
2414 if (fbud.state == BUD_FRUITING) {
2415 scale_factor = phytomer_parameters.inflorescence.fruit_prototype_scale.val();
2416 phytomer_parameters.inflorescence.fruit_prototype_scale.resample();
2417 } else {
2418 scale_factor = phytomer_parameters.inflorescence.flower_prototype_scale.val();
2419 phytomer_parameters.inflorescence.flower_prototype_scale.resample();
2420 }
2421
2422 float ind_from_tip = fabs(fruit - float(flowers_per_peduncle - 1) / float(phytomer_parameters.petiole.petioles_per_internode));
2423
2424 // Calculate position on peduncle
2425 vec3 fruit_base = peduncle_vertices.back();
2426 float frac = 1;
2427 if (flowers_per_peduncle > 1 && flower_offset_val > 0) {
2428 if (ind_from_tip != 0) {
2429 float offset = (ind_from_tip - 0.5f) * flower_offset_val * phytomer_parameters.peduncle.length.val();
2430 if (phytomer_parameters.peduncle.length.val() > 0) {
2431 frac = 1.f - offset / phytomer_parameters.peduncle.length.val();
2432 }
2433 fruit_base = PlantArchitecture::interpolateTube(peduncle_vertices, frac);
2434 }
2435 }
2436
2437 // Calculate compound rotation about the peduncle
2438 float compound_rotation = 0;
2439 if (flowers_per_peduncle > 1) {
2440 if (flower_offset_val == 0) {
2441 // flowers/fruit are all at the tip, so just equally distribute them about the azimuth
2442 float dphi = PI_F / (floor(0.5 * float(flowers_per_peduncle - 1)) + 1);
2443 compound_rotation = -float(PI_F) + dphi * (fruit + 0.5f);
2444 } else {
2445 compound_rotation = deg2rad(phytomer_parameters.internode.phyllotactic_angle.val()) * float(ind_from_tip) + 2.f * PI_F / float(phytomer_parameters.petiole.petioles_per_internode) * float(fruit);
2446 phytomer_parameters.internode.phyllotactic_angle.resample();
2447 }
2448 }
2449
2450 vec3 peduncle_axis = getAxisVector(frac, peduncle_vertices);
2451
2452 // Calculate rotation parameters (sample BEFORE resampling for XML storage)
2453 float applied_roll = deg2rad(phytomer_parameters.inflorescence.roll.val());
2454 phytomer_parameters.inflorescence.roll.resample();
2455
2456 float applied_pitch_param = deg2rad(phytomer_parameters.inflorescence.pitch.val());
2457 phytomer_parameters.inflorescence.pitch.resample();
2458
2459 // Calculate pitch with peduncle alignment and gravity (for fruit)
2460 float pitch_inflorescence = -asin_safe(peduncle_axis.z) + applied_pitch_param;
2461 if (fbud.state == BUD_FRUITING) {
2462 // gravity effect for fruit
2463 pitch_inflorescence = pitch_inflorescence + phytomer_parameters.inflorescence.fruit_gravity_factor_fraction.val() * (0.5f * PI_F - pitch_inflorescence);
2464 }
2465 phytomer_parameters.inflorescence.fruit_gravity_factor_fraction.resample();
2466
2467 // Calculate azimuth to align with peduncle orientation
2468 float azimuth = -std::atan2(peduncle_axis.y, peduncle_axis.x);
2469
2470 // Calculate compound yaw (peduncle roll + compound rotation)
2471 float yaw_compound = deg2rad(phytomer_parameters.peduncle.roll.val()) + compound_rotation;
2472
2473 // Determine if flower is open
2474 bool is_open_flower = (fbud.state == BUD_FLOWER_OPEN);
2475
2476 // Call unified creation function
2477 createInflorescenceGeometry(fbud, fruit_base, peduncle_axis, pitch_inflorescence, applied_roll, azimuth, yaw_compound, scale_factor, is_open_flower);
2478 }
2479 phytomer_parameters.inflorescence.flowers_per_peduncle.resample();
2480 phytomer_parameters.peduncle.roll.resample();
2481
2482 if (plantarchitecture_ptr->output_object_data.at("rank")) {
2483 context_ptr->setObjectData(fbud.peduncle_objIDs, "rank", rank);
2484 context_ptr->setObjectData(fbud.inflorescence_objIDs, "rank", rank);
2485 }
2486
2487 if (plantarchitecture_ptr->output_object_data.at("plant_name")) {
2488 context_ptr->setObjectData(fbud.peduncle_objIDs, "plant_name", plantarchitecture_ptr->plant_instances.at(plantID).plant_name);
2489 context_ptr->setObjectData(fbud.inflorescence_objIDs, "plant_name", plantarchitecture_ptr->plant_instances.at(plantID).plant_name);
2490 }
2491
2492 if (plantarchitecture_ptr->output_object_data.at("peduncleID")) {
2493 for (uint objID: fbud.peduncle_objIDs) {
2494 context_ptr->setObjectData(objID, "peduncleID", (int) objID);
2495 }
2496 }
2497 for (uint objID: fbud.inflorescence_objIDs) {
2498 if (fbud.state == BUD_FLOWER_CLOSED && plantarchitecture_ptr->output_object_data.at("closedflowerID")) {
2499 context_ptr->setObjectData(objID, "closedflowerID", (int) objID);
2500 } else if (fbud.state == BUD_FLOWER_OPEN && plantarchitecture_ptr->output_object_data.at("openflowerID")) {
2501 context_ptr->clearObjectData(objID, "closedflowerID");
2502 context_ptr->setObjectData(objID, "openflowerID", (int) objID);
2503 } else if (plantarchitecture_ptr->output_object_data.at("fruitID")) {
2504 context_ptr->setObjectData(objID, "fruitID", (int) objID);
2505 }
2506 }
2507}
2508
2509void Phytomer::setPetioleBase(const helios::vec3 &base_position) {
2510 // If there are no petioles, nothing to update
2511 if (petiole_vertices.empty()) {
2512 return;
2513 }
2514
2515 vec3 old_base = petiole_vertices.front().front();
2516 vec3 shift = base_position - old_base;
2517
2518 for (auto &petiole_vertice: petiole_vertices) {
2519 for (auto &vertex: petiole_vertice) {
2520 vertex += shift;
2521 }
2522 }
2523
2524 if (build_context_geometry_petiole) {
2525 context_ptr->translateObject(flatten(petiole_objIDs), shift);
2526 }
2527 context_ptr->translateObject(flatten(leaf_objIDs), shift);
2528
2529 for (auto &petiole: leaf_bases) {
2530 for (auto &leaf_base: petiole) {
2531 leaf_base += shift;
2532 }
2533 }
2534 // Update peduncle vertices when the phytomer is translated
2535 for (auto &petiole_peduncles: peduncle_vertices) {
2536 for (auto &bud_peduncle_vertices: petiole_peduncles) {
2537 for (auto &vertex: bud_peduncle_vertices) {
2538 vertex += shift;
2539 }
2540 }
2541 }
2542
2543 for (auto &floral_bud: floral_buds) {
2544 for (auto &fbud: floral_bud) {
2545 fbud.base_position = petiole_vertices.front().front();
2546 context_ptr->translateObject(fbud.inflorescence_objIDs, shift);
2547 for (auto &base: fbud.inflorescence_bases) {
2548 base += shift;
2549 }
2550 if (build_context_geometry_peduncle) {
2551 context_ptr->translateObject(fbud.peduncle_objIDs, shift);
2552 }
2553 }
2554 }
2555}
2556
2557void Phytomer::rotateLeaf(uint petiole_index, uint leaf_index, const AxisRotation &rotation) {
2558 if (petiole_index >= leaf_objIDs.size()) {
2559 helios_runtime_error("ERROR (PlantArchitecture::Phytomer): Invalid petiole index.");
2560 } else if (leaf_index >= leaf_objIDs.at(petiole_index).size()) {
2561 helios_runtime_error("ERROR (PlantArchitecture::Phytomer): Invalid leaf index.");
2562 }
2563
2564 vec3 petiole_axis = getPetioleAxisVector(1.f, petiole_index);
2565 // note: this is not exactly correct because it should get the axis at the leaf position and not the tip
2566
2567 vec3 internode_axis = getInternodeAxisVector(1.f);
2568
2569 vec3 pitch_axis = -1 * cross(internode_axis, petiole_axis);
2570
2571 int leaves_per_petiole = leaf_rotation.at(petiole_index).size();
2572 float yaw;
2573 float roll;
2574 float compound_rotation = 0;
2575 if (leaves_per_petiole > 1 && leaf_index == float(leaves_per_petiole - 1) / 2.f) {
2576 // tip leaflet of compound leaf
2577 roll = 0;
2578 yaw = 0;
2579 compound_rotation = 0;
2580 } else if (leaves_per_petiole > 1 && leaf_index < float(leaves_per_petiole - 1) / 2.f) {
2581 // lateral leaflet of compound leaf
2582 yaw = -rotation.yaw;
2583 roll = -rotation.roll;
2584 compound_rotation = -0.5 * PI_F;
2585 } else {
2586 // not a compound leaf
2587 yaw = -rotation.yaw;
2588 roll = rotation.roll;
2589 compound_rotation = 0;
2590 }
2591
2592 // roll
2593 if (roll != 0.f) {
2594 vec3 roll_axis = rotatePointAboutLine({petiole_axis.x, petiole_axis.y, 0}, nullorigin, {0, 0, 1}, leaf_rotation.at(petiole_index).at(leaf_index).yaw + compound_rotation);
2595 context_ptr->rotateObject(leaf_objIDs.at(petiole_index).at(leaf_index), roll, leaf_bases.at(petiole_index).at(leaf_index), roll_axis);
2596 leaf_rotation.at(petiole_index).at(leaf_index).roll += roll;
2597 }
2598
2599 // pitch
2600 if (rotation.pitch != 0) {
2601 pitch_axis = rotatePointAboutLine(pitch_axis, nullorigin, {0, 0, 1}, -compound_rotation);
2602 context_ptr->rotateObject(leaf_objIDs.at(petiole_index).at(leaf_index), rotation.pitch, leaf_bases.at(petiole_index).at(leaf_index), pitch_axis);
2603 leaf_rotation.at(petiole_index).at(leaf_index).pitch += rotation.pitch;
2604 }
2605
2606 // yaw
2607 if (yaw != 0.f) {
2608 context_ptr->rotateObject(leaf_objIDs.at(petiole_index).at(leaf_index), yaw, leaf_bases.at(petiole_index).at(leaf_index), {0, 0, 1});
2609 leaf_rotation.at(petiole_index).at(leaf_index).yaw += yaw;
2610 }
2611}
2612
2613void Phytomer::setInternodeLengthScaleFraction(const float internode_scale_factor_fraction, const bool update_context_geometry) {
2614 assert(internode_scale_factor_fraction >= 0 && internode_scale_factor_fraction <= 1);
2615
2616 if (internode_scale_factor_fraction == current_internode_scale_factor) {
2617 return;
2618 }
2619
2620 float delta_scale = internode_scale_factor_fraction / current_internode_scale_factor;
2621
2622 current_internode_scale_factor = internode_scale_factor_fraction;
2623
2624 int p = shoot_index.x;
2625 int s_start = (p == 0) ? 1 : 0; // skip the first node at the base of the shoot
2626
2627 for (int s = s_start; s < parent_shoot_ptr->shoot_internode_vertices.at(p).size(); s++) {
2628 // looping over all segments within this phytomer internode
2629
2630 int p_minus = p;
2631 int s_minus = s - 1;
2632 if (s_minus < 0) {
2633 p_minus--;
2634 s_minus = static_cast<int>(parent_shoot_ptr->shoot_internode_vertices.at(p_minus).size() - 1);
2635 }
2636
2637 vec3 central_axis = (parent_shoot_ptr->shoot_internode_vertices.at(p).at(s) - parent_shoot_ptr->shoot_internode_vertices.at(p_minus).at(s_minus));
2638 float current_length = central_axis.magnitude();
2639 central_axis = central_axis / current_length;
2640 vec3 dL = central_axis * current_length * (delta_scale - 1);
2641
2642 // apply shift to all downstream nodes
2643 for (int p_downstream = p; p_downstream < parent_shoot_ptr->shoot_internode_vertices.size(); p_downstream++) {
2644 int sd_start = (p_downstream == p) ? s : 0;
2645 for (int s_downstream = sd_start; s_downstream < parent_shoot_ptr->shoot_internode_vertices.at(p_downstream).size(); s_downstream++) {
2646 parent_shoot_ptr->shoot_internode_vertices.at(p_downstream).at(s_downstream) += dL;
2647 }
2648 }
2649 }
2650
2651 parent_shoot_ptr->updateShootNodes(update_context_geometry);
2652}
2653
2654void Phytomer::scaleInternodeMaxLength(const float scale_factor) {
2655 this->internode_length_max *= scale_factor;
2656
2657 current_internode_scale_factor = current_internode_scale_factor / scale_factor;
2658
2659 if (current_internode_scale_factor >= 1.f) {
2661 current_internode_scale_factor = 1.f;
2662 }
2663}
2664
2665void Phytomer::setInternodeMaxLength(const float internode_length_max_new) {
2666 float scale_factor = internode_length_max_new / this->internode_length_max;
2667 scaleInternodeMaxLength(scale_factor);
2668}
2669
2670void Phytomer::setInternodeMaxRadius(float internode_radius_max_new) {
2671 this->internode_radius_max = internode_radius_max_new;
2672}
2673
2674
2675void Phytomer::setLeafScaleFraction(uint petiole_index, float leaf_scale_factor_fraction) {
2676 assert(leaf_scale_factor_fraction >= 0 && leaf_scale_factor_fraction <= 1);
2677
2678 if (current_leaf_scale_factor.size() <= petiole_index) {
2679 helios_runtime_error("ERROR (PlantArchitecture::Phytomer): Invalid petiole index for leaf scale factor.");
2680 }
2681
2682 // If the leaf is already at leaf_scale_factor_fraction, or there are no petioles/leaves, nothing to do.
2683 if (leaf_scale_factor_fraction == current_leaf_scale_factor.at(petiole_index) || (leaf_objIDs.at(petiole_index).empty() && petiole_objIDs.at(petiole_index).empty())) {
2684 return;
2685 }
2686
2687 float delta_scale = leaf_scale_factor_fraction / current_leaf_scale_factor.at(petiole_index);
2688
2689 petiole_length.at(petiole_index) *= delta_scale;
2690
2691 current_leaf_scale_factor.at(petiole_index) = leaf_scale_factor_fraction;
2692
2693 assert(leaf_objIDs.size() == leaf_bases.size());
2694
2695 // scale the petiole geometry if it exists, or create it if it doesn't but should now
2696
2697 if (!petiole_objIDs.at(petiole_index).empty()) {
2698 int node = 0;
2699 vec3 last_base = petiole_vertices.at(petiole_index).front(); // looping over petioles
2700 for (uint objID: petiole_objIDs.at(petiole_index)) {
2701 // looping over cones/segments within petiole
2702 context_ptr->scaleConeObjectLength(objID, delta_scale);
2703 context_ptr->scaleConeObjectGirth(objID, delta_scale);
2704 petiole_radii.at(petiole_index).at(node) *= delta_scale;
2705 if (node > 0) {
2706 vec3 new_base = context_ptr->getConeObjectNode(objID, 0);
2707 context_ptr->translateObject(objID, last_base - new_base);
2708 } else {
2709 petiole_vertices.at(petiole_index).at(0) = context_ptr->getConeObjectNode(objID, 0);
2710 }
2711 last_base = context_ptr->getConeObjectNode(objID, 1);
2712 petiole_vertices.at(petiole_index).at(node + 1) = last_base;
2713 node++;
2714 }
2715 } else if (build_context_geometry_petiole) {
2716 // Petiole geometry doesn't exist - scale the radii AND vertices data and try to create geometry
2717 vec3 base = petiole_vertices.at(petiole_index).at(0);
2718 for (uint node = 0; node < petiole_radii.at(petiole_index).size(); node++) {
2719 petiole_radii.at(petiole_index).at(node) *= delta_scale;
2720 }
2721 // Scale vertices relative to base point to match the scaling of existing geometry
2722 for (uint node = 1; node < petiole_vertices.at(petiole_index).size(); node++) {
2723 vec3 offset = petiole_vertices.at(petiole_index).at(node) - base;
2724 petiole_vertices.at(petiole_index).at(node) = base + offset * delta_scale;
2725 }
2726
2727 uint Ndiv_petiole_radius = std::max(uint(3), phytomer_parameters.petiole.radial_subdivisions);
2728 petiole_objIDs.at(petiole_index) = makeTubeFromCones(Ndiv_petiole_radius, petiole_vertices.at(petiole_index), petiole_radii.at(petiole_index), petiole_colors, context_ptr);
2729 if (!petiole_objIDs.at(petiole_index).empty()) {
2730 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(petiole_objIDs.at(petiole_index)), "object_label", "petiole");
2731 }
2732 }
2733
2734 // scale and translate leaves
2735 assert(leaf_objIDs.at(petiole_index).size() == leaf_bases.at(petiole_index).size());
2736 for (int leaf = 0; leaf < leaf_objIDs.at(petiole_index).size(); leaf++) {
2737 float ind_from_tip = float(leaf) - float(leaf_objIDs.at(petiole_index).size() - 1) / 2.f;
2738
2739 float leaflet_offset_val = clampOffset(int(leaf_objIDs.at(petiole_index).size()), phytomer_parameters.leaf.leaflet_offset.val());
2740
2741 context_ptr->translateObject(leaf_objIDs.at(petiole_index).at(leaf), -1 * leaf_bases.at(petiole_index).at(leaf));
2742 context_ptr->scaleObject(leaf_objIDs.at(petiole_index).at(leaf), delta_scale * make_vec3(1, 1, 1));
2743 if (ind_from_tip == 0) {
2744 context_ptr->translateObject(leaf_objIDs.at(petiole_index).at(leaf), petiole_vertices.at(petiole_index).back());
2745 leaf_bases.at(petiole_index).at(leaf) = petiole_vertices.at(petiole_index).back();
2746 } else {
2747 float offset = (fabs(ind_from_tip) - 0.5f) * leaflet_offset_val * phytomer_parameters.petiole.length.val();
2748 vec3 leaf_base = PlantArchitecture::interpolateTube(petiole_vertices.at(petiole_index), 1.f - offset / phytomer_parameters.petiole.length.val());
2749 context_ptr->translateObject(leaf_objIDs.at(petiole_index).at(leaf), leaf_base);
2750 leaf_bases.at(petiole_index).at(leaf) = leaf_base;
2751 }
2752 }
2753}
2754
2755void Phytomer::setLeafScaleFraction(float leaf_scale_factor_fraction) {
2756 for (uint petiole_index = 0; petiole_index < leaf_objIDs.size(); petiole_index++) {
2757 setLeafScaleFraction(petiole_index, leaf_scale_factor_fraction);
2758 }
2759}
2760
2761void Phytomer::setLeafPrototypeScale(uint petiole_index, float leaf_prototype_scale) {
2762 if (leaf_objIDs.size() <= petiole_index) {
2763 helios_runtime_error("ERROR (PlantArchitecture::Phytomer): Invalid petiole index for leaf prototype scale.");
2764 }
2765 if (leaf_prototype_scale < 0.f) {
2766 leaf_prototype_scale = 0;
2767 }
2768
2769 float tip_ind = ceil(scast<float>(leaf_size_max.at(petiole_index).size() - 1) / 2.f);
2770 float scale_factor = leaf_prototype_scale / leaf_size_max.at(petiole_index).at(tip_ind);
2771 current_leaf_scale_factor.at(petiole_index) *= scale_factor;
2772
2773 for (int leaf = 0; leaf < leaf_objIDs.at(petiole_index).size(); leaf++) {
2774 leaf_size_max.at(petiole_index).at(leaf) *= scale_factor;
2775 context_ptr->scaleObjectAboutPoint(leaf_objIDs.at(petiole_index).at(leaf), scale_factor * make_vec3(1, 1, 1), leaf_bases.at(petiole_index).at(leaf));
2776 }
2777
2778 // note: at time of phytomer creation, petiole curvature was based on the petiole length prior to this scaling. To stay consistent, we will scale the curvature appropriately.
2779 this->petiole_curvature.at(petiole_index) /= scale_factor;
2780
2781 if (current_leaf_scale_factor.at(petiole_index) >= 1.f) {
2782 setLeafScaleFraction(petiole_index, 1.f);
2783 current_leaf_scale_factor.at(petiole_index) = 1.f;
2784 }
2785}
2786
2787void Phytomer::setLeafPrototypeScale(float leaf_prototype_scale) {
2788 for (uint petiole_index = 0; petiole_index < leaf_objIDs.size(); petiole_index++) {
2789 setLeafPrototypeScale(petiole_index, leaf_prototype_scale);
2790 }
2791}
2792
2793void Phytomer::scaleLeafPrototypeScale(uint petiole_index, float scale_factor) {
2794 if (leaf_objIDs.size() <= petiole_index) {
2795 helios_runtime_error("ERROR (PlantArchitecture::Phytomer): Invalid petiole index for leaf prototype scale.");
2796 }
2797 if (scale_factor < 0.f) {
2798 scale_factor = 0;
2799 }
2800
2801 current_leaf_scale_factor.at(petiole_index) /= scale_factor;
2802
2803 for (int leaf = 0; leaf < leaf_objIDs.at(petiole_index).size(); leaf++) {
2804 leaf_size_max.at(petiole_index).at(leaf) *= scale_factor;
2805 context_ptr->scaleObjectAboutPoint(leaf_objIDs.at(petiole_index).at(leaf), scale_factor * make_vec3(1, 1, 1), leaf_bases.at(petiole_index).at(leaf));
2806 }
2807
2808 // note: at time of phytomer creation, petiole curvature was based on the petiole length prior to this scaling. To stay consistent, we will scale the curvature appropriately.
2809 this->petiole_curvature.at(petiole_index) /= scale_factor;
2810
2811 if (current_leaf_scale_factor.at(petiole_index) >= 1.f) {
2812 setLeafScaleFraction(petiole_index, 1.f);
2813 current_leaf_scale_factor.at(petiole_index) = 1.f;
2814 }
2815}
2816
2817void Phytomer::scaleLeafPrototypeScale(float scale_factor) {
2818 for (uint petiole_index = 0; petiole_index < leaf_objIDs.size(); petiole_index++) {
2819 scaleLeafPrototypeScale(petiole_index, scale_factor);
2820 }
2821}
2822
2823void Phytomer::scalePetioleGeometry(uint petiole_index, float target_length, float target_base_radius) {
2824 if (petiole_index >= petiole_length.size()) {
2825 helios_runtime_error("ERROR (PlantArchitecture::Phytomer::scalePetioleGeometry): Invalid petiole index " + std::to_string(petiole_index) + ".");
2826 }
2827 if (target_length <= 0.f || target_base_radius <= 0.f) {
2828 helios_runtime_error("ERROR (PlantArchitecture::Phytomer::scalePetioleGeometry): Target length and radius must be positive.");
2829 }
2830
2831 // Calculate scale factors from current to target dimensions
2832 float current_length = petiole_length.at(petiole_index);
2833 float current_base_radius = petiole_radii.at(petiole_index).at(0);
2834
2835 if (current_length <= 0.f || current_base_radius <= 0.f) {
2836 // Petiole wasn't properly initialized, just update the stored values
2837 petiole_length.at(petiole_index) = target_length;
2838 if (!petiole_radii.at(petiole_index).empty()) {
2839 petiole_radii.at(petiole_index).at(0) = target_base_radius;
2840 }
2841 return;
2842 }
2843
2844 float length_scale = target_length / current_length;
2845 float radius_scale = target_base_radius / current_base_radius;
2846
2847 // Get the petiole base position for scaling reference
2848 vec3 petiole_base = petiole_vertices.at(petiole_index).at(0);
2849
2850 // Update stored vertices: scale positions relative to base
2851 for (size_t j = 0; j < petiole_vertices.at(petiole_index).size(); j++) {
2852 vec3 offset = petiole_vertices.at(petiole_index).at(j) - petiole_base;
2853 petiole_vertices.at(petiole_index).at(j) = petiole_base + offset * length_scale;
2854 }
2855
2856 // Update stored radii: scale by radius factor
2857 for (size_t j = 0; j < petiole_radii.at(petiole_index).size(); j++) {
2858 petiole_radii.at(petiole_index).at(j) *= radius_scale;
2859 }
2860
2861 // Update scalar length
2862 petiole_length.at(petiole_index) = target_length;
2863
2864 // Update the Context geometry if it exists
2865 if (!petiole_objIDs.at(petiole_index).empty()) {
2866 // Delete existing geometry
2867 context_ptr->deleteObject(petiole_objIDs.at(petiole_index));
2868
2869 // Recreate tube with updated vertices and radii
2870 std::vector<RGBcolor> petiole_colors(petiole_radii.at(petiole_index).size(), phytomer_parameters.petiole.color);
2871 uint Ndiv_petiole_radius = std::max(uint(3), phytomer_parameters.petiole.radial_subdivisions);
2872
2873 petiole_objIDs.at(petiole_index) = makeTubeFromCones(Ndiv_petiole_radius, petiole_vertices.at(petiole_index), petiole_radii.at(petiole_index), petiole_colors, context_ptr);
2874
2875 // Restore primitive data labels
2876 if (!petiole_objIDs.at(petiole_index).empty()) {
2877 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(petiole_objIDs.at(petiole_index)), "object_label", "petiole");
2878 }
2879 }
2880
2881 // Translate leaf bases to maintain their relative positions along the scaled petiole
2882 if (petiole_index < leaf_bases.size()) {
2883 for (size_t leaf = 0; leaf < leaf_bases.at(petiole_index).size(); leaf++) {
2884 vec3 offset = leaf_bases.at(petiole_index).at(leaf) - petiole_base;
2885 leaf_bases.at(petiole_index).at(leaf) = petiole_base + offset * length_scale;
2886
2887 // Translate the actual leaf geometry in Context
2888 if (petiole_index < leaf_objIDs.size() && leaf < leaf_objIDs.at(petiole_index).size()) {
2889 vec3 translation = offset * length_scale - offset;
2890 context_ptr->translateObject(leaf_objIDs.at(petiole_index).at(leaf), translation);
2891 }
2892 }
2893 }
2894
2895 // Translate floral buds to maintain their relative positions along the scaled petiole
2896 if (petiole_index < floral_buds.size()) {
2897 for (auto &fbud: floral_buds.at(petiole_index)) {
2898 vec3 offset = fbud.base_position - petiole_base;
2899 vec3 translation = offset * length_scale - offset;
2900 fbud.base_position = petiole_base + offset * length_scale;
2901
2902 // Translate inflorescence bases
2903 for (size_t i = 0; i < fbud.inflorescence_bases.size(); i++) {
2904 fbud.inflorescence_bases.at(i) += translation;
2905 }
2906
2907 // Translate the actual floral geometry in Context
2908 for (size_t i = 0; i < fbud.inflorescence_objIDs.size(); i++) {
2909 context_ptr->translateObject(fbud.inflorescence_objIDs.at(i), translation);
2910 }
2911 for (size_t i = 0; i < fbud.peduncle_objIDs.size(); i++) {
2912 context_ptr->translateObject(fbud.peduncle_objIDs.at(i), translation);
2913 }
2914 }
2915 }
2916}
2917
2918void Phytomer::setInflorescenceScaleFraction(FloralBud &fbud, float inflorescence_scale_factor_fraction) const {
2919 assert(inflorescence_scale_factor_fraction >= 0 && inflorescence_scale_factor_fraction <= 1);
2920
2921 if (inflorescence_scale_factor_fraction == fbud.current_fruit_scale_factor) {
2922 return;
2923 }
2924
2925 float delta_scale = inflorescence_scale_factor_fraction / fbud.current_fruit_scale_factor;
2926
2927 fbud.current_fruit_scale_factor = inflorescence_scale_factor_fraction;
2928
2929 // scale and translate flowers/fruit
2930 for (int inflorescence = 0; inflorescence < fbud.inflorescence_objIDs.size(); inflorescence++) {
2931 context_ptr->scaleObjectAboutPoint(fbud.inflorescence_objIDs.at(inflorescence), delta_scale * make_vec3(1, 1, 1), fbud.inflorescence_bases.at(inflorescence));
2932 }
2933}
2934
2936 // parent_shoot_ptr->propagateDownstreamLeafArea( parent_shoot_ptr, this->shoot_index.x, -1.f*getLeafArea());
2937
2938 this->petiole_radii.resize(0);
2939 // this->petiole_vertices.resize(0);
2940 this->petiole_colors.resize(0);
2941 this->petiole_length.resize(0);
2942 this->leaf_size_max.resize(0);
2943 this->leaf_rotation.resize(0);
2944 this->leaf_bases.resize(0);
2945
2946 context_ptr->deleteObject(flatten(leaf_objIDs));
2947 leaf_objIDs.clear();
2948 leaf_bases.clear();
2949
2950 if (build_context_geometry_petiole) {
2951 context_ptr->deleteObject(flatten(petiole_objIDs));
2952 petiole_objIDs.resize(0);
2953 }
2954}
2955
2957 // prune the internode tube in the Context
2958 if (context_ptr->doesObjectExist(parent_shoot_ptr->internode_tube_objID)) {
2959 uint tube_nodes = context_ptr->getTubeObjectNodeCount(parent_shoot_ptr->internode_tube_objID);
2960 uint tube_segments = this->parent_shoot_ptr->shoot_parameters.phytomer_parameters.internode.length_segments;
2961 uint tube_prune_index;
2962 if (this->shoot_index.x == 0) {
2963 tube_prune_index = 0;
2964 } else {
2965 tube_prune_index = this->shoot_index.x * tube_segments + 1; // note that first segment has an extra vertex
2966 }
2967 if (tube_prune_index < tube_nodes) {
2968 context_ptr->pruneTubeNodes(parent_shoot_ptr->internode_tube_objID, tube_prune_index);
2969 }
2970 parent_shoot_ptr->terminateApicalBud();
2971 }
2972
2973 for (uint node = this->shoot_index.x; node < shoot_index.y; node++) {
2974 auto &phytomer = parent_shoot_ptr->phytomers.at(node);
2975
2976 // leaves
2977 phytomer->removeLeaf();
2978
2979 // inflorescence
2980 for (auto &petiole: phytomer->floral_buds) {
2981 for (auto &fbud: petiole) {
2982 for (int p = fbud.inflorescence_objIDs.size() - 1; p >= 0; p--) {
2983 uint objID = fbud.inflorescence_objIDs.at(p);
2984 context_ptr->deleteObject(objID);
2985 fbud.inflorescence_objIDs.erase(fbud.inflorescence_objIDs.begin() + p);
2986 fbud.inflorescence_bases.erase(fbud.inflorescence_bases.begin() + p);
2987 }
2988 for (int p = fbud.peduncle_objIDs.size() - 1; p >= 0; p--) {
2989 context_ptr->deleteObject(fbud.peduncle_objIDs);
2990 context_ptr->deleteObject(fbud.inflorescence_objIDs);
2991 fbud.peduncle_objIDs.clear();
2992 fbud.inflorescence_objIDs.clear();
2993 fbud.inflorescence_bases.clear();
2994 break;
2995 }
2996 }
2997 }
2998
2999 // delete any child shoots
3000 if (parent_shoot_ptr->childIDs.find(node) != parent_shoot_ptr->childIDs.end()) {
3001 for (auto childID: parent_shoot_ptr->childIDs.at(node)) {
3002 auto child_shoot = plantarchitecture_ptr->plant_instances.at(plantID).shoot_tree.at(childID);
3003 if (!child_shoot->phytomers.empty()) {
3004 child_shoot->phytomers.front()->deletePhytomer();
3005 }
3006 }
3007 }
3008 }
3009
3010 // delete shoot arrays
3011 parent_shoot_ptr->shoot_internode_radii.resize(this->shoot_index.x);
3012 parent_shoot_ptr->shoot_internode_vertices.resize(this->shoot_index.x);
3013 parent_shoot_ptr->phytomers.resize(this->shoot_index.x);
3014
3015 // set the correct node index for phytomers on this shoot
3016 for (const auto &phytomer: parent_shoot_ptr->phytomers) {
3017 phytomer->shoot_index.y = scast<int>(parent_shoot_ptr->phytomers.size());
3018 }
3019 parent_shoot_ptr->current_node_number = scast<int>(parent_shoot_ptr->phytomers.size());
3020}
3021
3022bool Phytomer::hasLeaf() const {
3023 return (!leaf_bases.empty() && !leaf_bases.front().empty());
3024}
3025
3027 return parent_shoot_ptr->sumShootLeafArea(shoot_index.x);
3028}
3029
3030Shoot::Shoot(uint plant_ID, int shoot_ID, int parent_shoot_ID, uint parent_node, uint parent_petiole_index, uint rank, const helios::vec3 &shoot_base_position, const AxisRotation &shoot_base_rotation, uint current_node_number,
3031 float internode_length_shoot_initial, ShootParameters &shoot_params, std::string shoot_type_label, PlantArchitecture *plant_architecture_ptr) :
3032 current_node_number(current_node_number), base_position(shoot_base_position), base_rotation(shoot_base_rotation), ID(shoot_ID), parent_shoot_ID(parent_shoot_ID), plantID(plant_ID), parent_node_index(parent_node), rank(rank),
3033 parent_petiole_index(parent_petiole_index), internode_length_max_shoot_initial(internode_length_shoot_initial), shoot_parameters(shoot_params), shoot_type_label(std::move(shoot_type_label)), plantarchitecture_ptr(plant_architecture_ptr) {
3034 carbohydrate_pool_molC = 0;
3035 phyllochron_counter = 0;
3036 isdormant = true;
3037 gravitropic_curvature = shoot_params.gravitropic_curvature.val();
3038 context_ptr = plant_architecture_ptr->context_ptr;
3039 phyllochron_instantaneous = shoot_parameters.phyllochron_min.val();
3040 elongation_rate_instantaneous = shoot_parameters.elongation_rate_max.val();
3041
3042 if (parent_shoot_ID >= 0) {
3043 plant_architecture_ptr->plant_instances.at(plantID).shoot_tree.at(parent_shoot_ID)->childIDs[(int) parent_node_index].push_back(shoot_ID);
3044 }
3045}
3046
3047void Shoot::buildShootPhytomers(float internode_radius, float internode_length, float internode_length_scale_factor_fraction, float leaf_scale_factor_fraction, float radius_taper) {
3048 for (int i = 0; i < current_node_number; i++) {
3049 // loop over phytomers to build up the shoot
3050
3051 float taper = 1.f;
3052 if (current_node_number > 1) {
3053 taper = 1.f - radius_taper * float(i) / float(current_node_number - 1);
3054 }
3055
3056 // Adding the phytomer(s) to the shoot
3057 appendPhytomer(internode_radius * taper, internode_length, internode_length_scale_factor_fraction, leaf_scale_factor_fraction, shoot_parameters.phytomer_parameters);
3058 }
3059}
3060
3061std::string Shoot::sampleChildShootType() const {
3062 auto shoot_ptr = this;
3063
3064 assert(shoot_ptr->shoot_parameters.child_shoot_type_labels.size() == shoot_ptr->shoot_parameters.child_shoot_type_probabilities.size());
3065
3066 std::string child_shoot_type_label;
3067
3068 if (shoot_ptr->shoot_parameters.child_shoot_type_labels.empty()) {
3069 // if user doesn't specify child shoot types, generate the same type by default
3070 child_shoot_type_label = shoot_ptr->shoot_type_label;
3071 } else if (shoot_ptr->shoot_parameters.child_shoot_type_labels.size() == 1) {
3072 // if only one child shoot types was specified, use it
3073 child_shoot_type_label = shoot_ptr->shoot_parameters.child_shoot_type_labels.at(0);
3074 } else {
3075 float randf = context_ptr->randu();
3076 int shoot_type_index = -1;
3077 float cumulative_probability = 0;
3078 for (int s = 0; s < shoot_ptr->shoot_parameters.child_shoot_type_labels.size(); s++) {
3079 cumulative_probability += shoot_ptr->shoot_parameters.child_shoot_type_probabilities.at(s);
3080 if (randf < cumulative_probability) {
3081 shoot_type_index = s;
3082 break;
3083 }
3084 }
3085 if (shoot_type_index < 0) {
3086 shoot_type_index = shoot_ptr->shoot_parameters.child_shoot_type_labels.size() - 1;
3087 }
3088 child_shoot_type_label = shoot_ptr->shoot_type_label;
3089 if (shoot_type_index >= 0) {
3090 child_shoot_type_label = shoot_ptr->shoot_parameters.child_shoot_type_labels.at(shoot_type_index);
3091 }
3092 }
3093
3094 return child_shoot_type_label;
3095}
3096
3098 if (node_index >= phytomers.size()) {
3099 helios_runtime_error("ERROR (PlantArchitecture::sampleVegetativeBudBreak): Invalid node index. Node index must be less than the number of phytomers on the shoot.");
3100 }
3101
3102 float probability_min = plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_types_snapshot.at(this->shoot_type_label).vegetative_bud_break_probability_min.val();
3103 float probability_max = plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_types_snapshot.at(this->shoot_type_label).vegetative_bud_break_probability_max.val();
3104 float probability_decay = plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_types_snapshot.at(this->shoot_type_label).vegetative_bud_break_probability_decay_rate.val();
3105
3106 float bud_break_probability;
3107 if (!shoot_parameters.growth_requires_dormancy && probability_decay < 0) {
3108 bud_break_probability = probability_min;
3109 } else if (probability_decay > 0) {
3110 // probability maximum at apex
3111 bud_break_probability = std::fmax(probability_min, probability_max - probability_decay * float(this->current_node_number - node_index - 1));
3112 } else if (probability_decay < 0) {
3113 // probability maximum at base
3114 bud_break_probability = std::fmax(probability_min, probability_max - fabs(probability_decay) * float(node_index));
3115 } else {
3116 if (probability_decay == 0.f) {
3117 bud_break_probability = probability_min;
3118 } else {
3119 bud_break_probability = probability_max;
3120 }
3121 }
3122
3123 bool bud_break = true;
3124 if (context_ptr->randu() > bud_break_probability) {
3125 bud_break = false;
3126 }
3127
3128 return bud_break;
3129}
3130
3131uint Shoot::sampleEpicormicShoot(float dt, std::vector<float> &epicormic_positions_fraction) const {
3132 std::string epicormic_shoot_label = plantarchitecture_ptr->plant_instances.at(this->plantID).epicormic_shoot_probability_perlength_per_day.first;
3133
3134 if (epicormic_shoot_label.empty()) {
3135 return 0;
3136 }
3137
3138 float epicormic_probability = plantarchitecture_ptr->plant_instances.at(this->plantID).epicormic_shoot_probability_perlength_per_day.second;
3139
3140 if (epicormic_probability == 0) {
3141 return 0;
3142 }
3143
3144 uint Nshoots = 0;
3145
3146 epicormic_positions_fraction.clear();
3147
3148 float shoot_length = this->calculateShootLength();
3149
3150 float time = dt;
3151 while (time > 0) {
3152 float dta = std::min(time, 1.f);
3153
3154 float shoot_fraction = context_ptr->randu();
3155
3156 float elevation = fabs(getShootAxisVector(shoot_fraction).z);
3157
3158 bool new_shoot = uint((epicormic_probability * shoot_length * dta * elevation > context_ptr->randu()));
3159
3160 Nshoots += uint(new_shoot);
3161
3162 if (new_shoot) {
3163 epicormic_positions_fraction.push_back(shoot_fraction);
3164 }
3165
3166 time -= dta;
3167 }
3168
3169 assert(epicormic_positions_fraction.size() == Nshoots);
3170
3171 return Nshoots;
3172}
3173
3174uint PlantArchitecture::addBaseStemShoot(uint plantID, uint current_node_number, const AxisRotation &base_rotation, float internode_radius, float internode_length_max, float internode_length_scale_factor_fraction, float leaf_scale_factor_fraction,
3175 float radius_taper, const std::string &shoot_type_label) {
3176 if (plant_instances.find(plantID) == plant_instances.end()) {
3177 helios_runtime_error("ERROR (PlantArchitecture::addBaseStemShoot): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3178 } else if (plant_instances.at(plantID).shoot_types_snapshot.find(shoot_type_label) == plant_instances.at(plantID).shoot_types_snapshot.end()) {
3179 helios_runtime_error("ERROR (PlantArchitecture::addBaseStemShoot): Shoot type with label of " + shoot_type_label + " does not exist.");
3180 }
3181
3182 auto shoot_tree_ptr = &plant_instances.at(plantID).shoot_tree;
3183
3184 auto shoot_parameters = plant_instances.at(plantID).shoot_types_snapshot.at(shoot_type_label);
3185 validateShootTypes(shoot_parameters, plant_instances.at(plantID).shoot_types_snapshot);
3186
3187 if (current_node_number > shoot_parameters.max_nodes.val()) {
3188 helios_runtime_error("ERROR (PlantArchitecture::addBaseStemShoot): Cannot add shoot with " + std::to_string(current_node_number) + " nodes since the specified max node number is " + std::to_string(shoot_parameters.max_nodes.val()) + ".");
3189 }
3190
3191 uint shootID = shoot_tree_ptr->size();
3192 vec3 base_position = plant_instances.at(plantID).base_position;
3193
3194 // Create the new shoot
3195 auto *shoot_new = (new Shoot(plantID, shootID, -1, 0, 0, 0, base_position, base_rotation, current_node_number, internode_length_max, shoot_parameters, shoot_type_label, this));
3196 shoot_tree_ptr->emplace_back(shoot_new);
3197
3198 // Build phytomer geometry
3199 shoot_new->buildShootPhytomers(internode_radius, internode_length_max, internode_length_scale_factor_fraction, leaf_scale_factor_fraction, radius_taper);
3200
3201 return shootID;
3202}
3203
3204uint PlantArchitecture::appendShoot(uint plantID, int parent_shoot_ID, uint current_node_number, const AxisRotation &base_rotation, float internode_radius, float internode_length_max, float internode_length_scale_factor_fraction,
3205 float leaf_scale_factor_fraction, float radius_taper, const std::string &shoot_type_label) {
3206 if (plant_instances.find(plantID) == plant_instances.end()) {
3207 helios_runtime_error("ERROR (PlantArchitecture::appendShoot): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3208 } else if (plant_instances.at(plantID).shoot_types_snapshot.find(shoot_type_label) == plant_instances.at(plantID).shoot_types_snapshot.end()) {
3209 helios_runtime_error("ERROR (PlantArchitecture::appendShoot): Shoot type with label of " + shoot_type_label + " does not exist.");
3210 }
3211
3212 auto shoot_tree_ptr = &plant_instances.at(plantID).shoot_tree;
3213
3214 auto shoot_parameters = plant_instances.at(plantID).shoot_types_snapshot.at(shoot_type_label);
3215 validateShootTypes(shoot_parameters, plant_instances.at(plantID).shoot_types_snapshot);
3216
3217 if (shoot_tree_ptr->empty()) {
3218 helios_runtime_error("ERROR (PlantArchitecture::appendShoot): Cannot append shoot to empty shoot. You must call addBaseStemShoot() first for each plant.");
3219 } else if (parent_shoot_ID >= int(shoot_tree_ptr->size())) {
3220 helios_runtime_error("ERROR (PlantArchitecture::appendShoot): Parent with ID of " + std::to_string(parent_shoot_ID) + " does not exist.");
3221 } else if (current_node_number > shoot_parameters.max_nodes.val()) {
3222 helios_runtime_error("ERROR (PlantArchitecture::appendShoot): Cannot add shoot with " + std::to_string(current_node_number) + " nodes since the specified max node number is " + std::to_string(shoot_parameters.max_nodes.val()) + ".");
3223 } else if (shoot_tree_ptr->at(parent_shoot_ID)->phytomers.empty()) {
3224 }
3225
3226 // stop parent shoot from producing new phytomers at the apex
3227 shoot_tree_ptr->at(parent_shoot_ID)->shoot_parameters.max_nodes = shoot_tree_ptr->at(parent_shoot_ID)->current_node_number;
3228 shoot_tree_ptr->at(parent_shoot_ID)->terminateApicalBud(); // meristem should not keep growing after appending shoot
3229
3230 // accumulate all the values that will be passed to Shoot constructor
3231 int appended_shootID = int(shoot_tree_ptr->size());
3232 uint parent_node = shoot_tree_ptr->at(parent_shoot_ID)->current_node_number - 1;
3233 uint rank = shoot_tree_ptr->at(parent_shoot_ID)->rank;
3234 vec3 base_position = interpolateTube(shoot_tree_ptr->at(parent_shoot_ID)->phytomers.back()->getInternodeNodePositions(), 0.9f);
3235
3236 // Create the new shoot
3237 auto *shoot_new = (new Shoot(plantID, appended_shootID, parent_shoot_ID, parent_node, 0, rank, base_position, base_rotation, current_node_number, internode_length_max, shoot_parameters, shoot_type_label, this));
3238 shoot_tree_ptr->emplace_back(shoot_new);
3239
3240 // Build phytomer geometry
3241 shoot_new->buildShootPhytomers(internode_radius, internode_length_max, internode_length_scale_factor_fraction, leaf_scale_factor_fraction, radius_taper);
3242
3243 return appended_shootID;
3244}
3245
3246uint PlantArchitecture::addChildShoot(uint plantID, int parent_shoot_ID, uint parent_node_index, uint current_node_number, const AxisRotation &shoot_base_rotation, float internode_radius, float internode_length_max,
3247 float internode_length_scale_factor_fraction, float leaf_scale_factor_fraction, float radius_taper, const std::string &shoot_type_label, uint petiole_index) {
3248 if (plant_instances.find(plantID) == plant_instances.end()) {
3249 helios_runtime_error("ERROR (PlantArchitecture::addChildShoot): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3250 } else if (plant_instances.at(plantID).shoot_types_snapshot.find(shoot_type_label) == plant_instances.at(plantID).shoot_types_snapshot.end()) {
3251 helios_runtime_error("ERROR (PlantArchitecture::addChildShoot): Shoot type with label of " + shoot_type_label + " does not exist.");
3252 }
3253
3254 auto shoot_tree_ptr = &plant_instances.at(plantID).shoot_tree;
3255
3256 if (parent_shoot_ID <= -1 || parent_shoot_ID >= shoot_tree_ptr->size()) {
3257 helios_runtime_error("ERROR (PlantArchitecture::addChildShoot): Parent with ID of " + std::to_string(parent_shoot_ID) + " does not exist.");
3258 } else if (shoot_tree_ptr->at(parent_shoot_ID)->phytomers.size() <= parent_node_index) {
3259 helios_runtime_error("ERROR (PlantArchitecture::addChildShoot): Parent shoot does not have a node " + std::to_string(parent_node_index) + ".");
3260 }
3261
3262 // accumulate all the values that will be passed to Shoot constructor
3263 auto shoot_parameters = plant_instances.at(plantID).shoot_types_snapshot.at(shoot_type_label);
3264 validateShootTypes(shoot_parameters, plant_instances.at(plantID).shoot_types_snapshot);
3265 uint parent_rank = (int) shoot_tree_ptr->at(parent_shoot_ID)->rank;
3266 int childID = int(shoot_tree_ptr->size());
3267
3268 // Calculate the position of the shoot base
3269 const auto parent_shoot_ptr = shoot_tree_ptr->at(parent_shoot_ID);
3270
3271 vec3 shoot_base_position = parent_shoot_ptr->shoot_internode_vertices.at(parent_node_index).back();
3272
3273 // Shift the shoot base position outward by the parent internode radius
3274 vec3 axis_vector;
3275 if (parent_shoot_ptr->phytomers.at(parent_node_index)->petiole_vertices.empty()) {
3276 // No petioles - use internode axis instead
3277 axis_vector = parent_shoot_ptr->phytomers.at(parent_node_index)->getInternodeAxisVector(1.f);
3278 } else {
3279 axis_vector = parent_shoot_ptr->phytomers.at(parent_node_index)->getPetioleAxisVector(0, petiole_index);
3280 }
3281 shoot_base_position += 0.9f * axis_vector * parent_shoot_ptr->phytomers.at(parent_node_index)->getInternodeRadius(1.f);
3282
3283 // Create the new shoot
3284 auto *shoot_new = (new Shoot(plantID, childID, parent_shoot_ID, parent_node_index, petiole_index, parent_rank + 1, shoot_base_position, shoot_base_rotation, current_node_number, internode_length_max, shoot_parameters, shoot_type_label, this));
3285 shoot_tree_ptr->emplace_back(shoot_new);
3286
3287 // Build phytomer geometry
3288 shoot_new->buildShootPhytomers(internode_radius, internode_length_max, internode_length_scale_factor_fraction, leaf_scale_factor_fraction, radius_taper);
3289
3290 return childID;
3291}
3292
3293uint PlantArchitecture::addEpicormicShoot(uint plantID, int parent_shoot_ID, float parent_position_fraction, uint current_node_number, float zenith_perturbation_degrees, float internode_radius, float internode_length_max,
3294 float internode_length_scale_factor_fraction, float leaf_scale_factor_fraction, float radius_taper, const std::string &shoot_type_label) {
3295 if (plant_instances.find(plantID) == plant_instances.end()) {
3296 helios_runtime_error("ERROR (PlantArchitecture::addEpicormicShoot): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3297 } else if (plant_instances.at(plantID).shoot_types_snapshot.find(shoot_type_label) == plant_instances.at(plantID).shoot_types_snapshot.end()) {
3298 helios_runtime_error("ERROR (PlantArchitecture::addEpicormicShoot): Shoot type with label of " + shoot_type_label + " does not exist.");
3299 }
3300
3301 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_ID);
3302
3303 uint parent_node_index = 0;
3304 if (parent_position_fraction > 0) {
3305 parent_node_index = std::ceil(parent_position_fraction * float(parent_shoot->phytomers.size())) - 1;
3306 }
3307
3308 vec3 axis_vector;
3309 if (plant_instances.at(plantID).shoot_tree.at(parent_shoot_ID)->phytomers.at(parent_node_index)->petiole_vertices.empty()) {
3310 // No petioles - use internode axis instead
3311 axis_vector = plant_instances.at(plantID).shoot_tree.at(parent_shoot_ID)->phytomers.at(parent_node_index)->getInternodeAxisVector(1.f);
3312 } else {
3313 axis_vector = plant_instances.at(plantID).shoot_tree.at(parent_shoot_ID)->phytomers.at(parent_node_index)->getPetioleAxisVector(0, 0);
3314 }
3315
3316 //\todo Figuring out how to set this correctly to make the shoot vertical, which avoids having to write a child shoot function.
3317 AxisRotation base_rotation = make_AxisRotation(0, acos_safe(axis_vector.z), 0);
3318
3319 return addChildShoot(plantID, parent_shoot_ID, parent_node_index, current_node_number, base_rotation, internode_radius, internode_length_max, internode_length_scale_factor_fraction, leaf_scale_factor_fraction, radius_taper, shoot_type_label, 0);
3320}
3321
3322void PlantArchitecture::validateShootTypes(ShootParameters &shoot_parameters, const std::map<std::string, ShootParameters> &shoot_types_ref) const {
3323 assert(shoot_parameters.child_shoot_type_probabilities.size() == shoot_parameters.child_shoot_type_labels.size());
3324
3325 for (int ind = shoot_parameters.child_shoot_type_labels.size() - 1; ind >= 0; ind--) {
3326 if (shoot_types_ref.find(shoot_parameters.child_shoot_type_labels.at(ind)) == shoot_types_ref.end()) {
3327 shoot_parameters.child_shoot_type_labels.erase(shoot_parameters.child_shoot_type_labels.begin() + ind);
3328 shoot_parameters.child_shoot_type_probabilities.erase(shoot_parameters.child_shoot_type_probabilities.begin() + ind);
3329 }
3330 }
3331}
3332
3333int PlantArchitecture::appendPhytomerToShoot(uint plantID, uint shootID, const PhytomerParameters &phytomer_parameters, float internode_radius, float internode_length_max, float internode_length_scale_factor_fraction,
3334 float leaf_scale_factor_fraction) {
3335 if (plant_instances.find(plantID) == plant_instances.end()) {
3336 helios_runtime_error("ERROR (PlantArchitecture::appendPhytomerToShoot): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3337 }
3338
3339 auto shoot_tree_ptr = &plant_instances.at(plantID).shoot_tree;
3340
3341 if (shootID >= shoot_tree_ptr->size()) {
3342 helios_runtime_error("ERROR (PlantArchitecture::appendPhytomerToShoot): Parent with ID of " + std::to_string(shootID) + " does not exist.");
3343 }
3344
3345 auto current_shoot_ptr = plant_instances.at(plantID).shoot_tree.at(shootID);
3346
3347 int pID = current_shoot_ptr->appendPhytomer(internode_radius, internode_length_max, internode_length_scale_factor_fraction, leaf_scale_factor_fraction, phytomer_parameters);
3348
3349 current_shoot_ptr->current_node_number++;
3350 current_shoot_ptr->nodes_this_season++;
3351
3352 for (auto &phytomers: current_shoot_ptr->phytomers) {
3353 phytomers->shoot_index.y = current_shoot_ptr->current_node_number;
3354 }
3355
3356 // If this shoot reached max nodes, add a terminal floral bud if max_terminal_floral_buds > 0
3357 if (current_shoot_ptr->current_node_number == current_shoot_ptr->shoot_parameters.max_nodes.val()) {
3358 if (!current_shoot_ptr->shoot_parameters.flowers_require_dormancy && current_shoot_ptr->shoot_parameters.max_terminal_floral_buds.val() > 0) {
3359 current_shoot_ptr->addTerminalFloralBud();
3360 BudState state;
3361 if (current_shoot_ptr->shoot_parameters.phytomer_parameters.inflorescence.flower_prototype_function != nullptr) {
3362 state = BUD_FLOWER_CLOSED;
3363 } else if (current_shoot_ptr->shoot_parameters.phytomer_parameters.inflorescence.fruit_prototype_function != nullptr) {
3364 state = BUD_FRUITING;
3365 } else {
3366 return pID;
3367 }
3368 for (auto &fbuds: current_shoot_ptr->phytomers.back()->floral_buds) {
3369 for (auto &fbud: fbuds) {
3370 if (fbud.isterminal) {
3371 fbud.state = state;
3372 current_shoot_ptr->phytomers.back()->updateInflorescence(fbud);
3373 }
3374 }
3375 }
3376 }
3377 }
3378
3379 // If this shoot reached the max nodes for the season, add a dormant floral bud and make terminal vegetative bud dormant
3380 else if (current_shoot_ptr->nodes_this_season >= current_shoot_ptr->shoot_parameters.max_nodes_per_season.val()) {
3381 if (!current_shoot_ptr->shoot_parameters.flowers_require_dormancy && current_shoot_ptr->shoot_parameters.max_terminal_floral_buds.val() > 0) {
3382 current_shoot_ptr->addTerminalFloralBud();
3383 for (auto &fbuds: current_shoot_ptr->phytomers.back()->floral_buds) {
3384 for (auto &fbud: fbuds) {
3385 if (fbud.isterminal) {
3386 fbud.state = BUD_DORMANT;
3387 current_shoot_ptr->phytomers.back()->updateInflorescence(fbud);
3388 }
3389 }
3390 }
3391 }
3392 current_shoot_ptr->phytomers.at(pID)->isdormant = true;
3393 }
3394
3395 return pID;
3396}
3397
3398void PlantArchitecture::enableEpicormicChildShoots(uint plantID, const std::string &epicormic_shoot_type_label, float epicormic_probability_perlength_perday) {
3399 if (plant_instances.find(plantID) == plant_instances.end()) {
3400 helios_runtime_error("ERROR (PlantArchitecture::enableEpicormicChildShoots): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3401 } else if (plant_instances.at(plantID).shoot_types_snapshot.find(epicormic_shoot_type_label) == plant_instances.at(plantID).shoot_types_snapshot.end()) {
3402 helios_runtime_error("ERROR (PlantArchitecture::enableEpicormicChildShoots): Shoot type with label of " + epicormic_shoot_type_label + " does not exist.");
3403 } else if (epicormic_probability_perlength_perday < 0) {
3404 helios_runtime_error("ERROR (PlantArchitecture::enableEpicormicChildShoots): Epicormic probability must be greater than or equal to zero.");
3405 }
3406
3407 plant_instances.at(plantID).epicormic_shoot_probability_perlength_per_day = std::make_pair(epicormic_shoot_type_label, epicormic_probability_perlength_perday);
3408}
3409
3411 build_context_geometry_internode = false;
3412}
3413
3415 build_context_geometry_petiole = false;
3416}
3417
3419 build_context_geometry_peduncle = false;
3420}
3421
3423 ground_clipping_height = ground_height;
3424}
3425
3426void PlantArchitecture::incrementPhytomerInternodeGirth(uint plantID, uint shootID, uint node_number, float dt, bool update_context_geometry) {
3427 if (plant_instances.find(plantID) == plant_instances.end()) {
3428 helios_runtime_error("ERROR (PlantArchitecture::incrementPhytomerInternodeGirth): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3429 }
3430
3431 auto shoot = plant_instances.at(plantID).shoot_tree.at(shootID);
3432
3433 if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
3434 helios_runtime_error("ERROR (PlantArchitecture::incrementPhytomerInternodeGirth): Shoot with ID of " + std::to_string(shootID) + " does not exist.");
3435 } else if (node_number >= shoot->current_node_number) {
3436 helios_runtime_error("ERROR (PlantArchitecture::incrementPhytomerInternodeGirth): Cannot scale internode " + std::to_string(node_number) + " because there are only " + std::to_string(shoot->current_node_number) + " nodes in this shoot.");
3437 }
3438
3439 auto phytomer = shoot->phytomers.at(node_number);
3440
3441 float leaf_area = phytomer->downstream_leaf_area;
3442
3443 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
3444 context_ptr->setObjectData(shoot->internode_tube_objID, "leaf_area", leaf_area);
3445 }
3446 float phytomer_age = phytomer->age;
3447 float girth_area_factor = shoot->shoot_parameters.girth_area_factor.val();
3448 if (phytomer_age > 365) {
3449 girth_area_factor = shoot->shoot_parameters.girth_area_factor.val() * 365 / phytomer_age;
3450 }
3451
3452 float internode_area = girth_area_factor * leaf_area * 1e-4;
3453 float phytomer_radius = sqrtf(internode_area / PI_F);
3454
3455 auto &segment = shoot->shoot_internode_radii.at(node_number);
3456 for (float &radius: segment) {
3457 if (phytomer_radius > radius) {
3458 // radius should only increase
3459 radius = radius + 0.5 * (phytomer_radius - radius);
3460 }
3461 }
3462
3463 if (update_context_geometry && context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
3464 context_ptr->setTubeRadii(shoot->internode_tube_objID, flatten(shoot->shoot_internode_radii));
3465 }
3466}
3467
3468void PlantArchitecture::pruneGroundCollisions(uint plantID) {
3469 if (plant_instances.find(plantID) == plant_instances.end()) {
3470 helios_runtime_error("ERROR (PlantArchitecture::pruneGroundCollisions): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3471 }
3472
3473 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
3474 for (auto &phytomer: shoot->phytomers) {
3475 // internode
3476 if ((phytomer->shoot_index.x == 0 && phytomer->rank > 0) && context_ptr->doesObjectExist(shoot->internode_tube_objID) && detectGroundCollision(shoot->internode_tube_objID)) {
3477 context_ptr->deleteObject(shoot->internode_tube_objID);
3478 shoot->terminateApicalBud();
3479 }
3480
3481 // leaves
3482 for (uint petiole = 0; petiole < phytomer->leaf_objIDs.size(); petiole++) {
3483 if (detectGroundCollision(phytomer->leaf_objIDs.at(petiole))) {
3484 phytomer->removeLeaf();
3485 }
3486 }
3487
3488 // inflorescence
3489 for (auto &petiole: phytomer->floral_buds) {
3490 for (auto &fbud: petiole) {
3491 for (int p = fbud.inflorescence_objIDs.size() - 1; p >= 0; p--) {
3492 uint objID = fbud.inflorescence_objIDs.at(p);
3493 if (detectGroundCollision(objID)) {
3494 context_ptr->deleteObject(objID);
3495 fbud.inflorescence_objIDs.erase(fbud.inflorescence_objIDs.begin() + p);
3496 fbud.inflorescence_bases.erase(fbud.inflorescence_bases.begin() + p);
3497 }
3498 }
3499 for (int p = fbud.peduncle_objIDs.size() - 1; p >= 0; p--) {
3500 uint objID = fbud.peduncle_objIDs.at(p);
3501 if (detectGroundCollision(objID)) {
3502 context_ptr->deleteObject(fbud.peduncle_objIDs);
3503 context_ptr->deleteObject(fbud.inflorescence_objIDs);
3504 fbud.peduncle_objIDs.clear();
3505 fbud.inflorescence_objIDs.clear();
3506 fbud.inflorescence_bases.clear();
3507 break;
3508 }
3509 }
3510 }
3511 }
3512 }
3513 }
3514
3515 // prune the shoots if all downstream leaves have been removed
3516 // for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
3517 // int node = -1;
3518 // for ( node = shoot->phytomers.size() - 2; node >= 0; node--) {
3519 // if ( shoot->phytomers.size() > node && shoot->phytomers.at(node)->hasLeaf() ) {
3520 // break;
3521 // }else {
3522 // }
3523 // }
3524 // if ( node>=0 && node+1 < shoot-> phytomers.size()-1 ) {
3525 // pruneBranch(plantID, shoot->ID, node+1);
3526 // }
3527 // }
3528}
3529
3530void PlantArchitecture::setPhytomerLeafScale(uint plantID, uint shootID, uint node_number, float leaf_scale_factor_fraction) {
3531 if (plant_instances.find(plantID) == plant_instances.end()) {
3532 helios_runtime_error("ERROR (PlantArchitecture::setPhytomerLeafScale): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3533 }
3534
3535 auto parent_shoot = plant_instances.at(plantID).shoot_tree.at(shootID);
3536
3537 if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
3538 helios_runtime_error("ERROR (PlantArchitecture::setPhytomerLeafScale): Shoot with ID of " + std::to_string(shootID) + " does not exist.");
3539 } else if (node_number >= parent_shoot->current_node_number) {
3540 helios_runtime_error("ERROR (PlantArchitecture::setPhytomerLeafScale): Cannot scale leaf " + std::to_string(node_number) + " because there are only " + std::to_string(parent_shoot->current_node_number) + " nodes in this shoot.");
3541 }
3542 if (leaf_scale_factor_fraction < 0 || leaf_scale_factor_fraction > 1) {
3543 return;
3544 }
3545
3546 parent_shoot->phytomers.at(node_number)->setLeafScaleFraction(leaf_scale_factor_fraction);
3547}
3548
3549void PlantArchitecture::setPlantBasePosition(uint plantID, const helios::vec3 &base_position) {
3550 if (plant_instances.find(plantID) == plant_instances.end()) {
3551 helios_runtime_error("ERROR (PlantArchitecture::setPlantBasePosition): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3552 }
3553
3554 plant_instances.at(plantID).base_position = base_position;
3555
3556 //\todo Does not work after shoots have been added to the plant.
3557 if (!plant_instances.at(plantID).shoot_tree.empty()) {
3558 }
3559}
3560
3561void PlantArchitecture::setPlantLeafElevationAngleDistribution(uint plantID, float Beta_mu_inclination, float Beta_nu_inclination) const {
3562 if (Beta_mu_inclination <= 0.f) {
3563 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafElevationAngleDistribution): Beta_mu_inclination must be greater than or equal to zero.");
3564 } else if (Beta_nu_inclination <= 0.f) {
3565 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafElevationAngleDistribution): Beta_nu_inclination must be greater than or equal to zero.");
3566 }
3567
3568 setPlantLeafAngleDistribution_private({plantID}, Beta_mu_inclination, Beta_nu_inclination, 0.f, 0.f, true, false);
3569}
3570
3571void PlantArchitecture::setPlantLeafElevationAngleDistribution(const std::vector<uint> &plantIDs, float Beta_mu_inclination, float Beta_nu_inclination) const {
3572 if (Beta_mu_inclination <= 0.f) {
3573 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafElevationAngleDistribution): Beta_mu_inclination must be greater than or equal to zero.");
3574 } else if (Beta_nu_inclination <= 0.f) {
3575 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafElevationAngleDistribution): Beta_nu_inclination must be greater than or equal to zero.");
3576 }
3577
3578 setPlantLeafAngleDistribution_private(plantIDs, Beta_mu_inclination, Beta_nu_inclination, 0.f, 0.f, true, false);
3579}
3580
3581void PlantArchitecture::setPlantLeafAzimuthAngleDistribution(uint plantID, float eccentricity, float ellipse_rotation_degrees) const {
3582 if (eccentricity < 0.f || eccentricity > 1.f) {
3583 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAzimuthAngleDistribution): Eccentricity must be between 0 and 1.");
3584 }
3585
3586 setPlantLeafAngleDistribution_private({plantID}, 0.f, 0.f, eccentricity, ellipse_rotation_degrees, false, true);
3587}
3588
3589void PlantArchitecture::setPlantLeafAzimuthAngleDistribution(const std::vector<uint> &plantIDs, float eccentricity, float ellipse_rotation_degrees) const {
3590 if (eccentricity < 0.f || eccentricity > 1.f) {
3591 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAzimuthAngleDistribution): Eccentricity must be between 0 and 1.");
3592 }
3593
3594 setPlantLeafAngleDistribution_private(plantIDs, 0.f, 0.f, eccentricity, ellipse_rotation_degrees, false, true);
3595}
3596
3597void PlantArchitecture::setPlantLeafAngleDistribution(uint plantID, float Beta_mu_inclination, float Beta_nu_inclination, float eccentricity, float ellipse_rotation_degrees) const {
3598 if (Beta_mu_inclination <= 0.f) {
3599 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAngleDistribution): Beta_mu_inclination must be greater than or equal to zero.");
3600 } else if (Beta_nu_inclination <= 0.f) {
3601 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAngleDistribution): Beta_nu_inclination must be greater than or equal to zero.");
3602 } else if (eccentricity < 0.f || eccentricity > 1.f) {
3603 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAngleDistribution): Eccentricity must be between 0 and 1.");
3604 }
3605
3606 setPlantLeafAngleDistribution_private({plantID}, Beta_mu_inclination, Beta_nu_inclination, eccentricity, ellipse_rotation_degrees, true, true);
3607}
3608
3609void PlantArchitecture::setPlantLeafAngleDistribution(const std::vector<uint> &plantIDs, float Beta_mu_inclination, float Beta_nu_inclination, float eccentricity, float ellipse_rotation_degrees) const {
3610 if (Beta_mu_inclination <= 0.f) {
3611 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAngleDistribution): Beta_mu_inclination must be greater than or equal to zero.");
3612 } else if (Beta_nu_inclination <= 0.f) {
3613 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAngleDistribution): Beta_nu_inclination must be greater than or equal to zero.");
3614 } else if (eccentricity < 0.f || eccentricity > 1.f) {
3615 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAngleDistribution): Eccentricity must be between 0 and 1.");
3616 }
3617
3618 setPlantLeafAngleDistribution_private(plantIDs, Beta_mu_inclination, Beta_nu_inclination, eccentricity, ellipse_rotation_degrees, true, true);
3619}
3620
3621
3623 if (plant_instances.find(plantID) == plant_instances.end()) {
3624 helios_runtime_error("ERROR (PlantArchitecture::setPlantBasePosition): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3625 } else if (plant_instances.at(plantID).shoot_tree.empty()) {
3626 helios_runtime_error("ERROR (PlantArchitecture::setPlantBasePosition): Plant with ID of " + std::to_string(plantID) + " has no shoots, so could not get a base position.");
3627 }
3628 return plant_instances.at(plantID).base_position;
3629}
3630
3631std::vector<helios::vec3> PlantArchitecture::getPlantBasePosition(const std::vector<uint> &plantIDs) const {
3632 std::vector<vec3> positions;
3633 positions.reserve(plantIDs.size());
3634 for (uint plantID: plantIDs) {
3635 positions.push_back(getPlantBasePosition(plantID));
3636 }
3637 return positions;
3638}
3639
3641 if (plant_instances.find(plantID) == plant_instances.end()) {
3642 helios_runtime_error("ERROR (PlantArchitecture::sumPlantLeafArea): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3643 }
3644
3645 std::vector<uint> leaf_objIDs = getPlantLeafObjectIDs(plantID);
3646
3647 float area = 0;
3648 for (uint objID: leaf_objIDs) {
3649 area += context_ptr->getObjectArea(objID);
3650 }
3651
3652 return area;
3653}
3654
3656 if (plant_instances.find(plantID) == plant_instances.end()) {
3657 helios_runtime_error("ERROR (PlantArchitecture::getPlantStemHeight): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3658 }
3659
3660 auto base_shoot_ptr = plant_instances.at(plantID).shoot_tree.front();
3661
3662 std::vector<uint> stem_objID{base_shoot_ptr->internode_tube_objID};
3663
3664 if (!context_ptr->doesObjectExist(stem_objID.front())) {
3665 helios_runtime_error("ERROR (PlantArchitecture::getPlantStemHeight): The plant does not contain any geometry.");
3666 }
3667
3668 // check if there was an appended shoot on this same shoot
3669 if (base_shoot_ptr->childIDs.find(base_shoot_ptr->current_node_number - 1) != base_shoot_ptr->childIDs.end()) {
3670 auto terminal_children = base_shoot_ptr->childIDs.at(base_shoot_ptr->current_node_number - 1);
3671 for (uint childID: terminal_children) {
3672 auto child_shoot_ptr = plant_instances.at(plantID).shoot_tree.at(childID);
3673 if (child_shoot_ptr->rank == base_shoot_ptr->rank) {
3674 if (context_ptr->doesObjectExist(child_shoot_ptr->internode_tube_objID)) {
3675 stem_objID.push_back(child_shoot_ptr->internode_tube_objID);
3676 }
3677 }
3678 }
3679 }
3680
3681 vec3 min_box;
3682 vec3 max_box;
3683 context_ptr->getObjectBoundingBox(stem_objID, min_box, max_box);
3684
3685 return max_box.z - min_box.z;
3686}
3687
3688
3690 if (plant_instances.find(plantID) == plant_instances.end()) {
3691 helios_runtime_error("ERROR (PlantArchitecture::getPlantHeight): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3692 }
3693
3694 vec3 min_box;
3695 vec3 max_box;
3696 context_ptr->getObjectBoundingBox(getAllPlantObjectIDs(plantID), min_box, max_box);
3697
3698 return max_box.z - min_box.z;
3699}
3700
3701std::vector<float> PlantArchitecture::getPlantLeafInclinationAngleDistribution(uint plantID, uint Nbins, bool normalize) const {
3702 if (plant_instances.find(plantID) == plant_instances.end()) {
3703 helios_runtime_error("ERROR (PlantArchitecture::getPlantLeafInclinationAngleDistribution): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3704 }
3705
3706 const std::vector<uint> leaf_objIDs = getPlantLeafObjectIDs(plantID);
3707 const std::vector<uint> leaf_UUIDs = context_ptr->getObjectPrimitiveUUIDs(leaf_objIDs);
3708
3709 std::vector<float> leaf_inclination_angles(Nbins, 0.f);
3710 const float dtheta = 0.5f * PI_F / float(Nbins);
3711 for (const uint UUID: leaf_UUIDs) {
3712 const vec3 normal = context_ptr->getPrimitiveNormal(UUID);
3713 const float theta = acos_safe(fabs(normal.z));
3714 const float area = context_ptr->getPrimitiveArea(UUID);
3715 uint bin = static_cast<uint>(std::floor(theta / dtheta));
3716 if (bin >= Nbins) {
3717 bin = Nbins - 1; // Ensure bin index is within range
3718 }
3719 if (!std::isnan(area)) {
3720 leaf_inclination_angles.at(bin) += area;
3721 }
3722 }
3723
3724 if (normalize) {
3725 const float sum = helios::sum(leaf_inclination_angles);
3726 if (sum > 0.f) {
3727 for (float &angle: leaf_inclination_angles) {
3728 angle /= sum;
3729 }
3730 }
3731 }
3732
3733 return leaf_inclination_angles;
3734}
3735
3736std::vector<float> PlantArchitecture::getPlantLeafInclinationAngleDistribution(const std::vector<uint> &plantIDs, uint Nbins, bool normalize) const {
3737 std::vector<float> leaf_inclination_angles(Nbins, 0.f);
3738 for (const uint plantID: plantIDs) {
3739 leaf_inclination_angles += getPlantLeafInclinationAngleDistribution(plantID, Nbins, false);
3740 }
3741
3742 if (normalize) {
3743 const float sum = helios::sum(leaf_inclination_angles);
3744 if (sum > 0.f) {
3745 for (float &angle: leaf_inclination_angles) {
3746 angle /= sum;
3747 }
3748 }
3749 }
3750
3751 return leaf_inclination_angles;
3752}
3753
3754std::vector<float> PlantArchitecture::getPlantLeafAzimuthAngleDistribution(uint plantID, uint Nbins, bool normalize) const {
3755 if (plant_instances.find(plantID) == plant_instances.end()) {
3756 helios_runtime_error("ERROR (PlantArchitecture::getPlantLeafAzimuthAngleDistribution): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3757 }
3758
3759 const std::vector<uint> leaf_objIDs = getPlantLeafObjectIDs(plantID);
3760 const std::vector<uint> leaf_UUIDs = context_ptr->getObjectPrimitiveUUIDs(leaf_objIDs);
3761
3762 std::vector<float> leaf_azimuth_angles(Nbins, 0.f);
3763 const float dtheta = 2.f * PI_F / static_cast<float>(Nbins);
3764 for (const uint UUID: leaf_UUIDs) {
3765 const vec3 normal = context_ptr->getPrimitiveNormal(UUID);
3766 const float phi = cart2sphere(normal).azimuth;
3767 const float area = context_ptr->getPrimitiveArea(UUID);
3768 uint bin = static_cast<uint>(std::floor(phi / dtheta));
3769 if (bin >= Nbins) {
3770 bin = Nbins - 1; // Ensure bin index is within range
3771 }
3772 if (!std::isnan(area)) {
3773 leaf_azimuth_angles.at(bin) += area;
3774 }
3775 }
3776
3777 if (normalize) {
3778 const float sum = helios::sum(leaf_azimuth_angles);
3779 if (sum > 0.f) {
3780 for (float &angle: leaf_azimuth_angles) {
3781 angle /= sum;
3782 }
3783 }
3784 }
3785
3786 return leaf_azimuth_angles;
3787}
3788
3789std::vector<float> PlantArchitecture::getPlantLeafAzimuthAngleDistribution(const std::vector<uint> &plantIDs, uint Nbins, bool normalize) const {
3790 std::vector<float> leaf_azimuth_angles(Nbins, 0.f);
3791 for (const uint plantID: plantIDs) {
3792 leaf_azimuth_angles += getPlantLeafAzimuthAngleDistribution(plantID, Nbins, false);
3793 }
3794
3795 if (normalize) {
3796 const float sum = helios::sum(leaf_azimuth_angles);
3797 if (sum > 0.f) {
3798 for (float &angle: leaf_azimuth_angles) {
3799 angle /= sum;
3800 }
3801 }
3802 }
3803
3804 return leaf_azimuth_angles;
3805}
3806
3807
3809 if (plant_instances.find(plantID) == plant_instances.end()) {
3810 helios_runtime_error("ERROR (PlantArchitecture::getPlantLeafCount): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3811 }
3812
3813 return getPlantLeafObjectIDs(plantID).size();
3814}
3815
3816std::vector<helios::vec3> PlantArchitecture::getPlantLeafBases(uint plantID) const {
3817 if (plant_instances.find(plantID) == plant_instances.end()) {
3818 helios_runtime_error("ERROR (PlantArchitecture::getPlantLeafBases): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3819 }
3820
3821 std::vector<vec3> leaf_bases;
3822
3823 // First calculate total size needed to avoid reallocations
3824 size_t total_size = 0;
3825 for (const auto &shoot: plant_instances.at(plantID).shoot_tree) {
3826 for (const auto &phytomer: shoot->phytomers) {
3827 total_size += phytomer->leaf_bases.size() * phytomer->leaf_bases.front().size();
3828 }
3829 }
3830 leaf_bases.reserve(total_size);
3831
3832 // Now collect all leaf bases by appending at the end
3833 for (const auto &shoot: plant_instances.at(plantID).shoot_tree) {
3834 for (const auto &phytomer: shoot->phytomers) {
3835 std::vector<vec3> bases_flat = flatten(phytomer->leaf_bases);
3836 leaf_bases.insert(leaf_bases.end(), bases_flat.begin(), bases_flat.end());
3837 }
3838 }
3839
3840 return leaf_bases;
3841}
3842
3843std::vector<helios::vec3> PlantArchitecture::getPlantLeafBases(const std::vector<uint> &plantIDs) const {
3844 std::vector<helios::vec3> leaf_bases;
3845 for (const uint plantID: plantIDs) {
3846 auto bases = getPlantLeafBases(plantID);
3847 leaf_bases.insert(leaf_bases.end(), bases.begin(), bases.end());
3848 }
3849 return leaf_bases;
3850}
3851
3853 if (plant_instances.find(plantID) == plant_instances.end()) {
3854 helios_runtime_error("ERROR (PlantArchitecture::isPlantDormant): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3855 }
3856
3857 for (const auto &shoot: plant_instances.at(plantID).shoot_tree) {
3858 if (!shoot->isdormant) {
3859 return false;
3860 }
3861 }
3862
3863 return true;
3864}
3865
3867 if (plant_instances.find(plantID) == plant_instances.end()) {
3868 helios_runtime_error("ERROR (PlantArchitecture::determinePhenologyStage): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3869 }
3870
3871 // Check if plant is dormant
3872 if (isPlantDormant(plantID)) {
3873 return "dormant";
3874 }
3875
3876 // Check if plant has flowers or fruits (reproductive stage)
3877 std::vector<uint> flowers = getPlantFlowerObjectIDs(plantID);
3878 std::vector<uint> fruits = getPlantFruitObjectIDs(plantID);
3879 if (!flowers.empty() || !fruits.empty()) {
3880 return "reproductive";
3881 }
3882
3883 // Check if plant is approaching senescence
3884 const auto &plant_instance = plant_instances.at(plantID);
3885 if (plant_instance.dd_to_dormancy > 0) {
3886 float senescence_threshold = plant_instance.dd_to_dormancy_break + plant_instance.dd_to_dormancy * 0.9f;
3887 if (plant_instance.time_since_dormancy > senescence_threshold) {
3888 return "senescent";
3889 }
3890 }
3891
3892 // Default to vegetative stage
3893 return "vegetative";
3894}
3895
3896void PlantArchitecture::writePlantMeshVertices(uint plantID, const std::string &filename) const {
3897 if (plant_instances.find(plantID) == plant_instances.end()) {
3898 helios_runtime_error("ERROR (PlantArchitecture::writePlantMeshVertices): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3899 }
3900
3901 std::vector<uint> plant_UUIDs = getAllPlantUUIDs(plantID);
3902
3903 std::ofstream file;
3904 file.open(filename);
3905
3906 if (!file.is_open()) {
3907 helios_runtime_error("ERROR (PlantArchitecture::writePlantMeshVertices): Could not open file " + filename + " for writing.");
3908 }
3909
3910 for (uint UUID: plant_UUIDs) {
3911 std::vector<vec3> vertex = context_ptr->getPrimitiveVertices(UUID);
3912 for (vec3 &v: vertex) {
3913 file << v.x << " " << v.y << " " << v.z << std::endl;
3914 }
3915 }
3916
3917 file.close();
3918}
3919
3920void PlantArchitecture::setPlantAge(uint plantID, float a_current_age) {
3921 //\todo
3922 // this->current_age = current_age;
3923}
3924
3925std::string PlantArchitecture::getPlantName(uint plantID) const {
3926 if (plant_instances.find(plantID) == plant_instances.end()) {
3927 helios_runtime_error("ERROR (PlantArchitecture::getPlantName): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3928 }
3929 return plant_instances.at(plantID).plant_name;
3930}
3931
3933 if (plant_instances.find(plantID) == plant_instances.end()) {
3934 helios_runtime_error("ERROR (PlantArchitecture::setPlantAge): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3935 } else if (plant_instances.at(plantID).shoot_tree.empty()) {
3936 helios_runtime_error("ERROR (PlantArchitecture::setPlantAge): Plant with ID of " + std::to_string(plantID) + " has no shoots, so could not get a base position.");
3937 }
3938 return plant_instances.at(plantID).current_age;
3939}
3940
3941std::vector<std::string> PlantArchitecture::listShootTypeLabels(uint plantID) const {
3942 // Validate plant instance exists
3943 if (plant_instances.find(plantID) == plant_instances.end()) {
3944 helios_runtime_error("ERROR (PlantArchitecture::listShootTypeLabels): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3945 }
3946
3947 // Get reference to shoot types snapshot
3948 const auto &shoot_types_snap = plant_instances.at(plantID).shoot_types_snapshot;
3949
3950 // Extract shoot type labels
3951 std::vector<std::string> labels;
3952 labels.reserve(shoot_types_snap.size());
3953 for (const auto &pair: shoot_types_snap) {
3954 labels.push_back(pair.first);
3955 }
3956
3957 return labels;
3958}
3959
3961 if (plant_instances.find(plantID) == plant_instances.end()) {
3962 helios_runtime_error("ERROR (PlantArchitecture::harvestPlant): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3963 }
3964
3965 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
3966 for (auto &phytomer: shoot->phytomers) {
3967 for (auto &petiole: phytomer->floral_buds) {
3968 for (auto &fbud: petiole) {
3969 if (fbud.state != BUD_DORMANT) {
3970 phytomer->setFloralBudState(BUD_DEAD, fbud);
3971 }
3972 }
3973 }
3974 }
3975 }
3976}
3977
3979 if (plant_instances.find(plantID) == plant_instances.end()) {
3980 helios_runtime_error("ERROR (PlantArchitecture::removePlantLeaves): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3981 }
3982
3983 if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
3984 helios_runtime_error("ERROR (PlantArchitecture::removeShootLeaves): Shoot with ID of " + std::to_string(shootID) + " does not exist.");
3985 }
3986
3987 auto &shoot = plant_instances.at(plantID).shoot_tree.at(shootID);
3988
3989 for (auto &phytomer: shoot->phytomers) {
3990 phytomer->removeLeaf();
3991 }
3992}
3993
3995 if (plant_instances.find(plantID) == plant_instances.end()) {
3996 helios_runtime_error("ERROR (PlantArchitecture::removeShootVegetativeBuds): Plant with ID of " + std::to_string(plantID) + " does not exist.");
3997 }
3998
3999 if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
4000 helios_runtime_error("ERROR (PlantArchitecture::removeShootVegetativeBuds): Shoot with ID of " + std::to_string(shootID) + " does not exist.");
4001 }
4002
4003 auto &shoot = plant_instances.at(plantID).shoot_tree.at(shootID);
4004
4005 for (auto &phytomer: shoot->phytomers) {
4006 phytomer->setVegetativeBudState(BUD_DEAD);
4007 }
4008}
4009
4011 if (plant_instances.find(plantID) == plant_instances.end()) {
4012 helios_runtime_error("ERROR (PlantArchitecture::removeShootFloralBuds): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4013 }
4014
4015 if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
4016 helios_runtime_error("ERROR (PlantArchitecture::removeShootFloralBuds): Shoot with ID of " + std::to_string(shootID) + " does not exist.");
4017 }
4018
4019 auto &shoot = plant_instances.at(plantID).shoot_tree.at(shootID);
4020
4021 for (auto &phytomer: shoot->phytomers) {
4022 phytomer->setFloralBudState(BUD_DEAD);
4023 }
4024}
4025
4027 if (plant_instances.find(plantID) == plant_instances.end()) {
4028 helios_runtime_error("ERROR (PlantArchitecture::removePlantLeaves): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4029 }
4030
4031 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
4032 for (auto &phytomer: shoot->phytomers) {
4033 phytomer->removeLeaf();
4034 }
4035 }
4036}
4037
4039 if (plant_instances.find(plantID) == plant_instances.end()) {
4040 helios_runtime_error("ERROR (PlantArchitecture::makePlantDormant): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4041 }
4042
4043 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
4044 shoot->makeDormant();
4045 }
4046 plant_instances.at(plantID).time_since_dormancy = 0;
4047}
4048
4050 if (plant_instances.find(plantID) == plant_instances.end()) {
4051 helios_runtime_error("ERROR (PlantArchitecture::breakPlantDormancy): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4052 }
4053
4054 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
4055 shoot->breakDormancy();
4056 }
4057}
4058
4059void PlantArchitecture::pruneBranch(uint plantID, uint shootID, uint node_index) {
4060 if (plant_instances.find(plantID) == plant_instances.end()) {
4061 helios_runtime_error("ERROR (PlantArchitecture::pruneBranch): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4062 } else if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
4063 helios_runtime_error("ERROR (PlantArchitecture::pruneBranch): Shoot with ID of " + std::to_string(shootID) + " does not exist on plant " + std::to_string(plantID) + ".");
4064 } else if (node_index >= plant_instances.at(plantID).shoot_tree.at(shootID)->current_node_number) {
4065 helios_runtime_error("ERROR (PlantArchitecture::pruneBranch): Node index " + std::to_string(node_index) + " is out of range for shoot " + std::to_string(shootID) + ".");
4066 }
4067
4068 auto &shoot = plant_instances.at(plantID).shoot_tree.at(shootID);
4069
4070 shoot->phytomers.at(node_index)->deletePhytomer();
4071
4072 if (plant_instances.at(plantID).shoot_tree.empty()) {
4073 std::cout << "WARNING (PlantArchitecture::pruneBranch): Plant " << plantID << " base shoot was pruned." << std::endl;
4074 }
4075}
4076
4077// fallback axis if v×u is (near) zero:
4078static vec3 orthonormal_axis(const vec3 &v) {
4079 // try X axis
4080 vec3 ax = cross(v, vec3(1.f, 0.f, 0.f));
4081 if (ax.magnitude() < 1e-6f)
4082 ax = cross(v, vec3(0.f, 1.f, 0.f));
4083 return ax.normalize();
4084}
4085
4086// Rodrigues formula: rotate v about unit‐axis k by angle α
4087static vec3 rodrigues(const vec3 &v, const vec3 &k, float a) {
4088 float c = std::cos(a);
4089 float s = std::sin(a);
4090 // dot = k·v
4091 float kv = k * v;
4092 return v * c + cross(k, v) * s + k * (kv * (1.f - c));
4093}
4094
4095void PlantArchitecture::setPlantLeafAngleDistribution_private(const std::vector<uint> &plantIDs, float Beta_mu_inclination, float Beta_nu_inclination, float eccentricity_azimuth, float ellipse_rotation_azimuth_degrees, bool set_elevation,
4096 bool set_azimuth) const {
4097 for (uint plantID: plantIDs) {
4098 if (plant_instances.find(plantID) == plant_instances.end()) {
4099 helios_runtime_error("ERROR (PlantArchitecture::setPlantLeafAngleDistribution): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4100 }
4101 }
4102
4103 // ── 2) Gather leaves ────────────────────────────────────────────────────
4104 std::vector<uint> objIDs = getPlantLeafObjectIDs(plantIDs);
4105 std::vector<vec3> bases = getPlantLeafBases(plantIDs);
4106 size_t N = objIDs.size();
4107 assert(bases.size() == N);
4108 if (N == 0 || (!set_elevation && !set_azimuth))
4109 return;
4110
4111 // ── 3) Sample current & target (θ,φ) ───────────────────────────────────
4112 std::vector<float> theta(N), phi(N), theta_t(N), phi_t(N);
4113 for (size_t i = 0; i < N; ++i) {
4114 // current normal → (θ,φ)
4115 vec3 n0 = context_ptr->getObjectAverageNormal(objIDs[i]);
4116 if (!std::isfinite(n0.x) || !std::isfinite(n0.y) || !std::isfinite(n0.z) || n0.magnitude() < 1e-6f) {
4117 n0 = vec3(0.f, 0.f, 1.f);
4118 } else {
4119 n0 = n0.normalize();
4120 }
4121 n0.z = fabs(n0.z);
4122 SphericalCoord sc = cart2sphere(n0);
4123 theta[i] = sc.zenith;
4124 phi[i] = sc.azimuth;
4125
4126 // target angles
4127 if (set_elevation && !set_azimuth) {
4128 theta_t[i] = sample_Beta_distribution(Beta_mu_inclination, Beta_nu_inclination, context_ptr->getRandomGenerator());
4129 phi_t[i] = phi[i];
4130 } else if (!set_elevation && set_azimuth) {
4131 theta_t[i] = theta[i];
4132 phi_t[i] = sample_ellipsoidal_azimuth(eccentricity_azimuth, ellipse_rotation_azimuth_degrees, context_ptr->getRandomGenerator());
4133 } else {
4134 // both elevation & azimuth
4135 theta_t[i] = sample_Beta_distribution(Beta_mu_inclination, Beta_nu_inclination, context_ptr->getRandomGenerator());
4136 phi_t[i] = sample_ellipsoidal_azimuth(eccentricity_azimuth, ellipse_rotation_azimuth_degrees, context_ptr->getRandomGenerator());
4137 }
4138 }
4139
4140 // ── 4) Pure-1D shortcuts ─────────────────────────────────────────────────
4141 if (set_elevation && !set_azimuth) {
4142 // only θ changes
4143 for (size_t i = 0; i < N; ++i) {
4144 float elev = PI_F * 0.5f - theta_t[i];
4145 vec3 new_n = sphere2cart(SphericalCoord(1.f, elev, phi[i]));
4146 context_ptr->setObjectAverageNormal(objIDs[i], bases[i], new_n);
4147 }
4148 return;
4149 }
4150 if (!set_elevation && set_azimuth) {
4151 // only φ changes
4152 for (size_t i = 0; i < N; ++i) {
4153 float elev = PI_F * 0.5f - theta[i];
4154 vec3 new_n = sphere2cart(SphericalCoord(1.f, elev, phi_t[i]));
4155 context_ptr->setObjectAverageNormal(objIDs[i], bases[i], new_n);
4156 }
4157 return;
4158 }
4159
4160 // ── 5) Full 2-D case: build V0/V1 ───────────────────────────────────────
4161 std::vector<vec3> V0(N), V1(N);
4162 for (size_t i = 0; i < N; ++i) {
4163 float e0 = PI_F * 0.5f - theta[i];
4164 float e1 = PI_F * 0.5f - theta_t[i];
4165 V0[i] = sphere2cart(SphericalCoord(1.f, e0, phi[i]));
4166 V1[i] = sphere2cart(SphericalCoord(1.f, e1, phi_t[i]));
4167 }
4168
4169 // ── 6) Solve assignment ─────────────────────────────────────────────────
4170 std::vector<int> assignment(N);
4171 {
4172 HungarianAlgorithm hung;
4173 std::vector<std::vector<double>> C(N, std::vector<double>(N));
4174 for (size_t i = 0; i < N; ++i) {
4175 for (size_t j = 0; j < N; ++j) {
4176 double d = (V0[i] - V1[j]).magnitude();
4177 C[i][j] = std::isfinite(d) ? d : ((std::numeric_limits<double>::max)() * 0.5);
4178 }
4179 }
4180 hung.Solve(C, assignment);
4181 }
4182
4183 // ── 7) Rotate & write back ───────────────────────────────────────────────
4184 for (size_t i = 0; i < N; ++i) {
4185 int j = assignment[i];
4186 // pick your target; if out-of-bounds, just keep the original V0[i]
4187 vec3 v = V0[i];
4188 vec3 u = (j >= 0 && j < (int) N ? V1[j] : V0[i]);
4189
4190 // normalize
4191 v = (v.magnitude() < 1e-6f ? vec3(0, 0, 1) : v.normalize());
4192 u = (u.magnitude() < 1e-6f ? vec3(0, 0, 1) : u.normalize());
4193
4194 // minimal‐angle between them
4195 float dot = std::clamp(v * u, -1.f, 1.f);
4196 float ang = acos_safe(dot);
4197
4198 // choose axis
4199 vec3 axis = cross(v, u);
4200 if (!set_elevation && set_azimuth) {
4201 // if it's really just φ, rotate about Z
4202 axis = vec3(0.f, 0.f, 1.f);
4203 } else if (axis.magnitude() < 1e-6f) {
4204 // degenerate → pick any perpendicular
4205 axis = orthonormal_axis(v);
4206 } else {
4207 axis = axis.normalize();
4208 }
4209
4210 // apply Rodrigues + final guard
4211 vec3 r = rodrigues(v, axis, ang);
4212 if (!std::isfinite(r.x) || !std::isfinite(r.y) || !std::isfinite(r.z) || r.magnitude() < 1e-6f) {
4213 r = u;
4214 } else {
4215 r = r.normalize();
4216 }
4217
4218 // convert back & set
4219 SphericalCoord out = cart2sphere(r);
4220 float new_elev = PI_F * 0.5f - out.zenith;
4221 vec3 new_n = sphere2cart(SphericalCoord(1.f, new_elev, out.azimuth));
4222 context_ptr->setObjectAverageNormal(objIDs[i], bases[i], new_n);
4223 }
4224}
4225
4226// std::vector<uint> objIDs_leaf = getPlantLeafObjectIDs(plantIDs);
4227// std::vector<vec3> leaf_bases = getPlantLeafBases(plantIDs);
4228//
4229//
4230// assert( objIDs_leaf.size() == leaf_bases.size() );
4231//
4232//
4233// const size_t Nleaves = objIDs_leaf.size();
4234//
4235//
4236// std::vector<float> thetaL(Nleaves);
4237// std::vector<float> phiL(Nleaves);
4238// std::vector<float> thetaL_target(Nleaves);
4239// std::vector<float> phiL_target(Nleaves);
4240// for ( int i=0; i<Nleaves; i++ ) {
4241// vec3 norm = context_ptr->getObjectAverageNormal(objIDs_leaf.at(i));
4242// norm.z = fabs(norm.z);
4243// SphericalCoord leaf_angle = cart2sphere(norm);
4244// thetaL.at(i) = leaf_angle.zenith;
4245// phiL.at(i) = leaf_angle.azimuth;
4246// if ( set_elevation && !set_azimuth ) { //only set elevation
4247// thetaL_target.at(i) = sample_Beta_distribution(Beta_mu_inclination, Beta_nu_inclination, context_ptr->getRandomGenerator());
4248// phiL_target.at(i) = phiL.at(i);
4249// }else if ( !set_elevation && set_azimuth ) {
4250// thetaL_target.at(i) = thetaL.at(i);
4251// phiL_target.at(i) = sample_ellipsoidal_azimuth( eccentricity_azimuth, ellipse_rotation_azimuth_degrees, context_ptr->getRandomGenerator() );
4252// }else if ( set_elevation && set_azimuth ) {
4253// thetaL_target.at(i) = sample_Beta_distribution(Beta_mu_inclination, Beta_nu_inclination, context_ptr->getRandomGenerator());
4254// phiL_target.at(i) = sample_ellipsoidal_azimuth( eccentricity_azimuth, ellipse_rotation_azimuth_degrees, context_ptr->getRandomGenerator() );
4255// }else {
4256// return;
4257// }
4258// }
4259//
4260//
4261// // ── Convert both sets to Cartesian using sphere2cart() ─────────────────
4262// std::vector<vec3> V0, V1;
4263// V0.reserve(Nleaves); V1.reserve(Nleaves);
4264// for (size_t i = 0; i < Nleaves; ++i) {
4265// // Helios uses (radius, elevation, azimuth), where elevation = π/2 – zenith
4266// float elev0 = PI_F*0.5f - thetaL[i];
4267// SphericalCoord sc0(1.f, elev0, phiL[i]);
4268// V0.push_back(sphere2cart(sc0));
4269//
4270//
4271// float elev1 = PI_F*0.5f - thetaL_target[i];
4272// SphericalCoord sc1(1.f, elev1, phiL_target[i]);
4273// V1.push_back(sphere2cart(sc1));
4274// }
4275//
4276//
4277// // ── Build cost matrix of great‐circle angles ───────────────────────────
4278// std::vector<std::vector<double>> cost(Nleaves, std::vector<double>(Nleaves));
4279// for (size_t i = 0; i < Nleaves; ++i) {
4280// for (size_t j = 0; j < Nleaves; ++j) {
4281// float d = std::clamp(V0[i] * V1[j], -1.f, 1.f); // dot product via operator*
4282// cost[i][j] = std::acos(static_cast<double>(d));
4283// }
4284// }
4285//
4286//
4287// // ── Global minimal‐sum assignment ──────────────────────────────────────
4288// HungarianAlgorithm hungarian;
4289// std::vector<int> assignment;
4290// double totalCost = hungarian.Solve(cost, assignment);
4291//
4292//
4293// // ── Rotate each V0[i] → V1[assignment[i]] by minimal axis–angle ────────
4294// std::vector<vec3> V0_matched(Nleaves);
4295// for (size_t i = 0; i < Nleaves; ++i) {
4296// vec3 v = V0[i];
4297// vec3 u = V1[assignment[i]];
4298//
4299//
4300// float dot = std::clamp(v * u, -1.f, 1.f);
4301// float a = std::acos(dot);
4302//
4303//
4304// vec3 axis = cross(v, u);
4305// if (axis.magnitude() < 1e-6f)
4306// axis = orthonormal_axis(v);
4307// else
4308// axis = axis.normalize();
4309//
4310//
4311// V0_matched[i] = rodrigues(v, axis, a);
4312// }
4313//
4314//
4315// // ── Convert rotated vectors back to (θ,φ) via cart2sphere() ────
4316// std::vector<float> theta_matched(Nleaves), phi_matched(Nleaves);
4317// for (size_t i = 0; i < Nleaves; ++i) {
4318// SphericalCoord out = cart2sphere(V0_matched[i]);
4319// theta_matched[i] = out.zenith; // your convention: zenith in [0,π]
4320// phi_matched [i] = out.azimuth; // in [0,2π)
4321//
4322//
4323// vec3 new_normal = sphere2cart(SphericalCoord(1.f, PI_F*0.5f - theta_matched[i], phi_matched[i]));
4324// context_ptr->setObjectAverageNormal(objIDs_leaf.at(i), leaf_bases.at(i), new_normal);
4325// }
4326//
4327//
4328// }
4329
4330
4332 if (plant_instances.find(plantID) == plant_instances.end()) {
4333 helios_runtime_error("ERROR (PlantArchitecture::getShootNodeCount): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4334 } else if (plant_instances.at(plantID).shoot_tree.size() <= shootID) {
4335 helios_runtime_error("ERROR (PlantArchitecture::getShootNodeCount): Shoot ID is out of range.");
4336 }
4337 return plant_instances.at(plantID).shoot_tree.at(shootID)->current_node_number;
4338}
4339
4340float PlantArchitecture::getShootTaper(uint plantID, uint shootID) const {
4341 if (plant_instances.find(plantID) == plant_instances.end()) {
4342 helios_runtime_error("ERROR (PlantArchitecture::getShootTaper): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4343 } else if (plant_instances.at(plantID).shoot_tree.size() <= shootID) {
4344 helios_runtime_error("ERROR (PlantArchitecture::getShootTaper): Shoot ID is out of range.");
4345 }
4346
4347 float r0 = plant_instances.at(plantID).shoot_tree.at(shootID)->shoot_internode_radii.front().front();
4348 float r1 = plant_instances.at(plantID).shoot_tree.at(shootID)->shoot_internode_radii.back().back();
4349
4350 float taper = (r0 - r1) / r0;
4351 if (taper < 0) {
4352 taper = 0;
4353 } else if (taper > 1) {
4354 taper = 1;
4355 }
4356
4357 return taper;
4358}
4359
4360std::vector<uint> PlantArchitecture::getAllPlantIDs() const {
4361 std::vector<uint> objIDs;
4362 objIDs.reserve(plant_instances.size());
4363
4364 for (const auto &plant: plant_instances) {
4365 objIDs.push_back(plant.first);
4366 }
4367
4368 return objIDs;
4369}
4370
4371std::vector<uint> PlantArchitecture::getAllPlantObjectIDs(uint plantID) const {
4372 if (plant_instances.find(plantID) == plant_instances.end()) {
4373 helios_runtime_error("ERROR (PlantArchitecture::getAllPlantObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4374 }
4375
4376 std::vector<uint> objIDs;
4377
4378 for (const auto &shoot: plant_instances.at(plantID).shoot_tree) {
4379 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
4380 objIDs.push_back(shoot->internode_tube_objID);
4381 }
4382 for (const auto &phytomer: shoot->phytomers) {
4383 std::vector<uint> petiole_objIDs_flat = flatten(phytomer->petiole_objIDs);
4384 objIDs.insert(objIDs.end(), petiole_objIDs_flat.begin(), petiole_objIDs_flat.end());
4385 std::vector<uint> leaf_objIDs_flat = flatten(phytomer->leaf_objIDs);
4386 objIDs.insert(objIDs.end(), leaf_objIDs_flat.begin(), leaf_objIDs_flat.end());
4387 for (auto &petiole: phytomer->floral_buds) {
4388 for (auto &fbud: petiole) {
4389 std::vector<uint> inflorescence_objIDs_flat = fbud.inflorescence_objIDs;
4390 objIDs.insert(objIDs.end(), inflorescence_objIDs_flat.begin(), inflorescence_objIDs_flat.end());
4391 std::vector<uint> peduncle_objIDs_flat = fbud.peduncle_objIDs;
4392 objIDs.insert(objIDs.end(), peduncle_objIDs_flat.begin(), peduncle_objIDs_flat.end());
4393 }
4394 }
4395 }
4396 }
4397
4398 return objIDs;
4399}
4400
4401std::vector<uint> PlantArchitecture::getAllPlantUUIDs(uint plantID) const {
4402 return context_ptr->getObjectPrimitiveUUIDs(getAllPlantObjectIDs(plantID));
4403}
4404
4405std::vector<uint> PlantArchitecture::getPlantInternodeObjectIDs(uint plantID) const {
4406 if (plant_instances.find(plantID) == plant_instances.end()) {
4407 helios_runtime_error("ERROR (PlantArchitecture::getPlantInternodeObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4408 }
4409
4410 std::vector<uint> objIDs;
4411
4412 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4413
4414 for (auto &shoot: shoot_tree) {
4415 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
4416 objIDs.push_back(shoot->internode_tube_objID);
4417 }
4418 }
4419
4420 return objIDs;
4421}
4422
4423std::vector<uint> PlantArchitecture::getPlantInternodeObjectIDs(uint plantID, const std::string &shoot_type_label) const {
4424 if (plant_instances.find(plantID) == plant_instances.end()) {
4425 helios_runtime_error("ERROR (PlantArchitecture::getPlantInternodeObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4426 }
4427
4428 std::vector<uint> objIDs;
4429
4430 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4431
4432 bool shoot_type_found = false;
4433 for (auto &shoot: shoot_tree) {
4434 if (shoot->shoot_type_label == shoot_type_label) {
4435 shoot_type_found = true;
4436 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
4437 objIDs.push_back(shoot->internode_tube_objID);
4438 }
4439 }
4440 }
4441
4442 if (!shoot_type_found) {
4443 helios_runtime_error("ERROR (PlantArchitecture::getPlantInternodeObjectIDs): No shoots with shoot type label '" + shoot_type_label + "' exist for plant with ID " + std::to_string(plantID) + ".");
4444 }
4445
4446 return objIDs;
4447}
4448
4449std::vector<uint> PlantArchitecture::getPlantPetioleObjectIDs(uint plantID) const {
4450 if (plant_instances.find(plantID) == plant_instances.end()) {
4451 helios_runtime_error("ERROR (PlantArchitecture::getPlantPetioleObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4452 }
4453
4454 std::vector<uint> objIDs;
4455
4456 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4457
4458 for (auto &shoot: shoot_tree) {
4459 for (auto &phytomer: shoot->phytomers) {
4460 for (auto &petiole: phytomer->petiole_objIDs) {
4461 objIDs.insert(objIDs.end(), petiole.begin(), petiole.end());
4462 }
4463 }
4464 }
4465
4466 return objIDs;
4467}
4468
4469std::vector<uint> PlantArchitecture::getPlantLeafObjectIDs(uint plantID) const {
4470 if (plant_instances.find(plantID) == plant_instances.end()) {
4471 helios_runtime_error("ERROR (PlantArchitecture::getPlantLeafObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4472 }
4473
4474 std::vector<uint> objIDs;
4475
4476 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4477
4478 for (auto &shoot: shoot_tree) {
4479 for (auto &phytomer: shoot->phytomers) {
4480 for (auto &leaf_objID: phytomer->leaf_objIDs) {
4481 objIDs.insert(objIDs.end(), leaf_objID.begin(), leaf_objID.end());
4482 }
4483 }
4484 }
4485
4486 return objIDs;
4487}
4488
4489std::vector<uint> PlantArchitecture::getPlantLeafObjectIDs(const std::vector<uint> &plantIDs) const {
4490 std::vector<uint> objIDs;
4491 objIDs.reserve(50 * plantIDs.size()); // assume we have at least 50 leaves/plant
4492 for (const uint plantID: plantIDs) {
4493 std::vector<uint> leaf_objIDs = getPlantLeafObjectIDs(plantID);
4494 objIDs.insert(objIDs.end(), leaf_objIDs.begin(), leaf_objIDs.end());
4495 }
4496 return objIDs;
4497}
4498
4499std::vector<uint> PlantArchitecture::getPlantPeduncleObjectIDs(uint plantID) const {
4500 if (plant_instances.find(plantID) == plant_instances.end()) {
4501 helios_runtime_error("ERROR (PlantArchitecture::getPlantPeduncleObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4502 }
4503
4504 std::vector<uint> objIDs;
4505
4506 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4507
4508 for (auto &shoot: shoot_tree) {
4509 for (auto &phytomer: shoot->phytomers) {
4510 for (auto &petiole: phytomer->floral_buds) {
4511 for (auto &fbud: petiole) {
4512 objIDs.insert(objIDs.end(), fbud.peduncle_objIDs.begin(), fbud.peduncle_objIDs.end());
4513 }
4514 }
4515 }
4516 }
4517
4518 return objIDs;
4519}
4520
4521std::vector<uint> PlantArchitecture::getPlantFlowerObjectIDs(uint plantID) const {
4522 if (plant_instances.find(plantID) == plant_instances.end()) {
4523 helios_runtime_error("ERROR (PlantArchitecture::getPlantInflorescenceObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4524 }
4525
4526 std::vector<uint> objIDs;
4527
4528 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4529
4530 for (auto &shoot: shoot_tree) {
4531 for (auto &phytomer: shoot->phytomers) {
4532 for (int petiole = 0; petiole < phytomer->floral_buds.size(); petiole++) {
4533 for (int bud = 0; bud < phytomer->floral_buds.at(petiole).size(); bud++) {
4534 if (phytomer->floral_buds.at(petiole).at(bud).state == BUD_FLOWER_OPEN || phytomer->floral_buds.at(petiole).at(bud).state == BUD_FLOWER_CLOSED) {
4535 objIDs.insert(objIDs.end(), phytomer->floral_buds.at(petiole).at(bud).inflorescence_objIDs.begin(), phytomer->floral_buds.at(petiole).at(bud).inflorescence_objIDs.end());
4536 }
4537 }
4538 }
4539 }
4540 }
4541
4542 return objIDs;
4543}
4544
4545std::vector<uint> PlantArchitecture::getPlantFruitObjectIDs(uint plantID) const {
4546 if (plant_instances.find(plantID) == plant_instances.end()) {
4547 helios_runtime_error("ERROR (PlantArchitecture::getPlantInflorescenceObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4548 }
4549
4550 std::vector<uint> objIDs;
4551
4552 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4553
4554 for (auto &shoot: shoot_tree) {
4555 for (auto &phytomer: shoot->phytomers) {
4556 for (int petiole = 0; petiole < phytomer->floral_buds.size(); petiole++) {
4557 for (int bud = 0; bud < phytomer->floral_buds.at(petiole).size(); bud++) {
4558 if (phytomer->floral_buds.at(petiole).at(bud).state == BUD_FRUITING) {
4559 objIDs.insert(objIDs.end(), phytomer->floral_buds.at(petiole).at(bud).inflorescence_objIDs.begin(), phytomer->floral_buds.at(petiole).at(bud).inflorescence_objIDs.end());
4560 }
4561 }
4562 }
4563 }
4564 }
4565
4566 return objIDs;
4567}
4568
4569
4571 if (plant_instances.find(plantID) == plant_instances.end()) {
4572 helios_runtime_error("ERROR (PlantArchitecture::getPlantInflorescenceObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4573 }
4574
4575 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4576
4577 for (const auto& shoot : shoot_tree) {
4578
4579 int fruit_count = 0;
4580
4581 for (const auto& phytomer : shoot->phytomers) {
4582 for (int petiole = 0; petiole < phytomer->floral_buds.size(); petiole++) {
4583 for (int bud = 0; bud < phytomer->floral_buds.at(petiole).size(); bud++) {
4584 if (phytomer->floral_buds.at(petiole).at(bud).state == BUD_FRUITING) {
4585 fruit_count++;
4586 }
4587 }
4588 }
4589 }
4590
4591 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
4592 context_ptr->setObjectData(shoot->internode_tube_objID, "fruit_count", fruit_count);
4593 }
4594 }
4595}
4596
4597
4598
4599std::vector<uint> PlantArchitecture::getShootInternodeObjectIDs(uint plantID) const {
4600 if (plant_instances.find(plantID) == plant_instances.end()) {
4601 helios_runtime_error("ERROR (PlantArchitecture::getPlantInflorescenceObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4602 }
4603
4604 std::vector<uint> objIDs;
4605
4606 auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4607
4608 for (auto &shoot: shoot_tree) {
4609 objIDs.push_back(shoot->internode_tube_objID);
4610 }
4611
4612 return objIDs;
4613}
4614
4615
4616
4618 if (plant_instances.find(plantID) == plant_instances.end()) {
4619 helios_runtime_error("ERROR (PlantArchitecture::getPlantCollisionRelevantObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4620 }
4621
4622 std::vector<uint> collision_relevant_objects;
4623
4624 // Collect collision-relevant geometry for this plant based on current settings
4625
4626 // Internodes - always include if enabled
4627 if (collision_include_internodes) {
4628 std::vector<uint> internodes = getPlantInternodeObjectIDs(plantID);
4629 collision_relevant_objects.insert(collision_relevant_objects.end(), internodes.begin(), internodes.end());
4630 }
4631
4632 // Leaves - include if enabled
4633 if (collision_include_leaves) {
4634 std::vector<uint> leaves = getPlantLeafObjectIDs(plantID);
4635 collision_relevant_objects.insert(collision_relevant_objects.end(), leaves.begin(), leaves.end());
4636 }
4637
4638 // Petioles - include if enabled (typically disabled for trees)
4639 if (collision_include_petioles) {
4640 std::vector<uint> petioles = getPlantPetioleObjectIDs(plantID);
4641 collision_relevant_objects.insert(collision_relevant_objects.end(), petioles.begin(), petioles.end());
4642 }
4643
4644 // Flowers - include if enabled (typically disabled)
4645 if (collision_include_flowers) {
4646 std::vector<uint> flowers = getPlantFlowerObjectIDs(plantID);
4647 collision_relevant_objects.insert(collision_relevant_objects.end(), flowers.begin(), flowers.end());
4648 }
4649
4650 // Fruit - include if enabled (typically disabled)
4651 if (collision_include_fruit) {
4652 std::vector<uint> fruit = getPlantFruitObjectIDs(plantID);
4653 collision_relevant_objects.insert(collision_relevant_objects.end(), fruit.begin(), fruit.end());
4654 }
4655
4656 return collision_relevant_objects;
4657}
4658
4659std::vector<uint> PlantArchitecture::getAllUUIDs() const {
4660 std::vector<uint> UUIDs_all;
4661 for (const auto &instance: plant_instances) {
4662 std::vector<uint> UUIDs = getAllPlantUUIDs(instance.first);
4663 UUIDs_all.insert(UUIDs_all.end(), UUIDs.begin(), UUIDs.end());
4664 }
4665 return UUIDs_all;
4666}
4667
4668std::vector<uint> PlantArchitecture::getAllLeafUUIDs() const {
4669 std::vector<uint> UUIDs_all;
4670 for (const auto &instance: plant_instances) {
4671 std::vector<uint> objIDs = getPlantLeafObjectIDs(instance.first);
4672 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objIDs);
4673 UUIDs_all.insert(UUIDs_all.end(), UUIDs.begin(), UUIDs.end());
4674 }
4675 return UUIDs_all;
4676}
4677
4679 std::vector<uint> UUIDs_all;
4680 for (const auto &instance: plant_instances) {
4681 std::vector<uint> objIDs = getPlantInternodeObjectIDs(instance.first);
4682 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objIDs);
4683 UUIDs_all.insert(UUIDs_all.end(), UUIDs.begin(), UUIDs.end());
4684 }
4685 return UUIDs_all;
4686}
4687
4688std::vector<uint> PlantArchitecture::getAllPetioleUUIDs() const {
4689 std::vector<uint> UUIDs_all;
4690 for (const auto &instance: plant_instances) {
4691 std::vector<uint> objIDs = getPlantPetioleObjectIDs(instance.first);
4692 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objIDs);
4693 UUIDs_all.insert(UUIDs_all.end(), UUIDs.begin(), UUIDs.end());
4694 }
4695 return UUIDs_all;
4696}
4697
4699 std::vector<uint> UUIDs_all;
4700 for (const auto &instance: plant_instances) {
4701 std::vector<uint> objIDs = getPlantPeduncleObjectIDs(instance.first);
4702 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objIDs);
4703 UUIDs_all.insert(UUIDs_all.end(), UUIDs.begin(), UUIDs.end());
4704 }
4705 return UUIDs_all;
4706}
4707
4708std::vector<uint> PlantArchitecture::getAllFlowerUUIDs() const {
4709 std::vector<uint> UUIDs_all;
4710 for (const auto &instance: plant_instances) {
4711 std::vector<uint> objIDs = getPlantFlowerObjectIDs(instance.first);
4712 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objIDs);
4713 UUIDs_all.insert(UUIDs_all.end(), UUIDs.begin(), UUIDs.end());
4714 }
4715 return UUIDs_all;
4716}
4717
4718std::vector<uint> PlantArchitecture::getAllFruitUUIDs() const {
4719 std::vector<uint> UUIDs_all;
4720 for (const auto &instance: plant_instances) {
4721 std::vector<uint> objIDs = getPlantFruitObjectIDs(instance.first);
4722 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objIDs);
4723 UUIDs_all.insert(UUIDs_all.end(), UUIDs.begin(), UUIDs.end());
4724 }
4725 return UUIDs_all;
4726}
4727
4728std::vector<uint> PlantArchitecture::getAllObjectIDs() const {
4729 std::vector<uint> objIDs_all;
4730 for (const auto &instance: plant_instances) {
4731 std::vector<uint> objIDs = getAllPlantObjectIDs(instance.first);
4732 objIDs_all.insert(objIDs_all.end(), objIDs.begin(), objIDs.end());
4733 }
4734 return objIDs_all;
4735}
4736
4738 carbon_model_enabled = true;
4739}
4740
4742 carbon_model_enabled = false;
4743}
4744
4745uint PlantArchitecture::addPlantInstance(const helios::vec3 &base_position, float current_age) {
4746 if (current_age < 0) {
4747 helios_runtime_error("ERROR (PlantArchitecture::addPlantInstance): Current age must be greater than or equal to zero.");
4748 }
4749
4750 PlantInstance instance(base_position, current_age, "custom", context_ptr);
4751
4752 plant_instances.emplace(plant_count, instance);
4753
4754 // Capture current shoot parameters to prevent contamination between plant types
4755 plant_instances.at(plant_count).shoot_types_snapshot = shoot_types;
4756
4757 plant_count++;
4758
4759 return plant_count - 1;
4760}
4761
4762uint PlantArchitecture::duplicatePlantInstance(uint plantID, const helios::vec3 &base_position, const AxisRotation &base_rotation, float current_age) {
4763 if (plant_instances.find(plantID) == plant_instances.end()) {
4764 helios_runtime_error("ERROR (PlantArchitecture::duplicatePlantInstance): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4765 }
4766
4767 auto plant_shoot_tree = &plant_instances.at(plantID).shoot_tree;
4768
4769 uint plantID_new = addPlantInstance(base_position, current_age);
4770
4771 // Copy the shoot parameters snapshot from the original plant to prevent parameter contamination
4772 plant_instances.at(plantID_new).shoot_types_snapshot = plant_instances.at(plantID).shoot_types_snapshot;
4773
4774 if (plant_shoot_tree->empty()) {
4775 // no shoots to add
4776 return plantID_new;
4777 }
4778 if (plant_shoot_tree->front()->phytomers.empty()) {
4779 // no phytomers to add
4780 return plantID_new;
4781 }
4782
4783 for (const auto &shoot: *plant_shoot_tree) {
4784 uint shootID_new = 0; // ID of the new shoot; will be set once the shoot is created on the first loop iteration
4785 for (int node = 0; node < shoot->current_node_number; node++) {
4786 auto phytomer = shoot->phytomers.at(node);
4787 float internode_radius = phytomer->internode_radius_initial;
4788 float internode_length_max = phytomer->internode_length_max;
4789 float internode_scale_factor_fraction = phytomer->current_internode_scale_factor;
4790 float leaf_scale_factor_fraction = 1.f; // phytomer->current_leaf_scale_factor;
4791
4792 if (node == 0) {
4793 // first phytomer on shoot
4794 AxisRotation original_base_rotation = shoot->base_rotation;
4795 if (shoot->parent_shoot_ID == -1) {
4796 // first shoot on plant
4797 shootID_new = addBaseStemShoot(plantID_new, 1, original_base_rotation + base_rotation, internode_radius, internode_length_max, internode_scale_factor_fraction, leaf_scale_factor_fraction, 0, shoot->shoot_type_label);
4798 } else {
4799 // child shoot
4800 uint parent_node = plant_shoot_tree->at(shoot->parent_shoot_ID)->parent_node_index;
4801 uint parent_petiole_index = 0;
4802 for (auto &petiole: phytomer->axillary_vegetative_buds) {
4803 shootID_new = addChildShoot(plantID_new, shoot->parent_shoot_ID, parent_node, 1, original_base_rotation, internode_radius, internode_length_max, internode_scale_factor_fraction, leaf_scale_factor_fraction, 0,
4804 shoot->shoot_type_label, parent_petiole_index);
4805 parent_petiole_index++;
4806 }
4807 }
4808 } else {
4809 // each phytomer needs to be added one-by-one to account for possible internodes/leaves that are not fully elongated
4810 appendPhytomerToShoot(plantID_new, shootID_new, plant_instances.at(plantID).shoot_types_snapshot.at(shoot->shoot_type_label).phytomer_parameters, internode_radius, internode_length_max, internode_scale_factor_fraction,
4811 leaf_scale_factor_fraction);
4812 }
4813 auto phytomer_new = plant_instances.at(plantID_new).shoot_tree.at(shootID_new)->phytomers.back();
4814 for (uint petiole_index = 0; petiole_index < phytomer->petiole_objIDs.size(); petiole_index++) {
4815 phytomer_new->setLeafScaleFraction(petiole_index, phytomer->current_leaf_scale_factor.at(petiole_index));
4816 }
4817 }
4818 }
4819
4820 return plantID_new;
4821}
4822
4824 if (plant_instances.find(plantID) == plant_instances.end()) {
4825 return;
4826 }
4827
4828 context_ptr->deleteObject(getAllPlantObjectIDs(plantID));
4829
4830 plant_instances.erase(plantID);
4831}
4832
4833void PlantArchitecture::deletePlantInstance(const std::vector<uint> &plantIDs) {
4834 for (uint ID: plantIDs) {
4836 }
4837}
4838
4839void PlantArchitecture::setPlantPhenologicalThresholds(uint plantID, float time_to_dormancy_break, float time_to_flower_initiation, float time_to_flower_opening, float time_to_fruit_set, float time_to_fruit_maturity, float time_to_dormancy,
4840 float max_leaf_lifespan, bool is_evergreen) {
4841 if (plant_instances.find(plantID) == plant_instances.end()) {
4842 helios_runtime_error("ERROR (PlantArchitecture::setPlantPhenologicalThresholds): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4843 }
4844
4845 plant_instances.at(plantID).dd_to_dormancy_break = time_to_dormancy_break;
4846 plant_instances.at(plantID).dd_to_flower_initiation = time_to_flower_initiation;
4847 plant_instances.at(plantID).dd_to_flower_opening = time_to_flower_opening;
4848 plant_instances.at(plantID).dd_to_fruit_set = time_to_fruit_set;
4849 plant_instances.at(plantID).dd_to_fruit_maturity = time_to_fruit_maturity;
4850 plant_instances.at(plantID).dd_to_dormancy = time_to_dormancy;
4851 if (max_leaf_lifespan == 0) {
4852 plant_instances.at(plantID).max_leaf_lifespan = 1e6;
4853 } else {
4854 plant_instances.at(plantID).max_leaf_lifespan = max_leaf_lifespan;
4855 }
4856 plant_instances.at(plantID).is_evergreen = is_evergreen;
4857}
4858
4860 if (plant_instances.find(plantID) == plant_instances.end()) {
4861 helios_runtime_error("ERROR (PlantArchitecture::setPlantCarbohydrateModelParameters): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4862 }
4863
4864 plant_instances.at(plantID).carb_parameters = carb_parameters;
4865}
4866
4867void PlantArchitecture::setPlantCarbohydrateModelParameters(const std::vector<uint> &plantIDs, const CarbohydrateParameters &carb_parameters) {
4868 for (uint plantID: plantIDs) {
4869 setPlantCarbohydrateModelParameters(plantID, carb_parameters);
4870 }
4871}
4872
4874 plant_instances.at(plantID).dd_to_dormancy_break = 0;
4875 plant_instances.at(plantID).dd_to_flower_initiation = -1;
4876 plant_instances.at(plantID).dd_to_flower_opening = -1;
4877 plant_instances.at(plantID).dd_to_fruit_set = -1;
4878 plant_instances.at(plantID).dd_to_fruit_maturity = -1;
4879 plant_instances.at(plantID).dd_to_dormancy = 1e6;
4880}
4881
4882void PlantArchitecture::advanceTime(float time_step_days) {
4883 advanceTime(this->getAllPlantIDs(), time_step_days);
4884}
4885
4886void PlantArchitecture::advanceTime(int time_step_years, float time_step_days) {
4887 advanceTime(this->getAllPlantIDs(), float(time_step_years) * 365.f + time_step_days);
4888}
4889
4890void PlantArchitecture::advanceTime(uint plantID, float time_step_days) {
4891 std::vector<uint> plantIDs = {plantID};
4892 advanceTime(plantIDs, time_step_days);
4893}
4894
4895void PlantArchitecture::advanceTime(const std::vector<uint> &plantIDs, float time_step_days) {
4896 for (uint plantID: plantIDs) {
4897 if (plant_instances.find(plantID) == plant_instances.end()) {
4898 helios_runtime_error("ERROR (PlantArchitecture::advanceTime): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4899 }
4900 }
4901
4902 // Clear BVH cache at start of plant growth operation
4903 clearBVHCache();
4904
4905 // Rebuild BVH once at the start if collision detection is enabled
4906 if (collision_detection_enabled && collision_detection_ptr != nullptr) {
4907 rebuildBVHForTimestep();
4908 }
4909
4910 // accounting for case of time_step_days>phyllochron_min
4911 float phyllochron_min = 9999;
4912 for (uint plantID: plantIDs) {
4913 PlantInstance &plant_instance = plant_instances.at(plantID);
4914 auto shoot_tree = &plant_instance.shoot_tree;
4915 if (shoot_tree->empty()) {
4916 continue;
4917 }
4918 float phyllochron_min_shoot = shoot_tree->front()->shoot_parameters.phyllochron_min.val();
4919 if (phyllochron_min_shoot < phyllochron_min) {
4920 phyllochron_min = phyllochron_min_shoot;
4921 }
4922 for (int i = 1; i < shoot_tree->size(); i++) {
4923 if (shoot_tree->at(i)->shoot_parameters.phyllochron_min.val() < phyllochron_min) {
4924 phyllochron_min_shoot = shoot_tree->at(i)->shoot_parameters.phyllochron_min.val();
4925 if (phyllochron_min_shoot < phyllochron_min) {
4926 phyllochron_min = phyllochron_min_shoot;
4927 }
4928 }
4929 }
4930 }
4931 if (phyllochron_min == 9999) {
4932 return;
4933 }
4934
4935 // **** accumulate photosynthate **** //
4936 if (carbon_model_enabled) {
4937 accumulateShootPhotosynthesis();
4938 }
4939
4940 float dt_max_days;
4941 int Nsteps;
4942
4943 if (time_step_days <= phyllochron_min) {
4944 Nsteps = time_step_days;
4945 dt_max_days = 1;
4946 } else {
4947 Nsteps = std::floor(time_step_days / phyllochron_min);
4948 dt_max_days = phyllochron_min;
4949 }
4950
4951 float remainder_time = time_step_days - dt_max_days * float(Nsteps);
4952 if (remainder_time > 0.f) {
4953 Nsteps++;
4954 }
4955
4956 // Initialize progress bar for timesteps
4957 helios::ProgressBar progress_bar(Nsteps, 50, Nsteps > 1 && printmessages, "Advancing time");
4958
4959 for (int timestep = 0; timestep < Nsteps; timestep++) {
4960
4961 // Rebuild BVH periodically - less frequent for per-tree BVH since trees are isolated
4962 bool should_rebuild_bvh = false;
4963 if (collision_detection_enabled && collision_detection_ptr != nullptr) {
4964 // For per-tree BVH, rebuild less frequently (every 25 timesteps) since spatial isolation reduces need
4965 // For unified BVH, keep original frequency (every 10 timesteps) for better accuracy
4966 if (collision_detection_ptr->isTreeBasedBVHEnabled()) {
4967 should_rebuild_bvh = (timestep % 25 == 0);
4968 } else {
4969 should_rebuild_bvh = (timestep % 10 == 0);
4970 }
4971 }
4972
4973 if (should_rebuild_bvh) {
4974 rebuildBVHForTimestep();
4975
4976 // Re-register plants with per-tree BVH to update primitive counts as plants grow
4977 if (collision_detection_ptr->isTreeBasedBVHEnabled()) {
4978 for (uint plantID: plantIDs) {
4979 std::vector<uint> plant_primitives = getPlantCollisionRelevantObjectIDs(plantID);
4980 if (!plant_primitives.empty()) {
4981 collision_detection_ptr->registerTree(plantID, plant_primitives);
4982 }
4983 }
4984 }
4985 }
4986
4987 if (timestep == Nsteps - 1 && remainder_time != 0.f) {
4988 dt_max_days = remainder_time;
4989 }
4990
4991 for (uint plantID: plantIDs) {
4992 PlantInstance &plant_instance = plant_instances.at(plantID);
4993
4994 auto shoot_tree = &plant_instance.shoot_tree;
4995
4996 if (shoot_tree->empty()) {
4997 continue;
4998 }
4999
5000 if (plant_instance.current_age <= plant_instance.max_age && plant_instance.current_age + dt_max_days > plant_instance.max_age) {
5001 } else if (plant_instance.current_age >= plant_instance.max_age) {
5002 // update Context geometry
5003 shoot_tree->front()->updateShootNodes(true);
5004 continue;
5005 }
5006
5007 plant_instance.current_age += dt_max_days;
5008 plant_instance.time_since_dormancy += dt_max_days;
5009
5010 if (plant_instance.time_since_dormancy > plant_instance.dd_to_dormancy_break + plant_instance.dd_to_dormancy) {
5011 plant_instance.time_since_dormancy = 0;
5012 for (const auto &shoot: *shoot_tree) {
5013 shoot->makeDormant();
5014 shoot->phyllochron_counter = 0;
5015 }
5016 harvestPlant(plantID);
5017 continue;
5018 }
5019
5020 size_t shoot_count = shoot_tree->size();
5021 for (int i = 0; i < shoot_count; i++) {
5022 auto shoot = shoot_tree->at(i);
5023
5024 for (auto &phytomer: shoot->phytomers) {
5025 phytomer->age += dt_max_days;
5026
5027 if (phytomer->phytomer_parameters.phytomer_callback_function != nullptr) {
5028 phytomer->phytomer_parameters.phytomer_callback_function(phytomer);
5029 }
5030 }
5031
5032 // ****** PHENOLOGICAL TRANSITIONS ****** //
5033
5034 // breaking dormancy
5035 if (shoot->isdormant && plant_instance.time_since_dormancy >= plant_instance.dd_to_dormancy_break) {
5036 shoot->phyllochron_counter = 0;
5037 shoot->breakDormancy();
5038 }
5039
5040 if (shoot->isdormant) {
5041 // dormant, don't do anything
5042 continue;
5043 }
5044
5045 for (auto &phytomer: shoot->phytomers) {
5046 if (phytomer->age > plant_instance.max_leaf_lifespan) {
5047 // delete old leaves that exceed maximum lifespan
5048 phytomer->removeLeaf();
5049 }
5050
5051 if (phytomer->floral_buds.empty()) {
5052 // no floral buds - skip this phytomer
5053 continue;
5054 }
5055
5056 for (auto &petiole: phytomer->floral_buds) {
5057 for (auto &fbud: petiole) {
5058 if (fbud.state != BUD_DORMANT && fbud.state != BUD_DEAD) {
5059 fbud.time_counter += dt_max_days;
5060 }
5061
5062 // -- Flowering -- //
5063 if (shoot->shoot_parameters.phytomer_parameters.inflorescence.flower_prototype_function != nullptr) {
5064 // user defined a flower prototype function
5065 // -- Flower initiation (closed flowers) -- //
5066 if (fbud.state == BUD_ACTIVE && plant_instance.dd_to_flower_initiation >= 0.f) {
5067 // bud is active and flower initiation is enabled
5068 if ((!shoot->shoot_parameters.flowers_require_dormancy && fbud.time_counter >= plant_instance.dd_to_flower_initiation) ||
5069 (shoot->shoot_parameters.flowers_require_dormancy && fbud.time_counter >= plant_instance.dd_to_flower_initiation)) {
5070 fbud.time_counter = 0;
5071 if (context_ptr->randu() < shoot->shoot_parameters.flower_bud_break_probability.val()) {
5072 phytomer->setFloralBudState(BUD_FLOWER_CLOSED, fbud);
5073 } else {
5074 phytomer->setFloralBudState(BUD_DEAD, fbud);
5075 }
5076 if (shoot->shoot_parameters.determinate_shoot_growth) {
5077 shoot->terminateApicalBud();
5078 shoot->terminateAxillaryVegetativeBuds();
5079 }
5080 }
5081
5082 // -- Flower opening -- //
5083 } else if ((fbud.state == BUD_FLOWER_CLOSED && plant_instance.dd_to_flower_opening >= 0.f) || (fbud.state == BUD_ACTIVE && plant_instance.dd_to_flower_initiation < 0.f && plant_instance.dd_to_flower_opening >= 0.f)) {
5084 if (fbud.time_counter >= plant_instance.dd_to_flower_opening) {
5085 fbud.time_counter = 0;
5086 if (fbud.state == BUD_FLOWER_CLOSED) {
5087 phytomer->setFloralBudState(BUD_FLOWER_OPEN, fbud);
5088 } else {
5089 if (context_ptr->randu() < shoot->shoot_parameters.flower_bud_break_probability.val()) {
5090 phytomer->setFloralBudState(BUD_FLOWER_OPEN, fbud);
5091 } else {
5092 phytomer->setFloralBudState(BUD_DEAD, fbud);
5093 }
5094 }
5095 if (shoot->shoot_parameters.determinate_shoot_growth) {
5096 shoot->terminateApicalBud();
5097 shoot->terminateAxillaryVegetativeBuds();
5098 }
5099 }
5100 }
5101 }
5102
5103 // -- Fruit Set -- //
5104 // If the flower bud is in a 'flowering' state, the fruit set occurs after a certain amount of time
5105 if (shoot->shoot_parameters.phytomer_parameters.inflorescence.fruit_prototype_function != nullptr) {
5106 if ((fbud.state == BUD_FLOWER_OPEN && plant_instance.dd_to_fruit_set >= 0.f) ||
5107 // flower opened and fruit set is enabled
5108 (fbud.state == BUD_ACTIVE && plant_instance.dd_to_flower_initiation < 0.f &&
5109 (plant_instance.dd_to_flower_opening < 0.f || shoot->shoot_parameters.phytomer_parameters.inflorescence.flower_prototype_function == nullptr) && plant_instance.dd_to_fruit_set >= 0.f) ||
5110 // jumped straight to fruit set with no flowering (either flower opening disabled OR no flower prototype defined)
5111 (fbud.state == BUD_FLOWER_CLOSED && plant_instance.dd_to_flower_opening < 0.f && plant_instance.dd_to_fruit_set >= 0.f)) {
5112 // jumped from closed flower to fruit set with no flower opening
5113 if (fbud.time_counter >= plant_instance.dd_to_fruit_set) {
5114 fbud.time_counter = 0;
5115 // When skipping flowering entirely (BUD_ACTIVE -> BUD_FRUITING), apply compound probability
5116 float fruit_set_prob = shoot->shoot_parameters.fruit_set_probability.val();
5117 if (fbud.state == BUD_ACTIVE) {
5118 // Apply compound probability: flower_bud_break_probability * fruit_set_probability
5119 fruit_set_prob *= shoot->shoot_parameters.flower_bud_break_probability.val();
5120 }
5121 if (context_ptr->randu() < fruit_set_prob) {
5122 phytomer->setFloralBudState(BUD_FRUITING, fbud);
5123 } else {
5124 phytomer->setFloralBudState(BUD_DEAD, fbud);
5125 }
5126 if (shoot->shoot_parameters.determinate_shoot_growth) {
5127 shoot->terminateApicalBud();
5128 shoot->terminateAxillaryVegetativeBuds();
5129 }
5130 }
5131 }
5132 }
5133 }
5134 }
5135 }
5136
5137 // ****** GROWTH/SCALING OF CURRENT PHYTOMERS/FRUIT ****** //
5138
5139 int node_index = 0;
5140 for (auto &phytomer: shoot->phytomers) {
5141 // scale internode length
5142 if (phytomer->current_internode_scale_factor < 1) {
5143 float dL_internode = dt_max_days * shoot->elongation_rate_instantaneous * phytomer->internode_length_max;
5144 float length_scale = fmin(1.f, (phytomer->getInternodeLength() + dL_internode) / phytomer->internode_length_max);
5145 phytomer->setInternodeLengthScaleFraction(length_scale, false);
5146 }
5147
5148 // scale internode girth
5149 if (shoot->shoot_parameters.girth_area_factor.val() > 0.f) {
5150 if (carbon_model_enabled) {
5151 incrementPhytomerInternodeGirth_carb(plantID, shoot->ID, node_index, dt_max_days, false);
5152 } else {
5153 incrementPhytomerInternodeGirth(plantID, shoot->ID, node_index, dt_max_days, false);
5154 }
5155 }
5156
5157 node_index++;
5158 }
5159
5160 node_index = 0;
5161 for (auto &phytomer: shoot->phytomers) {
5162 // scale petiole/leaves
5163 if (phytomer->hasLeaf()) {
5164 for (uint petiole_index = 0; petiole_index < phytomer->current_leaf_scale_factor.size(); petiole_index++) {
5165 if (phytomer->current_leaf_scale_factor.at(petiole_index) >= 1) {
5166 continue;
5167 }
5168
5169 // Calculate petiole growth based on target petiole length (similar to internode growth)
5170 // float petiole_target_length = phytomer->phytomer_parameters.petiole.length.val();
5171 // float current_petiole_length = phytomer->petiole_length.at(petiole_index);
5172 // float dL_petiole = dt_max_days * shoot->elongation_rate_instantaneous * petiole_target_length;
5173 // float petiole_scale = fmin(1.f, (current_petiole_length + dL_petiole) / petiole_target_length);
5174
5175 // Also calculate leaf growth for proper leaf scaling
5176 float tip_ind = ceil(float(phytomer->leaf_size_max.at(petiole_index).size() - 1) / 2.f);
5177 float leaf_length = phytomer->current_leaf_scale_factor.at(petiole_index) * phytomer->leaf_size_max.at(petiole_index).at(tip_ind);
5178 float dL_leaf = dt_max_days * shoot->elongation_rate_instantaneous * phytomer->leaf_size_max.at(petiole_index).at(tip_ind);
5179 float leaf_scale = fmin(1.f, (leaf_length + dL_leaf) / phytomer->phytomer_parameters.leaf.prototype_scale.val());
5180
5181 // Use the minimum of petiole and leaf scaling to keep them synchronized
5182 // float scale = fmin(petiole_scale, leaf_scale);
5183 float scale = fmin(1.f, (leaf_length + dL_leaf) / phytomer->phytomer_parameters.leaf.prototype_scale.val());
5184 phytomer->phytomer_parameters.leaf.prototype_scale.resample();
5185 phytomer->setLeafScaleFraction(petiole_index, scale);
5186 }
5187 }
5188
5189 // Fruit Growth
5190 for (auto &petiole: phytomer->floral_buds) {
5191 for (auto &fbud: petiole) {
5192 // If the floral bud it in a 'fruiting' state, the fruit grows with time
5193 if (fbud.state == BUD_FRUITING && fbud.time_counter > 0) {
5194 // Save current scale for nitrogen model growth tracking
5195 fbud.previous_fruit_scale_factor = fbud.current_fruit_scale_factor;
5196 float scale = fmin(1, 0.25f + 0.75f * fbud.time_counter / plant_instance.dd_to_fruit_maturity);
5197 phytomer->setInflorescenceScaleFraction(fbud, scale);
5198 }
5199 }
5200 }
5201
5202 // ****** NEW CHILD SHOOTS FROM VEGETATIVE BUDS ****** //
5203 uint parent_petiole_index = 0;
5204 for (auto &petiole: phytomer->axillary_vegetative_buds) {
5205 for (auto &vbud: petiole) {
5206 if (vbud.state == BUD_ACTIVE && phytomer->age + dt_max_days > shoot->shoot_parameters.vegetative_bud_break_time.val()) {
5207 ShootParameters *new_shoot_parameters = &plant_instance.shoot_types_snapshot.at(vbud.shoot_type_label);
5208 int parent_node_count = shoot->current_node_number;
5209
5210 float insertion_angle_adjustment = fmin(new_shoot_parameters->insertion_angle_tip.val() + new_shoot_parameters->insertion_angle_decay_rate.val() * float(parent_node_count - phytomer->shoot_index.x - 1), 90.f);
5211 // NOTE: No additional rotation offset needed here because the child shoot orientation
5212 // is already determined by the parent_petiole_axis in appendPhytomer() (line 995),
5213 // which correctly uses parent_petiole_index to get the specific petiole's axis vector
5214 AxisRotation base_rotation = make_AxisRotation(deg2rad(insertion_angle_adjustment), deg2rad(new_shoot_parameters->base_yaw.val()), deg2rad(new_shoot_parameters->base_roll.val()));
5215 new_shoot_parameters->base_yaw.resample();
5216 if (new_shoot_parameters->insertion_angle_decay_rate.val() == 0) {
5217 new_shoot_parameters->insertion_angle_tip.resample();
5218 }
5219
5220 // scale the shoot internode length based on proximity from the tip
5221 float internode_length_max;
5222 if (new_shoot_parameters->growth_requires_dormancy) {
5223 internode_length_max = fmax(new_shoot_parameters->internode_length_max.val() - new_shoot_parameters->internode_length_decay_rate.val() * float(parent_node_count - phytomer->shoot_index.x - 1),
5224 new_shoot_parameters->internode_length_min.val());
5225 } else {
5226 internode_length_max = new_shoot_parameters->internode_length_max.val();
5227 }
5228
5229 float internode_radius = phytomer->internode_radius_initial;
5230
5231 uint childID = addChildShoot(plantID, shoot->ID, node_index, 1, base_rotation, internode_radius, internode_length_max, 0.01, 0.01, 0, vbud.shoot_type_label, parent_petiole_index);
5232
5233 phytomer->setVegetativeBudState(BUD_DEAD, vbud);
5234 vbud.shoot_ID = childID;
5235 shoot_tree->at(childID)->isdormant = false;
5236 }
5237 }
5238 parent_petiole_index++;
5239 }
5240
5241 node_index++;
5242 }
5243
5244 // if shoot has reached max_nodes, stop apical growth
5245 if (shoot->current_node_number >= shoot->shoot_parameters.max_nodes.val()) {
5246 shoot->terminateApicalBud();
5247 }
5248
5249 // If the apical bud is dead, don't do anything more with the shoot
5250 if (!shoot->meristem_is_alive) {
5251 continue;
5252 }
5253
5254 // ****** PHYLLOCHRON - NEW PHYTOMERS ****** //
5255 shoot->phyllochron_counter += dt_max_days;
5256 if (shoot->phyllochron_counter >= shoot->phyllochron_instantaneous && !shoot->phytomers.back()->isdormant) {
5257 float internode_radius = shoot->shoot_parameters.phytomer_parameters.internode.radius_initial.val();
5258 shoot->shoot_parameters.phytomer_parameters.internode.radius_initial.resample();
5259 float internode_length_max = shoot->internode_length_max_shoot_initial;
5260 appendPhytomerToShoot(plantID, shoot->ID, plant_instance.shoot_types_snapshot.at(shoot->shoot_type_label).phytomer_parameters, internode_radius, internode_length_max, 0.01,
5261 0.01); //\todo These factors should be set to be consistent with the shoot
5262 shoot->phyllochron_counter = shoot->phyllochron_counter - shoot->phyllochron_instantaneous;
5263 }
5264
5265 // ****** EPICORMIC SHOOTS ****** //
5266 std::string epicormic_shoot_label = plant_instance.epicormic_shoot_probability_perlength_per_day.first;
5267 if (!epicormic_shoot_label.empty()) {
5268 std::vector<float> epicormic_fraction;
5269 uint Nepicormic = shoot->sampleEpicormicShoot(time_step_days, epicormic_fraction);
5270 for (int s = 0; s < Nepicormic; s++) {
5271 float internode_radius = plant_instance.shoot_types_snapshot.at(epicormic_shoot_label).phytomer_parameters.internode.radius_initial.val();
5272 plant_instance.shoot_types_snapshot.at(epicormic_shoot_label).phytomer_parameters.internode.radius_initial.resample();
5273 float internode_length_max = plant_instance.shoot_types_snapshot.at(epicormic_shoot_label).internode_length_max.val();
5274 plant_instance.shoot_types_snapshot.at(epicormic_shoot_label).internode_length_max.resample();
5275 addEpicormicShoot(plantID, shoot->ID, epicormic_fraction.at(s), 1, 0, internode_radius, internode_length_max, 0.01, 0.01, 0, epicormic_shoot_label);
5276 }
5277 }
5278 if (carbon_model_enabled) {
5279 if (output_object_data.find("carbohydrate_concentration") != output_object_data.end() && context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
5280 float shoot_volume = shoot->calculateShootInternodeVolume();
5281 context_ptr->setObjectData(shoot->internode_tube_objID, "carbohydrate_concentration", shoot->carbohydrate_pool_molC / shoot_volume);
5282 }
5283 }
5284 }
5285
5286
5287 // Update Context geometry based on scheduling configuration
5288 bool should_update_context = collision_detection_enabled && (geometry_update_counter >= geometry_update_frequency);
5289
5290 // Force Context update if collision avoidance was applied and force_update_on_collision is enabled
5291 bool force_update = collision_avoidance_applied && force_update_on_collision;
5292
5293 if (should_update_context || force_update) {
5294 shoot_tree->front()->updateShootNodes(true);
5295 // Note: geometry_update_counter reset moved outside plant loop
5296 } else {
5297 // Update plant structure but not Context geometry
5298 shoot_tree->front()->updateShootNodes(false);
5299 }
5300
5301 // Reset collision avoidance flag for next timestep
5302 collision_avoidance_applied = false;
5303
5304 // *** ground collision detection *** //
5305 if (ground_clipping_height != -99999) {
5306 pruneGroundCollisions(plantID);
5307 }
5308
5309 // **** subtract maintenance carbon costs **** //
5310 if (carbon_model_enabled) {
5311 subtractShootMaintenanceCarbon(dt_max_days);
5312 subtractShootGrowthCarbon();
5313 checkCarbonPool_transferCarbon(dt_max_days);
5314 checkCarbonPool_adjustPhyllochron(dt_max_days);
5315 checkCarbonPool_abortOrgans(dt_max_days);
5316 }
5317
5318 // Assign current volume as old volume for your next timestep
5319 for (auto &shoot: *shoot_tree) {
5320 float shoot_volume = plant_instances.at(plantID).shoot_tree.at(shoot->ID)->calculateShootInternodeVolume();
5321 // Find current volume for each shoot in the plant
5322 float volume_ratio = shoot->old_shoot_volume/shoot_volume;
5323 context_ptr->setObjectData(shoot->internode_tube_objID, "volume_ratio", volume_ratio);
5324 shoot->old_shoot_volume = shoot_volume; // Set old volume to the current volume for the next timestep
5325 context_ptr->setObjectData(shoot->internode_tube_objID, "old_shoot_volume", shoot_volume);
5326 }
5327
5328 // Update plant-level dynamic object data
5329 std::vector<uint> plant_primitives = getAllPlantObjectIDs(plantID);
5330 if (!plant_primitives.empty()) {
5331 if (output_object_data.at("plant_height")) {
5332 context_ptr->setObjectData(plant_primitives, "plant_height", getPlantHeight(plantID));
5333 }
5334 if (output_object_data.at("phenology_stage")) {
5335 context_ptr->setObjectData(plant_primitives, "phenology_stage", determinePhenologyStage(plantID));
5336 }
5337 }
5338 }
5339
5340 // **** nitrogen model operations **** //
5341 if (nitrogen_model_enabled) {
5342 accumulateLeafNitrogen(dt_max_days); // Available pool → leaf pools (rate-limited)
5343 remobilizeNitrogen(dt_max_days); // Old leaves → young leaves (age-based)
5344 removeFruitNitrogen(); // Deduct N from available pool for fruit growth
5345 updateNitrogenStressFactor(); // Calculate and write stress factor to object data
5346 }
5347
5348 // Reset geometry counter if updates occurred this timestep
5349 if (geometry_update_counter >= geometry_update_frequency) {
5350 geometry_update_counter = 0;
5351 } else {
5352 geometry_update_counter++;
5353 }
5354
5355 // Update progress bar
5356 progress_bar.update();
5357 }
5358
5359 // Adjust fruit positions to avoid solid obstacle collisions
5361
5362 // Fallback collision detection: prune any objects that still intersect solid boundaries
5363 if (solid_obstacle_pruning_enabled) {
5364 pruneSolidBoundaryCollisions();
5365 }
5366
5367 // When collision detection is disabled, update all plant geometry once at the end
5368 // This is more efficient than periodic updates and ensures correct visualization
5369 if (!collision_detection_enabled) {
5370 for (uint plantID: plantIDs) {
5371 if (plant_instances.find(plantID) != plant_instances.end()) {
5372 plant_instances.at(plantID).shoot_tree.front()->updateShootNodes(true);
5373 }
5374 }
5375 }
5376
5377 // Update age object data once at the end for performance
5378 // This avoids updating age data every timestep (which would be ~100x more calls)
5379 if (output_object_data.at("age")) {
5380 for (uint plantID: plantIDs) {
5381 if (plant_instances.find(plantID) == plant_instances.end()) {
5382 continue;
5383 }
5384
5385 auto shoot_tree = &plant_instances.at(plantID).shoot_tree;
5386 for (auto &shoot: *shoot_tree) {
5387 // Update internode age once per shoot (fixes redundancy noted in previous TODO)
5388 // All phytomers in a shoot share the same internode tube object
5389 if (shoot->build_context_geometry_internode && !shoot->phytomers.empty()) {
5390 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
5391 // Use the age of the youngest (last) phytomer as the shoot age
5392 float shoot_age = shoot->phytomers.back()->age;
5393 context_ptr->setObjectData(shoot->internode_tube_objID, "age", shoot_age);
5394 }
5395 }
5396
5397 // Update each phytomer's petiole and leaf age
5398 for (auto &phytomer: shoot->phytomers) {
5399 if (phytomer->build_context_geometry_petiole) {
5400 context_ptr->setObjectData(phytomer->petiole_objIDs, "age", phytomer->age);
5401 }
5402 context_ptr->setObjectData(phytomer->leaf_objIDs, "age", phytomer->age);
5403 }
5404 }
5405 }
5406 }
5407
5408 // Ensure progress bar shows 100% completion
5409 progress_bar.finish();
5410}
5411
5413 if (!solid_obstacle_avoidance_enabled || solid_obstacle_UUIDs.empty() || !solid_obstacle_fruit_adjustment_enabled) {
5414 return; // No obstacles to check or fruit adjustment disabled
5415 }
5416
5417 if (collision_detection_ptr == nullptr) {
5418 return; // No collision detection available
5419 }
5420
5421 // Debug counter to limit output
5422 int debug_failures_shown = 0;
5423 const int max_debug_failures = 0; // Disable debugging for performance
5424
5425 // Initialize progress bar for processing plants
5426 helios::ProgressBar progress_bar(plant_instances.size(), 50, plant_instances.size() > 1 && printmessages, "Adjusting fruit collisions");
5427
5428 // Process each plant instance
5429 for (const auto &plant_instance: plant_instances) {
5430 uint plantID = plant_instance.first;
5431
5432 // Get all fruit object IDs for this plant
5433 std::vector<uint> fruit_objIDs = getPlantFruitObjectIDs(plantID);
5434
5435 if (fruit_objIDs.empty()) {
5436 continue; // No fruit to process
5437 }
5438
5439 // Check each fruit for collision
5440 for (uint fruit_objID: fruit_objIDs) {
5441 // Get fruit primitives
5442 std::vector<uint> fruit_UUIDs = context_ptr->getObjectPrimitiveUUIDs(fruit_objID);
5443
5444 if (fruit_UUIDs.empty()) {
5445 continue;
5446 }
5447
5448 // Check if fruit collides with any solid obstacle
5449 std::vector<uint> collisions = collision_detection_ptr->findCollisions(fruit_UUIDs, {}, solid_obstacle_UUIDs, {}, false);
5450
5451 if (!collisions.empty()) {
5452 // Fruit is colliding - need to rotate it up
5453
5454 // Get fruit bounding box to estimate rotation needed
5455 vec3 bbox_min, bbox_max;
5456 context_ptr->getObjectBoundingBox(fruit_objID, bbox_min, bbox_max);
5457
5458 // Find the fruit base position and peduncle info from the shoot tree
5459 vec3 fruit_base;
5460 vec3 peduncle_axis;
5461 const Phytomer *fruit_phytomer = nullptr;
5462 uint fruit_petiole_index = 0;
5463 uint fruit_bud_index = 0;
5464 bool found_base = false;
5465
5466 // Search through shoot tree to find this fruit's base position
5467 for (const auto &shoot: plant_instance.second.shoot_tree) {
5468 for (const auto &phytomer: shoot->phytomers) {
5469 uint petiole_idx = 0;
5470 for (const auto &petiole: phytomer->floral_buds) {
5471 for (const auto &fbud: petiole) {
5472 // Check if this floral bud contains our fruit
5473 for (size_t idx = 0; idx < fbud.inflorescence_objIDs.size(); idx++) {
5474 if (fbud.inflorescence_objIDs[idx] == fruit_objID && idx < fbud.inflorescence_bases.size()) {
5475 // Found it! Use the correct index to get the base position
5476 fruit_base = fbud.inflorescence_bases[idx];
5477 fruit_phytomer = phytomer.get();
5478 fruit_petiole_index = petiole_idx;
5479 fruit_bud_index = fbud.bud_index;
5480
5481 // Get actual peduncle axis using stored vertices
5482 try {
5483 peduncle_axis = phytomer->getPeduncleAxisVector(1.0f, petiole_idx, fbud.bud_index);
5484 } catch (const std::exception &e) {
5485 // Fallback if peduncle vertices not available
5486 peduncle_axis = make_vec3(0, 0, 1);
5487 }
5488
5489 found_base = true;
5490 break;
5491 }
5492 }
5493 if (found_base)
5494 break;
5495 }
5496 if (found_base)
5497 break;
5498 petiole_idx++;
5499 }
5500 if (found_base)
5501 break;
5502 }
5503 if (found_base)
5504 break;
5505 }
5506
5507 if (!found_base) {
5508 continue; // Couldn't find fruit base position
5509 }
5510
5511 // Calculate initial rotation estimate
5512 // Estimate fruit "radius" as distance from base to furthest point
5513 float fruit_radius = 0;
5514 fruit_radius = std::max(fruit_radius, (bbox_max - fruit_base).magnitude());
5515 fruit_radius = std::max(fruit_radius, (bbox_min - fruit_base).magnitude());
5516 fruit_radius = std::max(fruit_radius, (make_vec3(bbox_min.x, bbox_min.y, bbox_max.z) - fruit_base).magnitude());
5517 fruit_radius = std::max(fruit_radius, (make_vec3(bbox_min.x, bbox_max.y, bbox_min.z) - fruit_base).magnitude());
5518 fruit_radius = std::max(fruit_radius, (make_vec3(bbox_max.x, bbox_min.y, bbox_min.z) - fruit_base).magnitude());
5519 fruit_radius = std::max(fruit_radius, (make_vec3(bbox_min.x, bbox_max.y, bbox_max.z) - fruit_base).magnitude());
5520 fruit_radius = std::max(fruit_radius, (make_vec3(bbox_max.x, bbox_min.y, bbox_max.z) - fruit_base).magnitude());
5521 fruit_radius = std::max(fruit_radius, (make_vec3(bbox_max.x, bbox_max.y, bbox_min.z) - fruit_base).magnitude());
5522
5523 // Calculate penetration depth more accurately
5524 // Use the lowest point of the fruit bounding box vs ground level (z=0)
5525 float penetration_depth = std::max(0.0f, -bbox_min.z);
5526
5527 // Calculate initial rotation guess
5528 float initial_rotation = 0;
5529 if (fruit_radius > 0 && penetration_depth > 0) {
5530 // Use arc sine to estimate rotation needed
5531 float angle_estimate = std::asin(std::min(1.0f, penetration_depth / fruit_radius));
5532 // Multiply by 1.5 to account for fruit shape complexity (less aggressive than before)
5533 initial_rotation = std::min(deg2rad(35.0f), angle_estimate * 1.5f);
5534 } else {
5535 // Default rotation for partially submerged cases
5536 initial_rotation = deg2rad(10.0f);
5537 }
5538
5539 // Ensure minimum rotation for any collision case
5540 initial_rotation = std::max(initial_rotation, deg2rad(8.0f)); // Slightly smaller minimum
5541
5542 // Calculate the proper rotation axis based on peduncle orientation
5543 vec3 rotation_axis;
5544
5545 // Ensure peduncle axis is normalized
5546 if (peduncle_axis.magnitude() < 1e-6f) {
5547 // Fallback if peduncle axis is not available
5548 peduncle_axis = make_vec3(0, 0, 1);
5549 } else {
5550 peduncle_axis.normalize();
5551 }
5552
5553 // Get vector from fruit base to fruit center
5554 vec3 bbox_center = 0.5f * (bbox_min + bbox_max);
5555 vec3 to_fruit_center = bbox_center - fruit_base;
5556 if (to_fruit_center.magnitude() > 1e-6f) {
5557 to_fruit_center.normalize();
5558 } else {
5559 // If fruit center is at base, use peduncle direction
5560 to_fruit_center = peduncle_axis;
5561 }
5562
5563 // Rotation axis is perpendicular to both peduncle axis and to_fruit_center
5564 // This gives us the pitch rotation axis used for the original fruit positioning
5565 rotation_axis = cross(peduncle_axis, to_fruit_center);
5566 if (rotation_axis.magnitude() < 1e-6f) {
5567 // Peduncle and fruit are aligned, use perpendicular to peduncle
5568 if (std::abs(peduncle_axis.z) < 0.9f) {
5569 rotation_axis = cross(peduncle_axis, make_vec3(0, 0, 1));
5570 } else {
5571 rotation_axis = cross(peduncle_axis, make_vec3(1, 0, 0));
5572 }
5573 }
5574 rotation_axis.normalize();
5575
5576 // Iteratively rotate fruit until no collision
5577 float rotation_step = initial_rotation;
5578 float total_rotation = 0;
5579 const float max_rotation = deg2rad(120.0f); // Allow more rotation
5580 const int max_iterations = 25; // More iterations
5581
5582 // Debug info for this fruit (only show first few)
5583 bool debug_this_fruit = (debug_failures_shown < max_debug_failures);
5584 if (debug_this_fruit && printmessages) {
5585 std::cout << "\n=== DEBUG: Fruit " << fruit_objID << " collision adjustment ===" << std::endl;
5586 std::cout << "Fruit base: " << fruit_base << std::endl;
5587 std::cout << "Fruit bbox: " << bbox_min << " to " << bbox_max << std::endl;
5588 std::cout << "Fruit radius: " << fruit_radius << std::endl;
5589 std::cout << "Penetration depth: " << penetration_depth << std::endl;
5590 std::cout << "Peduncle axis: " << peduncle_axis << std::endl;
5591 std::cout << "Rotation axis: " << rotation_axis << std::endl;
5592 std::cout << "Initial rotation: " << rad2deg(initial_rotation) << " degrees" << std::endl;
5593 std::cout << "Initial collisions: " << collisions.size() << std::endl;
5594 }
5595
5596 for (int iter = 0; iter < max_iterations && total_rotation < max_rotation; iter++) {
5597 // Apply rotation about fruit base
5598 // Negative rotation to lift fruit up (opposite of gravity)
5599 context_ptr->rotateObject(fruit_objID, -rotation_step, fruit_base, rotation_axis);
5600 total_rotation += rotation_step;
5601
5602 // Check if still colliding
5603 fruit_UUIDs = context_ptr->getObjectPrimitiveUUIDs(fruit_objID);
5604 collisions = collision_detection_ptr->findCollisions(fruit_UUIDs, {}, solid_obstacle_UUIDs, {}, false);
5605
5606 if (debug_this_fruit && printmessages) {
5607 std::cout << "Iter " << iter << ": rotated " << rad2deg(rotation_step) << " deg (total " << rad2deg(total_rotation) << "), collisions: " << collisions.size() << std::endl;
5608 }
5609
5610 if (collisions.empty()) {
5611 // No longer colliding - now try to fine-tune by rotating back down slightly
5612 // to get as close to the ground as possible
5613 float fine_tune_step = deg2rad(3.0f); // Slightly larger steps for efficiency
5614 float fine_tune_attempts = 5;
5615 float original_total = total_rotation;
5616
5617 if (debug_this_fruit && printmessages) {
5618 std::cout << "Fine-tuning: trying to rotate back down from " << rad2deg(total_rotation) << " degrees" << std::endl;
5619 }
5620
5621 for (int fine_iter = 0; fine_iter < fine_tune_attempts; fine_iter++) {
5622 // Try rotating back towards ground (positive rotation)
5623 context_ptr->rotateObject(fruit_objID, fine_tune_step, fruit_base, rotation_axis);
5624
5625 // Check if still collision-free
5626 fruit_UUIDs = context_ptr->getObjectPrimitiveUUIDs(fruit_objID);
5627 std::vector<uint> test_collisions = collision_detection_ptr->findCollisions(fruit_UUIDs, {}, solid_obstacle_UUIDs, {}, false);
5628
5629 if (!test_collisions.empty()) {
5630 // Collision detected - rotate back up and stop fine-tuning
5631 context_ptr->rotateObject(fruit_objID, -fine_tune_step, fruit_base, rotation_axis);
5632 break;
5633 } else {
5634 // Still collision-free, reduce total rotation count
5635 total_rotation -= fine_tune_step;
5636 }
5637 }
5638
5639 break;
5640 }
5641
5642 // Adaptive step size - reduce for fine tuning, but not too aggressively
5643 if (iter > 8) {
5644 rotation_step *= 0.7f; // Less aggressive reduction
5645 }
5646 }
5647
5648 if (!collisions.empty()) {
5649 if (debug_this_fruit && printmessages) {
5650 std::cout << "FAILED: Fruit " << fruit_objID << " still colliding after " << rad2deg(total_rotation) << " degrees rotation (" << max_iterations << " iterations)" << std::endl;
5651
5652 // Get final bounding box to see where it ended up
5653 vec3 final_bbox_min, final_bbox_max;
5654 context_ptr->getObjectBoundingBox(fruit_objID, final_bbox_min, final_bbox_max);
5655 std::cout << "Final bbox: " << final_bbox_min << " to " << final_bbox_max << std::endl;
5656 std::cout << "Lowest point: " << final_bbox_min.z << std::endl;
5657
5658 debug_failures_shown++;
5659 }
5660 }
5661 }
5662 }
5663
5664 // Update progress bar
5665 progress_bar.update();
5666 }
5667
5668 // Ensure progress bar shows 100% completion
5669 progress_bar.finish();
5670}
5671
5672void PlantArchitecture::pruneSolidBoundaryCollisions() {
5673 if (!solid_obstacle_avoidance_enabled || solid_obstacle_UUIDs.empty()) {
5674 return; // No solid boundaries defined
5675 }
5676
5677 if (collision_detection_ptr == nullptr) {
5678 return; // No collision detection available
5679 }
5680
5681 if (printmessages) {
5682 std::cout << "Performing solid boundary collision detection..." << std::endl;
5683 }
5684
5685 // The BVH should already be current from advanceTime() - we're called at the very end
5686 // Collect all plant primitives and do one batch collision detection call for efficiency
5687 std::vector<uint> all_plant_primitives;
5688
5689 all_plant_primitives = getAllUUIDs();
5690
5691 std::vector<uint> intersecting_primitives = collision_detection_ptr->findCollisions(solid_obstacle_UUIDs, {}, all_plant_primitives, {}, false);
5692
5693 std::vector<uint> intersecting_objIDs = context_ptr->getUniquePrimitiveParentObjectIDs(intersecting_primitives);
5694
5695
5696 if (intersecting_primitives.empty()) {
5697 if (printmessages) {
5698 std::cout << "No collisions detected - this is unexpected given visible fruit penetration" << std::endl;
5699 }
5700 return; // No collisions detected
5701 }
5702
5703 if (printmessages) {
5704 std::cout << "Intersecting primitives found: " << intersecting_primitives.size() << std::endl;
5705 }
5706
5707 // Create lookup set for O(1) collision checking
5708 std::unordered_set<uint> collision_set(intersecting_objIDs.begin(), intersecting_objIDs.end());
5709
5710 // Traverse plant topology and prune intersected organs and all downstream organs
5711 for (auto &[plantID, plant]: plant_instances) {
5712 for (uint shootID = 0; shootID < plant.shoot_tree.size(); shootID++) {
5713 auto &shoot = plant.shoot_tree.at(shootID);
5714 bool shoot_was_deleted = false;
5715
5716 // Check if entire shoot's internode tube is colliding
5717 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
5718 if (collision_set.count(shoot->internode_tube_objID)) {
5719 // Protect the entire main stem (rank 0 shoots)
5720 if (shoot->rank != 0) {
5721 // Delete the entire branch shoot
5722 pruneBranch(plantID, shootID, 0); // Prune from the beginning of the shoot
5723 shoot_was_deleted = true;
5724 }
5725 }
5726 }
5727
5728 // If the shoot was deleted due to internode collision, skip checking individual organs
5729 if (shoot_was_deleted) {
5730 continue;
5731 }
5732
5733 for (uint node = 0; node < shoot->current_node_number; node++) {
5734 auto &phytomer = shoot->phytomers.at(node);
5735
5736 // Check leaves for collision
5737 for (uint petiole = 0; petiole < phytomer->leaf_objIDs.size(); petiole++) {
5738 for (uint leaflet = 0; leaflet < phytomer->leaf_objIDs.at(petiole).size(); leaflet++) {
5739 uint leaf_objID = phytomer->leaf_objIDs.at(petiole).at(leaflet);
5740 if (collision_set.count(leaf_objID)) {
5741 phytomer->removeLeaf();
5742 break; // removeLeaf() removes all leaflets on this petiole
5743 }
5744 }
5745 }
5746
5747 // Check petiole objects for collision
5748 for (uint petiole = 0; petiole < phytomer->petiole_objIDs.size(); petiole++) {
5749 for (uint segment = 0; segment < phytomer->petiole_objIDs.at(petiole).size(); segment++) {
5750 uint petiole_objID = phytomer->petiole_objIDs.at(petiole).at(segment);
5751 if (collision_set.count(petiole_objID)) {
5752 phytomer->removeLeaf();
5753 break; // removeLeaf() removes petiole and all leaflets
5754 }
5755 }
5756 }
5757
5758 // Check inflorescence for collision
5759 for (auto &petiole: phytomer->floral_buds) {
5760 for (auto &fbud: petiole) {
5761 // Check inflorescence objects
5762 for (int p = fbud.inflorescence_objIDs.size() - 1; p >= 0; p--) {
5763 uint objID = fbud.inflorescence_objIDs.at(p);
5764 if (collision_set.count(objID)) {
5765 context_ptr->deleteObject(objID);
5766 fbud.inflorescence_objIDs.erase(fbud.inflorescence_objIDs.begin() + p);
5767 fbud.inflorescence_bases.erase(fbud.inflorescence_bases.begin() + p);
5768 }
5769 }
5770 // Check peduncle objects
5771 for (int p = fbud.peduncle_objIDs.size() - 1; p >= 0; p--) {
5772 uint objID = fbud.peduncle_objIDs.at(p);
5773 if (collision_set.count(objID)) {
5774 // Delete all peduncle and inflorescence objects for this floral bud
5775 context_ptr->deleteObject(fbud.peduncle_objIDs);
5776 context_ptr->deleteObject(fbud.inflorescence_objIDs);
5777 fbud.peduncle_objIDs.clear();
5778 fbud.inflorescence_objIDs.clear();
5779 fbud.inflorescence_bases.clear();
5780 break;
5781 }
5782 }
5783 }
5784 }
5785 }
5786
5787 if (shoot_was_deleted) {
5788 break; // This shoot was pruned, no need to check more nodes
5789 }
5790 }
5791 }
5792
5793 if (printmessages) {
5794 std::cout << "Solid boundary collision pruning completed" << std::endl;
5795 }
5796}
5797
5798std::vector<uint> makeTubeFromCones(uint radial_subdivisions, const std::vector<helios::vec3> &vertices, const std::vector<float> &radii, const std::vector<helios::RGBcolor> &colors, helios::Context *context_ptr) {
5799 uint Nverts = vertices.size();
5800
5801 if (radii.size() != Nverts || colors.size() != Nverts) {
5802 helios_runtime_error("ERROR (makeTubeFromCones): Length of vertex vectors is not consistent.");
5803 }
5804
5805 // Check if tube is too small to create geometry - check both radii and total length
5806 bool all_radii_too_small = true;
5807 float max_radius = 0.0f;
5808 for (float radius: radii) {
5809 max_radius = std::max(max_radius, radius);
5810 if (radius >= MIN_TUBE_RADIUS_FOR_GEOMETRY) {
5811 all_radii_too_small = false;
5812 break;
5813 }
5814 }
5815
5816 // Calculate total tube length
5817 float total_length = 0.0f;
5818 for (uint v = 0; v < Nverts - 1; v++) {
5819 total_length += (vertices.at(v + 1) - vertices.at(v)).magnitude();
5820 }
5821
5822
5823 // Return empty if either condition fails
5824 if (all_radii_too_small || total_length < MIN_TUBE_LENGTH_FOR_GEOMETRY) {
5825 return std::vector<uint>();
5826 }
5827
5828 std::vector<uint> objIDs;
5829 objIDs.reserve(Nverts - 1);
5830
5831 for (uint v = 0; v < Nverts - 1; v++) {
5832 if ((vertices.at(v + 1) - vertices.at(v)).magnitude() < 1e-6f) {
5833 continue;
5834 }
5835 float r0 = std::max(radii.at(v), MIN_TUBE_RADIUS_FOR_GEOMETRY);
5836 float r1 = std::max(radii.at(v + 1), MIN_TUBE_RADIUS_FOR_GEOMETRY);
5837 objIDs.push_back(context_ptr->addConeObject(radial_subdivisions, vertices.at(v), vertices.at(v + 1), r0, r1, colors.at(v)));
5838 }
5839
5840 return objIDs;
5841}
5842
5843bool PlantArchitecture::detectGroundCollision(uint objID) {
5844 std::vector<uint> objIDs = {objID};
5845 return detectGroundCollision(objIDs);
5846}
5847
5848bool PlantArchitecture::detectGroundCollision(const std::vector<uint> &objID) const {
5849 for (uint ID: objID) {
5850 if (context_ptr->doesObjectExist(ID)) {
5851 const std::vector<uint> &UUIDs = context_ptr->getObjectPrimitiveUUIDs(ID);
5852 for (uint UUID: UUIDs) {
5853 const std::vector<vec3> &vertices = context_ptr->getPrimitiveVertices(UUID);
5854 for (const vec3 &v: vertices) {
5855 if (v.z < ground_clipping_height) {
5856 return true;
5857 }
5858 }
5859 }
5860 }
5861 }
5862 return false;
5863}
5864
5865void PlantArchitecture::optionalOutputObjectData(const std::string &object_data_label) {
5866 // Convert label to lowercase for case-insensitive comparison
5867 std::string label_lower = object_data_label;
5868 std::transform(label_lower.begin(), label_lower.end(), label_lower.begin(), ::tolower);
5869
5870 // Check if "all" was requested
5871 if (label_lower == "all") {
5872 // Enable all optional output object data
5873 for (auto &item: output_object_data) {
5874 item.second = true;
5875 }
5876 return;
5877 }
5878
5879 // Check if the label is valid
5880 if (output_object_data.find(object_data_label) == output_object_data.end()) {
5881 helios_runtime_error("ERROR (PlantArchitecture::optionalOutputObjectData): Output object data of '" + object_data_label + "' is not a valid option.");
5882 }
5883
5884 output_object_data.at(object_data_label) = true;
5885}
5886
5887void PlantArchitecture::optionalOutputObjectData(const std::vector<std::string> &object_data_labels) {
5888 for (const auto &label: object_data_labels) {
5889 // Call the single-string overload which handles "all" and error checking
5891 }
5892}
5893
5894void PlantArchitecture::enableSoftCollisionAvoidance(const std::vector<uint> &target_object_UUIDs, const std::vector<uint> &target_object_IDs, bool enable_petiole_collision, bool enable_fruit_collision) {
5895 // Clean up any existing collision detection instance
5896 if (collision_detection_ptr != nullptr && owns_collision_detection) {
5897 delete collision_detection_ptr;
5898 collision_detection_ptr = nullptr;
5899 owns_collision_detection = false;
5900 }
5901
5902 // Create new CollisionDetection instance
5903 try {
5904 collision_detection_ptr = new CollisionDetection(context_ptr);
5905 collision_detection_ptr->enableMessages(); // Enable debug output for debugging
5906 owns_collision_detection = true;
5907 collision_detection_enabled = true;
5908 collision_target_UUIDs = target_object_UUIDs;
5909 collision_target_object_IDs = target_object_IDs;
5910
5911 // Set organ-specific collision detection flags
5912 petiole_collision_detection_enabled = enable_petiole_collision;
5913 fruit_collision_detection_enabled = enable_fruit_collision;
5914
5915 // Disable automatic BVH rebuilds - PlantArchitecture will control rebuilds manually
5916 collision_detection_ptr->disableAutomaticBVHRebuilds();
5917
5918 // Enable per-tree BVH for linear scaling with multiple trees
5919 collision_detection_ptr->enableTreeBasedBVH(collision_cone_height); // Use collision cone height as isolation distance
5920
5921 // Set static obstacles (non-plant geometry that affects all trees)
5922 std::vector<uint> static_obstacles;
5923 static_obstacles.insert(static_obstacles.end(), target_object_UUIDs.begin(), target_object_UUIDs.end());
5924 static_obstacles.insert(static_obstacles.end(), target_object_IDs.begin(), target_object_IDs.end());
5925
5926 // Build initial BVH cache to prevent warnings during early collision detection calls
5927 rebuildBVHForTimestep();
5928
5929 // Also include solid obstacle avoidance primitives if enabled
5930 if (solid_obstacle_avoidance_enabled) {
5931 static_obstacles.insert(static_obstacles.end(), solid_obstacle_UUIDs.begin(), solid_obstacle_UUIDs.end());
5932 }
5933
5934 collision_detection_ptr->setStaticObstacles(static_obstacles);
5935
5936 // Register existing plants as separate trees for per-tree BVH
5937 // This allows each plant to have its own collision BVH for linear scaling
5938 std::vector<uint> plant_ids = getAllPlantIDs();
5939 for (uint plant_id: plant_ids) {
5940 std::vector<uint> plant_primitives = getPlantCollisionRelevantObjectIDs(plant_id);
5941 if (!plant_primitives.empty()) {
5942 collision_detection_ptr->registerTree(plant_id, plant_primitives);
5943 }
5944 }
5945
5946 setGeometryUpdateScheduling(3, true); // Update every 3 timesteps, force on collision
5947
5948 } catch (const std::exception &e) {
5949 helios_runtime_error("ERROR (PlantArchitecture::enableSoftCollisionAvoidance): Failed to create CollisionDetection instance: " + std::string(e.what()));
5950 }
5951}
5952
5954 collision_detection_enabled = false;
5955
5956 // Clean up owned CollisionDetection instance
5957 if (collision_detection_ptr != nullptr && owns_collision_detection) {
5958 delete collision_detection_ptr;
5959 owns_collision_detection = false;
5960 }
5961
5962 collision_detection_ptr = nullptr;
5963 collision_target_UUIDs.clear();
5964 collision_target_object_IDs.clear();
5965
5966 if (printmessages) {
5967 std::cout << "Collision detection disabled for plant growth and internal instance cleaned up" << std::endl;
5968 }
5969}
5970
5971void PlantArchitecture::setSoftCollisionAvoidanceParameters(float view_half_angle_deg, float look_ahead_distance, int sample_count, float inertia_weight) {
5972 if (view_half_angle_deg <= 0.0f || view_half_angle_deg > 180.f) {
5973 helios_runtime_error("ERROR (PlantArchitecture::setSoftCollisionAvoidanceParameters): cone_half_angle_deg must be between 0 and 180 degrees.");
5974 }
5975 if (look_ahead_distance <= 0.0f) {
5976 helios_runtime_error("ERROR (PlantArchitecture::setSoftCollisionAvoidanceParameters): sample_count must be positive.");
5977 }
5978 if (inertia_weight < 0.0f || inertia_weight > 1.0f) {
5979 helios_runtime_error("ERROR (PlantArchitecture::setSoftCollisionAvoidanceParameters): inertia_weight must be between 0.0 and 1.0.");
5980 }
5981
5982 collision_cone_half_angle_rad = deg2rad(view_half_angle_deg);
5983 collision_cone_height = look_ahead_distance;
5984 collision_sample_count = sample_count;
5985 collision_inertia_weight = inertia_weight;
5986}
5987
5988void PlantArchitecture::setStaticObstacles(const std::vector<uint> &target_UUIDs) {
5989 if (collision_detection_ptr == nullptr) {
5990 helios_runtime_error("ERROR (PlantArchitecture::setStaticObstacles): Collision detection must be enabled before setting static obstacles.");
5991 }
5992
5993 collision_detection_ptr->setStaticGeometry(target_UUIDs);
5994
5995 if (printmessages) {
5996 std::cout << "Marked " << target_UUIDs.size() << " primitives as static obstacles for collision detection" << std::endl;
5997 }
5998}
5999
6001 return collision_detection_ptr;
6002}
6003
6004void PlantArchitecture::setCollisionRelevantOrgans(bool include_internodes, bool include_leaves, bool include_petioles, bool include_flowers, bool include_fruit) {
6005 collision_include_internodes = include_internodes;
6006 collision_include_leaves = include_leaves;
6007 collision_include_petioles = include_petioles;
6008 collision_include_flowers = include_flowers;
6009 collision_include_fruit = include_fruit;
6010
6011 // Clear BVH cache to force rebuild with new organ filtering
6012 clearBVHCache();
6013
6014 if (printmessages) {
6015 std::cout << "Set collision-relevant organs: internodes=" << (include_internodes ? "yes" : "no") << ", leaves=" << (include_leaves ? "yes" : "no") << ", petioles=" << (include_petioles ? "yes" : "no")
6016 << ", flowers=" << (include_flowers ? "yes" : "no") << ", fruit=" << (include_fruit ? "yes" : "no") << std::endl;
6017 }
6018}
6019
6020
6021void PlantArchitecture::enableSolidObstacleAvoidance(const std::vector<uint> &obstacle_UUIDs, float avoidance_distance, bool enable_fruit_adjustment, bool enable_obstacle_pruning) {
6022 solid_obstacle_avoidance_enabled = true;
6023 solid_obstacle_UUIDs = obstacle_UUIDs;
6024 solid_obstacle_avoidance_distance = avoidance_distance;
6025 solid_obstacle_fruit_adjustment_enabled = enable_fruit_adjustment;
6026 solid_obstacle_pruning_enabled = enable_obstacle_pruning;
6027
6028 // Create CollisionDetection instance if needed for solid obstacle avoidance
6029 if (collision_detection_ptr == nullptr) {
6030 try {
6031 collision_detection_ptr = new CollisionDetection(context_ptr);
6032 collision_detection_ptr->enableMessages(); // Enable debug output for debugging
6033 owns_collision_detection = true;
6034 collision_detection_enabled = true;
6035
6036 // Disable automatic BVH rebuilds - PlantArchitecture will control rebuilds manually
6037 collision_detection_ptr->disableAutomaticBVHRebuilds();
6038 // Enable per-tree BVH for linear scaling with multiple trees
6039 collision_detection_ptr->enableTreeBasedBVH(collision_cone_height); // Use collision cone height as isolation distance
6040
6041 // Build initial BVH cache to prevent warnings during early collision detection calls
6042 rebuildBVHForTimestep();
6043 } catch (std::exception &e) {
6044 helios_runtime_error("ERROR (PlantArchitecture::enableSolidObstacleAvoidance): Failed to create CollisionDetection instance: " + std::string(e.what()));
6045 }
6046 }
6047
6048 // Update CollisionDetection static obstacles if per-tree BVH is enabled
6049 if (collision_detection_enabled && collision_detection_ptr != nullptr && collision_detection_ptr->isTreeBasedBVHEnabled()) {
6050 std::vector<uint> static_obstacles;
6051 static_obstacles.insert(static_obstacles.end(), collision_target_UUIDs.begin(), collision_target_UUIDs.end());
6052 static_obstacles.insert(static_obstacles.end(), collision_target_object_IDs.begin(), collision_target_object_IDs.end());
6053 static_obstacles.insert(static_obstacles.end(), solid_obstacle_UUIDs.begin(), solid_obstacle_UUIDs.end());
6054
6055 collision_detection_ptr->setStaticObstacles(static_obstacles);
6056 }
6057}
6058
6059void PlantArchitecture::clearBVHCache() const {
6060 bvh_cached_for_current_growth = false;
6061 cached_target_geometry.clear();
6062 cached_filtered_geometry.clear();
6063}
6064
6065
6066void PlantArchitecture::rebuildBVHForTimestep() {
6067 if (!collision_detection_enabled || collision_detection_ptr == nullptr) {
6068 return;
6069 }
6070
6071
6072 // Determine target geometry for BVH
6073 std::vector<uint> target_geometry;
6074
6075 // Always include solid obstacles if enabled
6076 if (solid_obstacle_avoidance_enabled && !solid_obstacle_UUIDs.empty()) {
6077 target_geometry.insert(target_geometry.end(), solid_obstacle_UUIDs.begin(), solid_obstacle_UUIDs.end());
6078 }
6079
6080 if (!collision_target_UUIDs.empty()) {
6081 // Validate that all target UUIDs still exist
6082 std::vector<uint> valid_targets;
6083 for (uint uuid: collision_target_UUIDs) {
6084 if (context_ptr->doesPrimitiveExist(uuid)) {
6085 valid_targets.push_back(uuid);
6086 }
6087 }
6088 // Add valid collision targets to existing target_geometry (which may include solid obstacles)
6089 target_geometry.insert(target_geometry.end(), valid_targets.begin(), valid_targets.end());
6090 } else if (!collision_target_object_IDs.empty()) {
6091 // Add object primitives to existing target_geometry (which may include solid obstacles)
6092 for (uint objID: collision_target_object_IDs) {
6093 if (context_ptr->doesObjectExist(objID)) {
6094 std::vector<uint> obj_primitives = context_ptr->getObjectPrimitiveUUIDs(objID);
6095 target_geometry.insert(target_geometry.end(), obj_primitives.begin(), obj_primitives.end());
6096 }
6097 }
6098 } else {
6099 // Use filtered plant geometry based on organ settings + external obstacles
6100 // Preserve solid obstacles that were already added
6101 std::vector<uint> preserved_solid_obstacles = target_geometry;
6102 target_geometry.clear();
6103
6104 // Add collision-relevant plant organs based on filtering settings (with safety checks)
6105 try {
6106 if (collision_include_internodes) {
6107 std::vector<uint> internode_uuids = getAllInternodeUUIDs();
6108 target_geometry.insert(target_geometry.end(), internode_uuids.begin(), internode_uuids.end());
6109 }
6110 if (collision_include_leaves) {
6111 std::vector<uint> leaf_uuids = getAllLeafUUIDs();
6112 target_geometry.insert(target_geometry.end(), leaf_uuids.begin(), leaf_uuids.end());
6113 }
6114 if (collision_include_petioles) {
6115 std::vector<uint> petiole_uuids = getAllPetioleUUIDs();
6116 target_geometry.insert(target_geometry.end(), petiole_uuids.begin(), petiole_uuids.end());
6117 }
6118 if (collision_include_flowers) {
6119 std::vector<uint> flower_uuids = getAllFlowerUUIDs();
6120 target_geometry.insert(target_geometry.end(), flower_uuids.begin(), flower_uuids.end());
6121 }
6122 if (collision_include_fruit) {
6123 std::vector<uint> fruit_uuids = getAllFruitUUIDs();
6124 target_geometry.insert(target_geometry.end(), fruit_uuids.begin(), fruit_uuids.end());
6125 }
6126 } catch (const std::exception &e) {
6127 if (printmessages) {
6128 std::cout << "Warning: Exception in organ filtering, falling back to all geometry: " << e.what() << std::endl;
6129 }
6130 target_geometry = context_ptr->getAllUUIDs();
6131 }
6132
6133 // Re-add the preserved solid obstacles
6134 target_geometry.insert(target_geometry.end(), preserved_solid_obstacles.begin(), preserved_solid_obstacles.end());
6135
6136 // Add any external obstacles from Context (non-plant geometry)
6137 std::vector<uint> all_context_geometry = context_ptr->getAllUUIDs();
6138 std::set<uint> all_plant_geometry_set;
6139 try {
6140 std::vector<uint> all_plant = getAllUUIDs();
6141 all_plant_geometry_set.insert(all_plant.begin(), all_plant.end());
6142 } catch (const std::exception &e) {
6143 if (printmessages) {
6144 std::cout << "Warning: Could not get plant geometry for external obstacle filtering: " << e.what() << std::endl;
6145 }
6146 }
6147
6148 for (uint uuid: all_context_geometry) {
6149 if (all_plant_geometry_set.find(uuid) == all_plant_geometry_set.end()) {
6150 target_geometry.push_back(uuid); // Add external obstacles
6151 }
6152 }
6153 }
6154
6155 if (!target_geometry.empty()) {
6156 // Separate static obstacles from plant geometry for hierarchical BVH
6157 std::vector<uint> plant_geometry;
6158 try {
6159 plant_geometry = getAllUUIDs();
6160 } catch (const std::exception &e) {
6161 if (printmessages) {
6162 std::cout << "Warning: Could not get plant geometry for hierarchical BVH: " << e.what() << std::endl;
6163 }
6164 plant_geometry.clear();
6165 }
6166 std::set<uint> plant_set(plant_geometry.begin(), plant_geometry.end());
6167
6168 std::vector<uint> static_obstacles;
6169 for (uint uuid: target_geometry) {
6170 if (plant_set.find(uuid) == plant_set.end()) {
6171 static_obstacles.push_back(uuid); // Not plant geometry = static obstacle
6172 }
6173 }
6174
6175 collision_detection_ptr->setStaticGeometry(static_obstacles);
6176
6177 // Build BVH once per timestep
6178 collision_detection_ptr->updateBVH(target_geometry, true); // Force rebuild
6179
6180
6181 // Cache the geometry for this growth cycle
6182 cached_target_geometry = target_geometry;
6183 cached_filtered_geometry = target_geometry; // No filtering at timestep level
6184 bvh_cached_for_current_growth = true;
6185 }
6186}
6187
6188void PlantArchitecture::setGeometryUpdateScheduling(int update_frequency, bool force_update_on_collision) {
6189 if (update_frequency < 1) {
6190 helios_runtime_error("ERROR (PlantArchitecture::setGeometryUpdateScheduling): update_frequency must be at least 1.");
6191 }
6192
6193 geometry_update_frequency = update_frequency;
6194 geometry_update_counter = 0; // Reset counter
6195}
6196
6197// ----- Attraction Points Methods ----- //
6198
6199void PlantArchitecture::enableAttractionPoints(const std::vector<helios::vec3> &attraction_points_input, float view_half_angle_deg, float look_ahead_distance, float attraction_weight_input) {
6200 if (view_half_angle_deg <= 0.0f || view_half_angle_deg > 180.f) {
6201 helios_runtime_error("ERROR (PlantArchitecture::enableAttractionPoints): view_half_angle_deg must be between 0 and 180 degrees.");
6202 }
6203 if (look_ahead_distance <= 0.0f) {
6204 helios_runtime_error("ERROR (PlantArchitecture::enableAttractionPoints): look_ahead_distance must be positive.");
6205 }
6206 if (attraction_weight_input < 0.0f || attraction_weight_input > 1.0f) {
6207 helios_runtime_error("ERROR (PlantArchitecture::enableAttractionPoints): attraction_weight must be between 0.0 and 1.0.");
6208 }
6209
6210 // Set global attraction points for backward compatibility
6211 attraction_points_enabled = true;
6212 attraction_points = attraction_points_input;
6213 attraction_cone_half_angle_rad = deg2rad(view_half_angle_deg);
6214 attraction_cone_height = look_ahead_distance;
6215 attraction_weight = attraction_weight_input;
6216
6217 // Also apply to all existing plants for backward compatibility
6218 for (auto &[plantID, plant]: plant_instances) {
6219 plant.attraction_points_enabled = true;
6220 plant.attraction_points = attraction_points_input;
6221 plant.attraction_cone_half_angle_rad = deg2rad(view_half_angle_deg);
6222 plant.attraction_cone_height = look_ahead_distance;
6223 plant.attraction_weight = attraction_weight_input;
6224 }
6225}
6226
6228 // Disable global attraction points for backward compatibility
6229 attraction_points_enabled = false;
6230 attraction_points.clear();
6231
6232 // Also disable for all existing plants for backward compatibility
6233 for (auto &[plantID, plant]: plant_instances) {
6234 plant.attraction_points_enabled = false;
6235 plant.attraction_points.clear();
6236 }
6237}
6238
6239void PlantArchitecture::updateAttractionPoints(const std::vector<helios::vec3> &attraction_points_input) {
6240 if (!attraction_points_enabled) {
6241 helios_runtime_error("ERROR (PlantArchitecture::updateAttractionPoints): Attraction points must be enabled before updating positions.");
6242 }
6243 if (attraction_points_input.empty()) {
6244 helios_runtime_error("ERROR (PlantArchitecture::updateAttractionPoints): attraction_points cannot be empty.");
6245 }
6246
6247 // Update global attraction points for backward compatibility
6248 attraction_points = attraction_points_input;
6249
6250 // Also update for all existing plants for backward compatibility
6251 for (auto &[plantID, plant]: plant_instances) {
6252 if (plant.attraction_points_enabled) {
6253 plant.attraction_points = attraction_points_input;
6254 }
6255 }
6256}
6257
6258void PlantArchitecture::appendAttractionPoints(const std::vector<helios::vec3> &attraction_points_input) {
6259 if (!attraction_points_enabled) {
6260 helios_runtime_error("ERROR (PlantArchitecture::appendAttractionPoints): Attraction points must be enabled before updating positions.");
6261 }
6262 if (attraction_points_input.empty()) {
6263 helios_runtime_error("ERROR (PlantArchitecture::appendAttractionPoints): attraction_points cannot be empty.");
6264 }
6265
6266 // Append to global attraction points for backward compatibility
6267 attraction_points.insert(attraction_points.end(), attraction_points_input.begin(), attraction_points_input.end());
6268
6269 // Also append for all existing plants for backward compatibility
6270 for (auto &[plantID, plant]: plant_instances) {
6271 if (plant.attraction_points_enabled) {
6272 plant.attraction_points.insert(plant.attraction_points.end(), attraction_points_input.begin(), attraction_points_input.end());
6273 }
6274 }
6275}
6276
6277void PlantArchitecture::setAttractionParameters(float view_half_angle_deg, float look_ahead_distance, float attraction_weight_input, float obstacle_reduction_factor) {
6278 if (view_half_angle_deg <= 0.0f || view_half_angle_deg > 180.f) {
6279 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): view_half_angle_deg must be between 0 and 180 degrees.");
6280 }
6281 if (look_ahead_distance <= 0.0f) {
6282 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): look_ahead_distance must be positive.");
6283 }
6284 if (attraction_weight_input < 0.0f || attraction_weight_input > 1.0f) {
6285 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): attraction_weight must be between 0.0 and 1.0.");
6286 }
6287 if (obstacle_reduction_factor < 0.0f || obstacle_reduction_factor > 1.0f) {
6288 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): obstacle_reduction_factor must be between 0.0 and 1.0.");
6289 }
6290
6291 // Update global attraction parameters for backward compatibility
6292 attraction_cone_half_angle_rad = deg2rad(view_half_angle_deg);
6293 attraction_cone_height = look_ahead_distance;
6294 attraction_weight = attraction_weight_input;
6295 attraction_obstacle_reduction_factor = obstacle_reduction_factor;
6296
6297 // Also update for all existing plants for backward compatibility
6298 for (auto &[plantID, plant]: plant_instances) {
6299 if (plant.attraction_points_enabled) {
6300 plant.attraction_cone_half_angle_rad = deg2rad(view_half_angle_deg);
6301 plant.attraction_cone_height = look_ahead_distance;
6302 plant.attraction_weight = attraction_weight_input;
6303 plant.attraction_obstacle_reduction_factor = obstacle_reduction_factor;
6304 }
6305 }
6306
6307 if (printmessages) {
6308 std::cout << "Updated attraction parameters: cone_angle=" << view_half_angle_deg << "°, look_ahead=" << look_ahead_distance << "m, weight=" << attraction_weight_input << ", obstacle_reduction=" << obstacle_reduction_factor << std::endl;
6309 if (!plant_instances.empty()) {
6310 std::cout << "Applied to " << plant_instances.size() << " existing plants with attraction points enabled" << std::endl;
6311 }
6312 }
6313}
6314
6315// Plant-specific attraction point methods
6316
6317void PlantArchitecture::enableAttractionPoints(uint plantID, const std::vector<helios::vec3> &attraction_points_input, float view_half_angle_deg, float look_ahead_distance, float attraction_weight_input) {
6318 if (plant_instances.find(plantID) == plant_instances.end()) {
6319 helios_runtime_error("ERROR (PlantArchitecture::enableAttractionPoints): Plant with ID " + std::to_string(plantID) + " does not exist.");
6320 }
6321
6322 if (view_half_angle_deg <= 0.0f || view_half_angle_deg > 180.f) {
6323 helios_runtime_error("ERROR (PlantArchitecture::enableAttractionPoints): view_half_angle_deg must be between 0 and 180 degrees.");
6324 }
6325 if (look_ahead_distance <= 0.0f) {
6326 helios_runtime_error("ERROR (PlantArchitecture::enableAttractionPoints): look_ahead_distance must be greater than 0.");
6327 }
6328 if (attraction_points_input.empty()) {
6329 helios_runtime_error("ERROR (PlantArchitecture::enableAttractionPoints): attraction_points cannot be empty.");
6330 }
6331
6332 auto &plant = plant_instances.at(plantID);
6333 plant.attraction_points_enabled = true;
6334 plant.attraction_points = attraction_points_input;
6335 plant.attraction_cone_half_angle_rad = deg2rad(view_half_angle_deg);
6336 plant.attraction_cone_height = look_ahead_distance;
6337 plant.attraction_weight = attraction_weight_input;
6338
6339 if (printmessages) {
6340 std::cout << "Enabled attraction points for plant " << plantID << " with " << attraction_points_input.size() << " target positions" << std::endl;
6341 std::cout << "Plant " << plantID << " attraction parameters: cone_angle=" << view_half_angle_deg << "°, look_ahead=" << look_ahead_distance << "m, weight=" << attraction_weight_input << std::endl;
6342 }
6343}
6344
6346 if (plant_instances.find(plantID) == plant_instances.end()) {
6347 helios_runtime_error("ERROR (PlantArchitecture::disableAttractionPoints): Plant with ID " + std::to_string(plantID) + " does not exist.");
6348 }
6349
6350 auto &plant = plant_instances.at(plantID);
6351 plant.attraction_points_enabled = false;
6352 plant.attraction_points.clear();
6353
6354 if (printmessages) {
6355 std::cout << "Disabled attraction points for plant " << plantID << " - will use natural growth patterns" << std::endl;
6356 }
6357}
6358
6359void PlantArchitecture::updateAttractionPoints(uint plantID, const std::vector<helios::vec3> &attraction_points_input) {
6360 if (plant_instances.find(plantID) == plant_instances.end()) {
6361 helios_runtime_error("ERROR (PlantArchitecture::updateAttractionPoints): Plant with ID " + std::to_string(plantID) + " does not exist.");
6362 }
6363
6364 auto &plant = plant_instances.at(plantID);
6365 if (!plant.attraction_points_enabled) {
6366 helios_runtime_error("ERROR (PlantArchitecture::updateAttractionPoints): Attraction points must be enabled for plant " + std::to_string(plantID) + " before updating positions.");
6367 }
6368 if (attraction_points_input.empty()) {
6369 helios_runtime_error("ERROR (PlantArchitecture::updateAttractionPoints): attraction_points cannot be empty.");
6370 }
6371
6372 plant.attraction_points = attraction_points_input;
6373}
6374
6375void PlantArchitecture::appendAttractionPoints(uint plantID, const std::vector<helios::vec3> &attraction_points_input) {
6376 if (plant_instances.find(plantID) == plant_instances.end()) {
6377 helios_runtime_error("ERROR (PlantArchitecture::appendAttractionPoints): Plant with ID " + std::to_string(plantID) + " does not exist.");
6378 }
6379
6380 auto &plant = plant_instances.at(plantID);
6381 if (!plant.attraction_points_enabled) {
6382 helios_runtime_error("ERROR (PlantArchitecture::appendAttractionPoints): Attraction points must be enabled for plant " + std::to_string(plantID) + " before updating positions.");
6383 }
6384 if (attraction_points_input.empty()) {
6385 helios_runtime_error("ERROR (PlantArchitecture::appendAttractionPoints): attraction_points cannot be empty.");
6386 }
6387
6388 plant.attraction_points.insert(plant.attraction_points.end(), attraction_points_input.begin(), attraction_points_input.end());
6389}
6390
6391void PlantArchitecture::setAttractionParameters(uint plantID, float view_half_angle_deg, float look_ahead_distance, float attraction_weight_input, float obstacle_reduction_factor) {
6392 if (plant_instances.find(plantID) == plant_instances.end()) {
6393 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): Plant with ID " + std::to_string(plantID) + " does not exist.");
6394 }
6395
6396 if (view_half_angle_deg <= 0.0f || view_half_angle_deg > 180.f) {
6397 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): view_half_angle_deg must be between 0 and 180 degrees.");
6398 }
6399 if (look_ahead_distance <= 0.0f) {
6400 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): look_ahead_distance must be greater than 0.");
6401 }
6402 if (obstacle_reduction_factor < 0.0f || obstacle_reduction_factor > 1.0f) {
6403 helios_runtime_error("ERROR (PlantArchitecture::setAttractionParameters): obstacle_reduction_factor must be between 0 and 1.");
6404 }
6405
6406 auto &plant = plant_instances.at(plantID);
6407 plant.attraction_cone_half_angle_rad = deg2rad(view_half_angle_deg);
6408 plant.attraction_cone_height = look_ahead_distance;
6409 plant.attraction_weight = attraction_weight_input;
6410 plant.attraction_obstacle_reduction_factor = obstacle_reduction_factor;
6411
6412 if (printmessages) {
6413 std::cout << "Updated attraction parameters for plant " << plantID << ": cone_angle=" << view_half_angle_deg << "°, look_ahead=" << look_ahead_distance << "m, weight=" << attraction_weight_input
6414 << ", obstacle_reduction=" << obstacle_reduction_factor << std::endl;
6415 }
6416}
6417
6418void PlantArchitecture::setPlantAttractionPoints(uint plantID, const std::vector<helios::vec3> &attraction_points_input, float view_half_angle_deg, float look_ahead_distance, float attraction_weight_input, float obstacle_reduction_factor) {
6419 if (plant_instances.find(plantID) == plant_instances.end()) {
6420 helios_runtime_error("ERROR (PlantArchitecture::setPlantAttractionPoints): Plant with ID " + std::to_string(plantID) + " does not exist.");
6421 }
6422
6423 if (view_half_angle_deg <= 0.0f || view_half_angle_deg > 180.f) {
6424 helios_runtime_error("ERROR (PlantArchitecture::setPlantAttractionPoints): view_half_angle_deg must be between 0 and 180 degrees.");
6425 }
6426 if (look_ahead_distance <= 0.0f) {
6427 helios_runtime_error("ERROR (PlantArchitecture::setPlantAttractionPoints): look_ahead_distance must be greater than 0.");
6428 }
6429 if (attraction_points_input.empty()) {
6430 helios_runtime_error("ERROR (PlantArchitecture::setPlantAttractionPoints): attraction_points cannot be empty.");
6431 }
6432 if (obstacle_reduction_factor < 0.0f || obstacle_reduction_factor > 1.0f) {
6433 helios_runtime_error("ERROR (PlantArchitecture::setPlantAttractionPoints): obstacle_reduction_factor must be between 0 and 1.");
6434 }
6435
6436 auto &plant = plant_instances.at(plantID);
6437 plant.attraction_points_enabled = true;
6438 plant.attraction_points = attraction_points_input;
6439 plant.attraction_cone_half_angle_rad = deg2rad(view_half_angle_deg);
6440 plant.attraction_cone_height = look_ahead_distance;
6441 plant.attraction_weight = attraction_weight_input;
6442 plant.attraction_obstacle_reduction_factor = obstacle_reduction_factor;
6443}
6444
6446 printmessages = false;
6447 if (collision_detection_ptr != nullptr) {
6448 collision_detection_ptr->disableMessages();
6449 }
6450}
6451
6453 printmessages = true;
6454 if (collision_detection_ptr != nullptr) {
6455 collision_detection_ptr->enableMessages();
6456 }
6457}