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