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