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