1.3.64
 
Loading...
Searching...
No Matches
InputOutput.cpp
Go to the documentation of this file.
1
16#include "PlantArchitecture.h"
17
18using namespace helios;
19
20// Helper function for calculating leaf base positions in compound leaves
21static float clampOffset(int count_per_axis, float offset) {
22 if (count_per_axis > 2) {
23 float denom = 0.5f * float(count_per_axis) - 1.f;
24 if (offset * denom > 1.f) {
25 offset = 1.f / denom;
26 }
27 }
28 return offset;
29}
30
31std::string PlantArchitecture::makeShootString(const std::string &current_string, const std::shared_ptr<Shoot> &shoot, const std::vector<std::shared_ptr<Shoot>> &shoot_tree) const {
32
33 std::string outstring = current_string;
34
35 if (shoot->parent_shoot_ID != -1) {
36 outstring += "[";
37 }
38
39 outstring += "{" + std::to_string(rad2deg(shoot->base_rotation.pitch)) + "," + std::to_string(rad2deg(shoot->base_rotation.yaw)) + "," + std::to_string(rad2deg(shoot->base_rotation.roll)) + "," +
40 std::to_string(shoot->shoot_parameters.gravitropic_curvature.val() /*\todo */) + "," + shoot->shoot_type_label + "}";
41
42 uint node_number = 0;
43 for (auto &phytomer: shoot->phytomers) {
44
45 float length = phytomer->getInternodeLength();
46 float radius = phytomer->getInternodeRadius();
47
48 outstring += "Internode(" + std::to_string(length) + "," + std::to_string(radius) + "," + std::to_string(rad2deg(phytomer->internode_pitch)) + "," + std::to_string(rad2deg(phytomer->internode_phyllotactic_angle)) + ")";
49
50 for (uint petiole = 0; petiole < phytomer->petiole_length.size(); petiole++) {
51
52 outstring += "Petiole(" + std::to_string(phytomer->petiole_length.at(petiole)) + "," + std::to_string(phytomer->petiole_radii.at(petiole).front()) + "," + std::to_string(rad2deg(phytomer->petiole_pitch.at(petiole))) + ")";
53
54 //\todo If leaf is compound, just using rotation for the first leaf for now rather than adding multiple 'Leaf()' strings for each leaflet.
55 outstring += "Leaf(" + std::to_string(phytomer->leaf_size_max.at(petiole).front() * phytomer->current_leaf_scale_factor.at(petiole)) + "," + std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().pitch)) + "," +
56 std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().yaw)) + "," + std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().roll)) + ")";
57
58 if (shoot->childIDs.find(node_number) != shoot->childIDs.end()) {
59 for (int childID: shoot->childIDs.at(node_number)) {
60 outstring = makeShootString(outstring, shoot_tree.at(childID), shoot_tree);
61 }
62 }
63 }
64
65 node_number++;
66 }
67
68 if (shoot->parent_shoot_ID != -1) {
69 outstring += "]";
70 }
71
72 return outstring;
73}
74
75std::string PlantArchitecture::getPlantString(uint plantID) const {
76
77 auto plant_shoot_tree = &plant_instances.at(plantID).shoot_tree;
78
79 std::string out_string;
80
81 for (auto &shoot: *plant_shoot_tree) {
82 out_string = makeShootString(out_string, shoot, *plant_shoot_tree);
83 }
84
85 return out_string;
86}
87
88void PlantArchitecture::parseShootArgument(const std::string &shoot_argument, const std::map<std::string, PhytomerParameters> &phytomer_parameters, ShootParameters &shoot_parameters, AxisRotation &base_rotation, std::string &phytomer_label) {
89
90 // shoot argument order {}:
91 // 1. shoot base pitch/insertion angle (degrees)
92 // 2. shoot base yaw angle (degrees)
93 // 3. shoot roll angle (degrees)
94 // 4. gravitropic curvature (degrees/meter)
95 // 5. [optional] phytomer parameters string
96
97 size_t pos_shoot_start = 0;
98
99 std::string s_argument = shoot_argument;
100
101 pos_shoot_start = s_argument.find(',');
102 if (pos_shoot_start == std::string::npos) {
103 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
104 }
105 float insertion_angle = std::stof(s_argument.substr(0, pos_shoot_start));
106 s_argument.erase(0, pos_shoot_start + 1);
107 base_rotation.pitch = deg2rad(insertion_angle);
108
109 pos_shoot_start = s_argument.find(',');
110 if (pos_shoot_start == std::string::npos) {
111 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
112 }
113 float shoot_yaw = std::stof(s_argument.substr(0, pos_shoot_start));
114 s_argument.erase(0, pos_shoot_start + 1);
115 base_rotation.yaw = deg2rad(shoot_yaw);
116
117 pos_shoot_start = s_argument.find(',');
118 if (pos_shoot_start == std::string::npos) {
119 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
120 }
121 float shoot_roll = std::stof(s_argument.substr(0, pos_shoot_start));
122 s_argument.erase(0, pos_shoot_start + 1);
123 base_rotation.roll = deg2rad(shoot_roll);
124
125 pos_shoot_start = s_argument.find(',');
126 if (pos_shoot_start == std::string::npos) {
127 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
128 }
129 float shoot_curvature = std::stof(s_argument.substr(0, pos_shoot_start));
130 s_argument.erase(0, pos_shoot_start + 1);
131 shoot_parameters.gravitropic_curvature = shoot_curvature;
132
133 if (pos_shoot_start != std::string::npos) { // shoot type argument was given
134 pos_shoot_start = s_argument.find(',');
135 phytomer_label = s_argument.substr(0, pos_shoot_start);
136 s_argument.erase(0, pos_shoot_start + 1);
137 if (phytomer_parameters.find(phytomer_label) == phytomer_parameters.end()) {
138 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Phytomer parameters with label " + phytomer_label + " was not provided to PlantArchitecture::generatePlantFromString().");
139 }
140 shoot_parameters.phytomer_parameters = phytomer_parameters.at(phytomer_label);
141 } else { // shoot type argument not given - use first phytomer parameters in the map
142 phytomer_label = phytomer_parameters.begin()->first;
143 shoot_parameters.phytomer_parameters = phytomer_parameters.begin()->second;
144 }
145}
146
147void PlantArchitecture::parseInternodeArgument(const std::string &internode_argument, float &internode_radius, float &internode_length, PhytomerParameters &phytomer_parameters) {
148
149 // internode argument order Internode():
150 // 1. internode length (m)
151 // 2. internode radius (m)
152 // 3. internode pitch (degrees)
153 // 4. phyllotactic angle (degrees)
154
155 size_t pos_inode_start = 0;
156
157 std::string inode_argument = internode_argument;
158
159 pos_inode_start = inode_argument.find(',');
160 if (pos_inode_start == std::string::npos) {
161 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
162 }
163 internode_length = std::stof(inode_argument.substr(0, pos_inode_start));
164 inode_argument.erase(0, pos_inode_start + 1);
165
166 pos_inode_start = inode_argument.find(',');
167 if (pos_inode_start == std::string::npos) {
168 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
169 }
170 internode_radius = std::stof(inode_argument.substr(0, pos_inode_start));
171 inode_argument.erase(0, pos_inode_start + 1);
172
173 pos_inode_start = inode_argument.find(',');
174 if (pos_inode_start == std::string::npos) {
175 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
176 }
177 float internode_pitch = std::stof(inode_argument.substr(0, pos_inode_start));
178 inode_argument.erase(0, pos_inode_start + 1);
179 phytomer_parameters.internode.pitch = internode_pitch;
180
181 pos_inode_start = inode_argument.find(',');
182 if (pos_inode_start != std::string::npos) {
183 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
184 }
185 float internode_phyllotaxis = std::stof(inode_argument.substr(0, pos_inode_start));
186 inode_argument.erase(0, pos_inode_start + 1);
187 phytomer_parameters.internode.phyllotactic_angle = internode_phyllotaxis;
188}
189
190void PlantArchitecture::parsePetioleArgument(const std::string &petiole_argument, PhytomerParameters &phytomer_parameters) {
191
192 // petiole argument order Petiole():
193 // 1. petiole length (m)
194 // 2. petiole radius (m)
195 // 3. petiole pitch (degrees)
196
197 if (petiole_argument.empty()) {
198 phytomer_parameters.petiole.length = 0;
199 return;
200 }
201
202 size_t pos_petiole_start = 0;
203
204 std::string pet_argument = petiole_argument;
205
206 pos_petiole_start = pet_argument.find(',');
207 if (pos_petiole_start == std::string::npos) {
208 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
209 }
210 float petiole_length = std::stof(pet_argument.substr(0, pos_petiole_start));
211 pet_argument.erase(0, pos_petiole_start + 1);
212 phytomer_parameters.petiole.length = petiole_length;
213
214 pos_petiole_start = pet_argument.find(',');
215 if (pos_petiole_start == std::string::npos) {
216 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
217 }
218 float petiole_radius = std::stof(pet_argument.substr(0, pos_petiole_start));
219 pet_argument.erase(0, pos_petiole_start + 1);
220 phytomer_parameters.petiole.radius = petiole_radius;
221
222 pos_petiole_start = pet_argument.find(',');
223 if (pos_petiole_start != std::string::npos) {
224 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
225 }
226 float petiole_pitch = std::stof(pet_argument.substr(0, pos_petiole_start));
227 pet_argument.erase(0, pos_petiole_start + 1);
228 phytomer_parameters.petiole.pitch = petiole_pitch;
229}
230
231void PlantArchitecture::parseLeafArgument(const std::string &leaf_argument, PhytomerParameters &phytomer_parameters) {
232
233 // leaf argument order Leaf():
234 // 1. leaf scale factor
235 // 2. leaf pitch (degrees)
236 // 3. leaf yaw (degrees)
237 // 4. leaf roll (degrees)
238
239 if (leaf_argument.empty()) {
240 phytomer_parameters.leaf.prototype_scale = 0;
241 return;
242 }
243
244 size_t pos_leaf_start = 0;
245
246 std::string l_argument = leaf_argument;
247
248 pos_leaf_start = l_argument.find(',');
249 if (pos_leaf_start == std::string::npos) {
250 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
251 }
252 float leaf_scale = std::stof(l_argument.substr(0, pos_leaf_start));
253 l_argument.erase(0, pos_leaf_start + 1);
254 phytomer_parameters.leaf.prototype_scale = leaf_scale;
255
256 pos_leaf_start = l_argument.find(',');
257 if (pos_leaf_start == std::string::npos) {
258 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
259 }
260 float leaf_pitch = std::stof(l_argument.substr(0, pos_leaf_start));
261 l_argument.erase(0, pos_leaf_start + 1);
262 phytomer_parameters.leaf.pitch = leaf_pitch;
263
264 pos_leaf_start = l_argument.find(',');
265 if (pos_leaf_start == std::string::npos) {
266 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
267 }
268 float leaf_yaw = std::stof(l_argument.substr(0, pos_leaf_start));
269 l_argument.erase(0, pos_leaf_start + 1);
270 phytomer_parameters.leaf.yaw = leaf_yaw;
271
272 pos_leaf_start = l_argument.find(',');
273 if (pos_leaf_start != std::string::npos) {
274 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
275 }
276 float leaf_roll = std::stof(l_argument.substr(0, pos_leaf_start));
277 l_argument.erase(0, pos_leaf_start + 1);
278 phytomer_parameters.leaf.roll = leaf_roll;
279}
280
281size_t findShootClosingBracket(const std::string &lstring) {
282
283 size_t pos_close = std::string::npos;
284 size_t pos_open = lstring.find_first_of('[', 0);
285 if (pos_open == std::string::npos) {
286 return pos_close;
287 }
288
289 size_t pos = pos_open;
290 int count = 1;
291 while (count > 0) {
292 pos++;
293 if (lstring[pos] == '[') {
294 count++;
295 } else if (lstring[pos] == ']') {
296 count--;
297 }
298 if (pos == lstring.length()) {
299 return pos_close;
300 }
301 }
302 if (count == 0) {
303 pos_close = pos;
304 }
305 return pos_close; // Return the position of the closing bracket
306}
307
308void PlantArchitecture::parseStringShoot(const std::string &LString_shoot, uint plantID, int parentID, uint parent_node, const std::map<std::string, PhytomerParameters> &phytomer_parameters, ShootParameters &shoot_parameters) {
309
310 std::string lstring_tobeparsed = LString_shoot;
311
312 size_t pos_inode_start = 0;
313 std::string inode_delimiter = "Internode(";
314 std::string petiole_delimiter = "Petiole(";
315 std::string leaf_delimiter = "Leaf(";
316 bool base_shoot = true;
317 uint baseID;
318 AxisRotation shoot_base_rotation;
319
320 // parse shoot arguments
321 if (LString_shoot.front() != '{') {
322 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly. All shoots should start with a curly bracket containing two arguments {X,Y}.");
323 }
324 size_t pos_shoot_end = lstring_tobeparsed.find('}');
325 std::string shoot_argument = lstring_tobeparsed.substr(1, pos_shoot_end - 1);
326 std::string phytomer_label;
327 parseShootArgument(shoot_argument, phytomer_parameters, shoot_parameters, shoot_base_rotation, phytomer_label);
328 lstring_tobeparsed.erase(0, pos_shoot_end + 1);
329
330 uint shoot_node_count = 0;
331 while ((pos_inode_start = lstring_tobeparsed.find(inode_delimiter)) != std::string::npos) {
332
333 if (pos_inode_start != 0) {
334 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly.");
335 }
336
337 size_t pos_inode_end = lstring_tobeparsed.find(')');
338 std::string inode_argument = lstring_tobeparsed.substr(pos_inode_start + inode_delimiter.length(), pos_inode_end - pos_inode_start - inode_delimiter.length());
339 float internode_radius = 0;
340 float internode_length = 0;
341 parseInternodeArgument(inode_argument, internode_radius, internode_length, shoot_parameters.phytomer_parameters);
342 lstring_tobeparsed.erase(0, pos_inode_end + 1);
343
344 size_t pos_petiole_start = lstring_tobeparsed.find(petiole_delimiter);
345 size_t pos_petiole_end = lstring_tobeparsed.find(')');
346 std::string petiole_argument;
347 if (pos_petiole_start == 0) {
348 petiole_argument = lstring_tobeparsed.substr(pos_petiole_start + petiole_delimiter.length(), pos_petiole_end - pos_petiole_start - petiole_delimiter.length());
349 } else {
350 petiole_argument = "";
351 }
352 parsePetioleArgument(petiole_argument, shoot_parameters.phytomer_parameters);
353 if (pos_petiole_start == 0) {
354 lstring_tobeparsed.erase(0, pos_petiole_end + 1);
355 }
356
357 size_t pos_leaf_start = lstring_tobeparsed.find(leaf_delimiter);
358 size_t pos_leaf_end = lstring_tobeparsed.find(')');
359 std::string leaf_argument;
360 if (pos_leaf_start == 0) {
361 leaf_argument = lstring_tobeparsed.substr(pos_leaf_start + leaf_delimiter.length(), pos_leaf_end - pos_leaf_start - leaf_delimiter.length());
362 } else {
363 leaf_argument = "";
364 }
365 parseLeafArgument(leaf_argument, shoot_parameters.phytomer_parameters);
366 if (pos_leaf_start == 0) {
367 lstring_tobeparsed.erase(0, pos_leaf_end + 1);
368 }
369
370 // override phytomer creation function
371 shoot_parameters.phytomer_parameters.phytomer_creation_function = nullptr;
372
373 if (base_shoot) { // this is the first phytomer of the shoot
374 // defineShootType("shoot_"+phytomer_label, shoot_parameters);
375 defineShootType(phytomer_label, shoot_parameters); //*testing*
376
377 if (parentID < 0) { // this is the first shoot of the plant
378 // baseID = addBaseStemShoot(plantID, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, "shoot_" + phytomer_label);
379 baseID = addBaseStemShoot(plantID, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, phytomer_label); //*testing*
380 } else { // this is a child of an existing shoot
381 // baseID = addChildShoot(plantID, parentID, parent_node, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, "shoot_" + phytomer_label, 0);
382 baseID = addChildShoot(plantID, parentID, parent_node, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, phytomer_label, 0); //*testing*
383 }
384
385 base_shoot = false;
386 } else {
387 appendPhytomerToShoot(plantID, baseID, shoot_parameters.phytomer_parameters, internode_radius, internode_length, 1, 1);
388 }
389
390 while (!lstring_tobeparsed.empty() && lstring_tobeparsed.substr(0, 1) == "[") {
391 size_t pos_shoot_bracket_end = findShootClosingBracket(lstring_tobeparsed);
392 if (pos_shoot_bracket_end == std::string::npos) {
393 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly. Shoots must be closed with a ']'.");
394 }
395 std::string lstring_child = lstring_tobeparsed.substr(1, pos_shoot_bracket_end - 1);
396 parseStringShoot(lstring_child, plantID, (int) baseID, shoot_node_count, phytomer_parameters, shoot_parameters);
397 lstring_tobeparsed.erase(0, pos_shoot_bracket_end + 1);
398 }
399
400 shoot_node_count++;
401 }
402}
403
404uint PlantArchitecture::generatePlantFromString(const std::string &generation_string, const PhytomerParameters &phytomer_parameters) {
405 std::map<std::string, PhytomerParameters> phytomer_parameters_map;
406 phytomer_parameters_map["default"] = phytomer_parameters;
407 return generatePlantFromString(generation_string, phytomer_parameters_map);
408}
409
410uint PlantArchitecture::generatePlantFromString(const std::string &generation_string, const std::map<std::string, PhytomerParameters> &phytomer_parameters) {
411
412 // check that first characters are 'Internode'
413 if (generation_string.front() != '{') {
414 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): First character of string must be '{'");
415 }
416
417 ShootParameters shoot_parameters(context_ptr->getRandomGenerator());
418 shoot_parameters.max_nodes = 200;
419 shoot_parameters.vegetative_bud_break_probability_min = 0;
420 shoot_parameters.vegetative_bud_break_time = 0;
421 shoot_parameters.phyllochron_min = 0;
422
423 // assign default phytomer parameters. This can be changed later if the optional phytomer parameters label is provided in the shoot argument '{}'.
424 if (phytomer_parameters.empty()) {
425 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Phytomer parameters must be provided.");
426 }
427
428 uint plantID;
429
430 plantID = addPlantInstance(nullorigin, 0);
431
432 size_t pos_first_child_shoot = generation_string.find('[');
433
434 if (pos_first_child_shoot == std::string::npos) {
435 pos_first_child_shoot = generation_string.length();
436 }
437
438 parseStringShoot(generation_string, plantID, -1, 0, phytomer_parameters, shoot_parameters);
439
440
441 return plantID;
442}
443
444void PlantArchitecture::writePlantStructureXML(uint plantID, const std::string &filename) const {
445
446 if (plant_instances.find(plantID) == plant_instances.end()) {
447 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Plant ID " + std::to_string(plantID) + " does not exist.");
448 }
449
450 std::string output_file = filename;
451 if (!validateOutputPath(output_file, {".xml", ".XML"})) {
452 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
453 } else if (getFileName(output_file).empty()) {
454 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): The output file given was a directory. This argument should be the path to a file not to a directory.");
455 }
456
457 std::ofstream output_xml(filename);
458
459 if (!output_xml.is_open()) {
460 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
461 }
462
463 output_xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
464 output_xml << "<helios>" << std::endl;
465 output_xml << "\t<plant_instance ID=\"" << plantID << "\">" << std::endl;
466
467 output_xml << "\t\t<base_position> " << plant_instances.at(plantID).base_position.x << " " << plant_instances.at(plantID).base_position.y << " " << plant_instances.at(plantID).base_position.z << " </base_position>" << std::endl;
468 output_xml << "\t\t<plant_age> " << plant_instances.at(plantID).current_age << " </plant_age>" << std::endl;
469
470 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
471
472 output_xml << "\t\t<shoot ID=\"" << shoot->ID << "\">" << std::endl;
473 output_xml << "\t\t\t<shoot_type_label> " << shoot->shoot_type_label << " </shoot_type_label>" << std::endl;
474 output_xml << "\t\t\t<parent_shoot_ID> " << shoot->parent_shoot_ID << " </parent_shoot_ID>" << std::endl;
475 output_xml << "\t\t\t<parent_node_index> " << shoot->parent_node_index << " </parent_node_index>" << std::endl;
476 output_xml << "\t\t\t<parent_petiole_index> " << shoot->parent_petiole_index << " </parent_petiole_index>" << std::endl;
477 output_xml << "\t\t\t<base_rotation> " << rad2deg(shoot->base_rotation.pitch) << " " << rad2deg(shoot->base_rotation.yaw) << " " << rad2deg(shoot->base_rotation.roll) << " </base_rotation>" << std::endl;
478
479 uint phytomer_index = 0;
480 for (auto &phytomer: shoot->phytomers) {
481
482 output_xml << "\t\t\t<phytomer>" << std::endl;
483 output_xml << "\t\t\t\t<internode>" << std::endl;
484 output_xml << "\t\t\t\t\t<internode_length>" << phytomer->getInternodeLength() << "</internode_length>" << std::endl;
485 output_xml << "\t\t\t\t\t<internode_radius>" << phytomer->getInternodeRadius() << "</internode_radius>" << std::endl;
486 output_xml << "\t\t\t\t\t<internode_pitch>" << rad2deg(phytomer->internode_pitch) << "</internode_pitch>" << std::endl;
487 output_xml << "\t\t\t\t\t<internode_phyllotactic_angle>" << rad2deg(phytomer->internode_phyllotactic_angle) << "</internode_phyllotactic_angle>" << std::endl;
488
489 // Additional parameters for reconstruction from parameters
490 output_xml << "\t\t\t\t\t<internode_length_max>" << phytomer->internode_length_max << "</internode_length_max>" << std::endl;
491
492 // Length segments should match perturbation count
493 uint length_segments = phytomer->internode_curvature_perturbations.size();
494 if (length_segments > 0) {
495 output_xml << "\t\t\t\t\t<internode_length_segments>" << length_segments << "</internode_length_segments>" << std::endl;
496 } else if (phytomer_index < shoot->shoot_internode_vertices.size()) {
497 // Fallback: infer from vertex count (for phytomers with no perturbations)
498 if (phytomer_index == 0) {
499 length_segments = shoot->shoot_internode_vertices[phytomer_index].size() - 1;
500 } else {
501 length_segments = shoot->shoot_internode_vertices[phytomer_index].size();
502 }
503 output_xml << "\t\t\t\t\t<internode_length_segments>" << length_segments << "</internode_length_segments>" << std::endl;
504 }
505
506 // Radial subdivisions - use default from Context geometry (stored during tube creation)
507 // For now, we'll read this from the tube object during reconstruction
508
509 // Save curvature perturbations (semicolon-delimited)
510 if (!phytomer->internode_curvature_perturbations.empty()) {
511 output_xml << "\t\t\t\t\t<curvature_perturbations>";
512 for (size_t i = 0; i < phytomer->internode_curvature_perturbations.size(); i++) {
513 output_xml << phytomer->internode_curvature_perturbations[i];
514 if (i < phytomer->internode_curvature_perturbations.size() - 1)
515 output_xml << ";";
516 }
517 output_xml << "</curvature_perturbations>" << std::endl;
518 }
519
520 // Save yaw perturbations (semicolon-delimited)
521 if (!phytomer->internode_yaw_perturbations.empty()) {
522 output_xml << "\t\t\t\t\t<yaw_perturbations>";
523 for (size_t i = 0; i < phytomer->internode_yaw_perturbations.size(); i++) {
524 output_xml << phytomer->internode_yaw_perturbations[i];
525 if (i < phytomer->internode_yaw_perturbations.size() - 1)
526 output_xml << ";";
527 }
528 output_xml << "</yaw_perturbations>" << std::endl;
529 }
530
531 // Note: internode_vertices and internode_radii are no longer written to XML
532 // Internodes are now reconstructed from parameters (length, pitch, phyllotactic_angle, length_max, segments)
533 // and stochastic state (curvature_perturbations, yaw_perturbations) during XML reading
534
535 // Removed in Phase 3 - internodes now reconstructed from parameters:
536 // if (phytomer_index < shoot->shoot_internode_vertices.size()) {
537 // const auto &vertices = shoot->shoot_internode_vertices[phytomer_index];
538 // output_xml << "\t\t\t\t\t<internode_vertices>";
539 // for (size_t v = 0; v < vertices.size(); v++) {
540 // output_xml << vertices[v].x << " " << vertices[v].y << " " << vertices[v].z;
541 // if (v < vertices.size() - 1) output_xml << ";";
542 // }
543 // output_xml << "</internode_vertices>" << std::endl;
544 // }
545 // if (phytomer_index < shoot->shoot_internode_radii.size()) {
546 // const auto &radii = shoot->shoot_internode_radii[phytomer_index];
547 // output_xml << "\t\t\t\t\t<internode_radii>";
548 // for (size_t r = 0; r < radii.size(); r++) {
549 // output_xml << radii[r];
550 // if (r < radii.size() - 1) output_xml << ";";
551 // }
552 // output_xml << "</internode_radii>" << std::endl;
553 // }
554
555 for (uint petiole = 0; petiole < phytomer->petiole_length.size(); petiole++) {
556
557 output_xml << "\t\t\t\t\t<petiole>" << std::endl;
558 output_xml << "\t\t\t\t\t\t<petiole_length>" << phytomer->petiole_length.at(petiole) << "</petiole_length>" << std::endl;
559 output_xml << "\t\t\t\t\t\t<petiole_radius>" << phytomer->petiole_radii.at(petiole).front() << "</petiole_radius>" << std::endl;
560 output_xml << "\t\t\t\t\t\t<petiole_pitch>" << rad2deg(phytomer->petiole_pitch.at(petiole)) << "</petiole_pitch>" << std::endl;
561 output_xml << "\t\t\t\t\t\t<petiole_curvature>" << phytomer->petiole_curvature.at(petiole) << "</petiole_curvature>" << std::endl;
562 // Note: petiole_base_position is no longer written to XML - it is auto-calculated from the parent internode tip during XML reading
563 output_xml << "\t\t\t\t\t\t<current_leaf_scale_factor>" << phytomer->current_leaf_scale_factor.at(petiole) << "</current_leaf_scale_factor>" << std::endl;
564
565 // Bulk parameters for exact petiole reconstruction
566 output_xml << "\t\t\t\t\t\t<petiole_taper>" << phytomer->petiole_taper.at(petiole) << "</petiole_taper>" << std::endl;
567 output_xml << "\t\t\t\t\t\t<petiole_length_segments>" << phytomer->phytomer_parameters.petiole.length_segments << "</petiole_length_segments>" << std::endl;
568 output_xml << "\t\t\t\t\t\t<petiole_radial_subdivisions>" << phytomer->phytomer_parameters.petiole.radial_subdivisions << "</petiole_radial_subdivisions>" << std::endl;
569
570 if (phytomer->leaf_rotation.at(petiole).size() == 1) { // not compound leaf
571 output_xml << "\t\t\t\t\t\t<leaflet_scale>" << 1.0 << "</leaflet_scale>" << std::endl;
572 } else {
573 float tip_ind = floor(float(phytomer->leaf_rotation.at(petiole).size() - 1) / 2.f);
574 output_xml << "\t\t\t\t\t\t<leaflet_scale>" << phytomer->leaf_size_max.at(petiole).at(int(tip_ind - 1)) / max(phytomer->leaf_size_max.at(petiole)) << "</leaflet_scale>" << std::endl;
575 }
576 output_xml << "\t\t\t\t\t\t<leaflet_offset>" << phytomer->phytomer_parameters.leaf.leaflet_offset.val() << "</leaflet_offset>" << std::endl;
577
578 for (uint leaf = 0; leaf < phytomer->leaf_rotation.at(petiole).size(); leaf++) {
579 output_xml << "\t\t\t\t\t\t<leaf>" << std::endl;
580 output_xml << "\t\t\t\t\t\t\t<leaf_scale>" << phytomer->leaf_size_max.at(petiole).at(leaf) * phytomer->current_leaf_scale_factor.at(petiole) << "</leaf_scale>" << std::endl;
581 // Note: leaf_base is no longer written to XML - it is auto-calculated from the petiole geometry and leaflet_offset during XML reading
582 // output_xml << "\t\t\t\t\t\t\t<leaf_base>" << phytomer->leaf_bases.at(petiole).at(leaf).x << " " << phytomer->leaf_bases.at(petiole).at(leaf).y << " " << phytomer->leaf_bases.at(petiole).at(leaf).z << "</leaf_base>" <<
583 // std::endl;
584 output_xml << "\t\t\t\t\t\t\t<leaf_pitch>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).pitch) << "</leaf_pitch>" << std::endl;
585 output_xml << "\t\t\t\t\t\t\t<leaf_yaw>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).yaw) << "</leaf_yaw>" << std::endl;
586 output_xml << "\t\t\t\t\t\t\t<leaf_roll>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).roll) << "</leaf_roll>" << std::endl;
587 output_xml << "\t\t\t\t\t\t</leaf>" << std::endl;
588 }
589
590 // Write floral buds
591 if (petiole < phytomer->floral_buds.size()) {
592 for (uint bud = 0; bud < phytomer->floral_buds.at(petiole).size(); bud++) {
593 const FloralBud &fbud = phytomer->floral_buds.at(petiole).at(bud);
594
595 output_xml << "\t\t\t\t\t\t<floral_bud>" << std::endl;
596
597 // Write bud state and indices
598 output_xml << "\t\t\t\t\t\t\t<bud_state>" << static_cast<int>(fbud.state) << "</bud_state>" << std::endl;
599 output_xml << "\t\t\t\t\t\t\t<parent_index>" << fbud.parent_index << "</parent_index>" << std::endl;
600 output_xml << "\t\t\t\t\t\t\t<bud_index>" << fbud.bud_index << "</bud_index>" << std::endl;
601 output_xml << "\t\t\t\t\t\t\t<is_terminal>" << (fbud.isterminal ? 1 : 0) << "</is_terminal>" << std::endl;
602
603 // Write fruit scale factor
604 output_xml << "\t\t\t\t\t\t\t<current_fruit_scale_factor>" << fbud.current_fruit_scale_factor << "</current_fruit_scale_factor>" << std::endl;
605
606 // Write peduncle parameters
607 // Note: Write the STORED VALUES that were actually used for this peduncle geometry
608 output_xml << "\t\t\t\t\t\t\t<peduncle>" << std::endl;
609 if (petiole < phytomer->peduncle_length.size() && bud < phytomer->peduncle_length.at(petiole).size()) {
610 output_xml << "\t\t\t\t\t\t\t\t<length>" << phytomer->peduncle_length.at(petiole).at(bud) << "</length>" << std::endl;
611 output_xml << "\t\t\t\t\t\t\t\t<radius>" << phytomer->peduncle_radius.at(petiole).at(bud) << "</radius>" << std::endl;
612 output_xml << "\t\t\t\t\t\t\t\t<pitch>" << phytomer->peduncle_pitch.at(petiole).at(bud) << "</pitch>" << std::endl;
613 output_xml << "\t\t\t\t\t\t\t\t<curvature>" << phytomer->peduncle_curvature.at(petiole).at(bud) << "</curvature>" << std::endl;
614 } else {
615 // Fallback to parameter values if stored values not available
616 output_xml << "\t\t\t\t\t\t\t\t<length>" << phytomer->phytomer_parameters.peduncle.length.val() << "</length>" << std::endl;
617 output_xml << "\t\t\t\t\t\t\t\t<radius>" << phytomer->phytomer_parameters.peduncle.radius.val() << "</radius>" << std::endl;
618 output_xml << "\t\t\t\t\t\t\t\t<pitch>" << phytomer->phytomer_parameters.peduncle.pitch.val() << "</pitch>" << std::endl;
619 output_xml << "\t\t\t\t\t\t\t\t<curvature>" << phytomer->phytomer_parameters.peduncle.curvature.val() << "</curvature>" << std::endl;
620 }
621 output_xml << "\t\t\t\t\t\t\t\t<roll>" << phytomer->phytomer_parameters.peduncle.roll.val() << "</roll>" << std::endl;
622
623 output_xml << "\t\t\t\t\t\t\t</peduncle>" << std::endl;
624
625 // Write inflorescence parameters
626 output_xml << "\t\t\t\t\t\t\t<inflorescence>" << std::endl;
627 output_xml << "\t\t\t\t\t\t\t\t<flower_offset>" << phytomer->phytomer_parameters.inflorescence.flower_offset.val() << "</flower_offset>" << std::endl;
628
629 // Write individual flower/fruit positions and rotations
630 for (uint i = 0; i < fbud.inflorescence_bases.size(); i++) {
631 output_xml << "\t\t\t\t\t\t\t\t<flower>" << std::endl;
632 // inflorescence_base is now auto-computed during XML reading - not saved to XML
633 // Save pitch, yaw, roll, and azimuth for this flower/fruit
634 if (i < fbud.inflorescence_rotation.size()) {
635 output_xml << "\t\t\t\t\t\t\t\t\t<flower_pitch>" << rad2deg(fbud.inflorescence_rotation.at(i).pitch) << "</flower_pitch>" << std::endl;
636 output_xml << "\t\t\t\t\t\t\t\t\t<flower_yaw>" << rad2deg(fbud.inflorescence_rotation.at(i).yaw) << "</flower_yaw>" << std::endl;
637 output_xml << "\t\t\t\t\t\t\t\t\t<flower_roll>" << rad2deg(fbud.inflorescence_rotation.at(i).roll) << "</flower_roll>" << std::endl;
638 output_xml << "\t\t\t\t\t\t\t\t\t<flower_azimuth>" << rad2deg(fbud.inflorescence_rotation.at(i).azimuth) << "</flower_azimuth>" << std::endl;
639 }
640 // Save individual base scale for this flower/fruit
641 if (i < fbud.inflorescence_base_scales.size()) {
642 output_xml << "\t\t\t\t\t\t\t\t\t<flower_base_scale>" << fbud.inflorescence_base_scales.at(i) << "</flower_base_scale>" << std::endl;
643 }
644 output_xml << "\t\t\t\t\t\t\t\t</flower>" << std::endl;
645 }
646
647 output_xml << "\t\t\t\t\t\t\t</inflorescence>" << std::endl;
648 output_xml << "\t\t\t\t\t\t</floral_bud>" << std::endl;
649 }
650 }
651
652 output_xml << "\t\t\t\t\t</petiole>" << std::endl;
653 }
654 output_xml << "\t\t\t\t</internode>" << std::endl;
655 output_xml << "\t\t\t</phytomer>" << std::endl;
656
657 phytomer_index++;
658 }
659 output_xml << "\t\t</shoot>" << std::endl;
660 }
661 output_xml << "\t</plant_instance>" << std::endl;
662 output_xml << "</helios>" << std::endl;
663 output_xml.close();
664}
665
666void PlantArchitecture::writeQSMCylinderFile(uint plantID, const std::string &filename) const {
667
668 if (plant_instances.find(plantID) == plant_instances.end()) {
669 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Plant ID " + std::to_string(plantID) + " does not exist.");
670 }
671
672 std::string output_file = filename;
673 if (!validateOutputPath(output_file, {".txt", ".TXT"})) {
674 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
675 } else if (getFileName(output_file).empty()) {
676 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): The output file given was a directory. This argument should be the path to a file not to a directory.");
677 }
678
679 std::ofstream output_qsm(filename);
680
681 if (!output_qsm.is_open()) {
682 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
683 }
684
685 // Write header line
686 output_qsm << "radius (m)\tlength (m)\tstart_point\taxis_direction\tparent\textension\tbranch\tbranch_order\tposition_in_branch\tmad\tSurfCov\tadded\tUnmodRadius (m)" << std::endl;
687
688 const auto &plant = plant_instances.at(plantID);
689
690 // Cylinder ID counter (row number = cylinder ID in TreeQSM format)
691 uint cylinder_id = 1;
692
693 // Maps to track relationships between shoots and cylinders
694 std::map<int, std::vector<uint>> shoot_cylinder_ids; // shoot ID -> cylinder IDs
695 std::map<int, uint> shoot_branch_id; // shoot ID -> branch ID
696 std::map<int, uint> shoot_branch_order; // shoot ID -> branch order
697
698 // Assign branch IDs and orders to shoots
699 uint branch_id_counter = 1;
700 for (const auto &shoot: plant.shoot_tree) {
701 shoot_branch_id[shoot->ID] = branch_id_counter++;
702
703 // Determine branch order based on parent
704 if (shoot->parent_shoot_ID == -1) {
705 // Base shoot is order 0 (trunk)
706 shoot_branch_order[shoot->ID] = 0;
707 } else {
708 // Child shoot has order = parent order + 1
709 shoot_branch_order[shoot->ID] = shoot_branch_order[shoot->parent_shoot_ID] + 1;
710 }
711 }
712
713 // Process each shoot
714 for (const auto &shoot: plant.shoot_tree) {
715
716 // Get shoot properties
717 uint branch_id = shoot_branch_id[shoot->ID];
718 uint branch_order = shoot_branch_order[shoot->ID];
719 uint position_in_branch = 1;
720 // Track the last vertex position for vertex sharing between phytomers
721 helios::vec3 last_vertex_position;
722 bool has_last_vertex = false;
723
724 // Process each phytomer in the shoot
725 for (uint phytomer_idx = 0; phytomer_idx < shoot->phytomers.size(); phytomer_idx++) {
726
727 const auto &vertices = shoot->shoot_internode_vertices[phytomer_idx];
728 const auto &radii = shoot->shoot_internode_radii[phytomer_idx];
729
730 // Handle vertex sharing for single-segment phytomer tubes
731 if (vertices.size() == 1 && has_last_vertex) {
732 // This phytomer has only one vertex - use the last vertex from previous phytomer as start
733 helios::vec3 start = last_vertex_position;
734 helios::vec3 current_end = vertices[0];
735 float current_radius = radii[0];
736
737 // Calculate initial length
738 helios::vec3 axis = current_end - start;
739 float length = axis.magnitude();
740
741 axis = axis / length; // Normalize axis
742
743 // Process this as a single cylinder
744 // Determine parent cylinder ID
745 uint parent_id = 0;
746 if (cylinder_id > 1) {
747 parent_id = cylinder_id - 1; // Parent is previous cylinder
748 }
749
750 // Extension cylinder (next cylinder in same branch) - will be updated later if needed
751 uint extension_id = 0;
752
753 // Write cylinder data
754 output_qsm << std::fixed << std::setprecision(4);
755 output_qsm << current_radius << "\t";
756 output_qsm << length << "\t";
757 output_qsm << start.x << "\t" << start.y << "\t" << start.z << "\t";
758 output_qsm << axis.x << "\t" << axis.y << "\t" << axis.z << "\t";
759 output_qsm << parent_id << "\t";
760 output_qsm << extension_id << "\t";
761 output_qsm << branch_id << "\t";
762 output_qsm << branch_order << "\t";
763 output_qsm << position_in_branch << "\t";
764 output_qsm << "0.0002" << "\t"; // mad (using default value)
765 output_qsm << "1" << "\t"; // SurfCov (using default value)
766 output_qsm << "0" << "\t"; // added flag
767 output_qsm << current_radius << std::endl; // UnmodRadius
768
769 // Store cylinder ID for this shoot
770 shoot_cylinder_ids[shoot->ID].push_back(cylinder_id);
771
772 cylinder_id++;
773 position_in_branch++;
774
775 // Update last vertex for next phytomer
776 last_vertex_position = current_end;
777
778 } else {
779 // Normal processing for phytomers with multiple vertices
780 for (int seg = 0; seg < vertices.size() - 1; seg++) {
781
782 // Start with the current segment
783 helios::vec3 start = vertices[seg];
784 helios::vec3 current_end = vertices[seg + 1];
785 float current_radius = radii[seg];
786
787 // Calculate initial length
788 helios::vec3 axis = current_end - start;
789 float length = axis.magnitude();
790
791 axis = axis / length; // Normalize axis
792
793 // Determine parent cylinder ID
794 uint parent_id = 0;
795 if (cylinder_id > 1) {
796 if (seg == 0 && phytomer_idx == 0 && shoot->parent_shoot_ID != -1) {
797 // First cylinder of child shoot - parent is last cylinder of connection point
798 // For simplicity, using previous cylinder as parent
799 parent_id = cylinder_id - 1;
800 } else if (seg == 0 && phytomer_idx > 0) {
801 // First segment of new phytomer - parent is last segment of previous phytomer
802 parent_id = cylinder_id - 1;
803 } else {
804 // Continuation within phytomer - parent is previous segment
805 parent_id = cylinder_id - 1;
806 }
807 }
808
809 // Extension cylinder (next cylinder in same branch) - will be updated later if needed
810 uint extension_id = 0;
811
812 // Write cylinder data
813 output_qsm << std::fixed << std::setprecision(4);
814 output_qsm << current_radius << "\t";
815 output_qsm << length << "\t";
816 output_qsm << start.x << "\t" << start.y << "\t" << start.z << "\t";
817 output_qsm << axis.x << "\t" << axis.y << "\t" << axis.z << "\t";
818 output_qsm << parent_id << "\t";
819 output_qsm << extension_id << "\t";
820 output_qsm << branch_id << "\t";
821 output_qsm << branch_order << "\t";
822 output_qsm << position_in_branch << "\t";
823 output_qsm << "0.0002" << "\t"; // mad (using default value)
824 output_qsm << "1" << "\t"; // SurfCov (using default value)
825 output_qsm << "0" << "\t"; // added flag
826 output_qsm << current_radius << std::endl; // UnmodRadius
827
828 // Store cylinder ID for this shoot
829 shoot_cylinder_ids[shoot->ID].push_back(cylinder_id);
830
831 cylinder_id++;
832 position_in_branch++;
833 }
834
835 // Update last vertex position for vertex sharing
836 if (vertices.size() >= 2) {
837 last_vertex_position = vertices.back();
838 has_last_vertex = true;
839 } else if (vertices.size() == 1) {
840 // This shouldn't happen in normal processing, but handle it for completeness
841 last_vertex_position = vertices[0];
842 has_last_vertex = true;
843 }
844 }
845 }
846 }
847
848 output_qsm.close();
849}
850
851std::vector<uint> PlantArchitecture::readPlantStructureXML(const std::string &filename, bool quiet) {
852
853 if (!quiet) {
854 std::cout << "Loading plant architecture XML file: " << filename << "..." << std::flush;
855 }
856
857 std::string fn = filename;
858 std::string ext = getFileExtension(filename);
859 if (ext != ".xml" && ext != ".XML") {
860 helios_runtime_error("failed.\n File " + fn + " is not XML format.");
861 }
862
863 std::vector<uint> plantIDs;
864
865 // Using "pugixml" parser. See pugixml.org
866 pugi::xml_document xmldoc;
867
868 // Resolve file path using project-based resolution
869 std::filesystem::path resolved_path = resolveProjectFile(filename);
870 std::string resolved_filename = resolved_path.string();
871
872 // load file
873 pugi::xml_parse_result load_result = xmldoc.load_file(resolved_filename.c_str());
874
875 // error checking
876 if (!load_result) {
877 helios_runtime_error("ERROR (Context::readPlantStructureXML): Could not parse " + std::string(filename) + ":\nError description: " + load_result.description());
878 }
879
880 pugi::xml_node helios = xmldoc.child("helios");
881
882 pugi::xml_node node;
883 std::string node_string;
884
885 if (helios.empty()) {
886 if (!quiet) {
887 std::cout << "failed." << std::endl;
888 }
889 helios_runtime_error("ERROR (Context::readPlantStructureXML): XML file must have tag '<helios> ... </helios>' bounding all other tags.");
890 }
891
892 size_t phytomer_count = 0;
893
894 std::map<int, int> shoot_ID_mapping;
895
896 for (pugi::xml_node plant = helios.child("plant_instance"); plant; plant = plant.next_sibling("plant_instance")) {
897
898 int plantID = std::stoi(plant.attribute("ID").value());
899
900 // base position
901 node_string = "base_position";
902 vec3 base_position = parse_xml_tag_vec3(plant.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
903
904 // plant age
905 node_string = "plant_age";
906 float plant_age = parse_xml_tag_float(plant.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
907
908 plantID = addPlantInstance(base_position, plant_age);
909 plantIDs.push_back(plantID);
910
911 int current_shoot_ID;
912
913 for (pugi::xml_node shoot = plant.child("shoot"); shoot; shoot = shoot.next_sibling("shoot")) {
914
915 int shootID = std::stoi(shoot.attribute("ID").value());
916 bool base_shoot = true;
917
918 // shoot type
919 node_string = "shoot_type_label";
920 std::string shoot_type_label = parse_xml_tag_string(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
921
922 // parent shoot ID
923 node_string = "parent_shoot_ID";
924 int parent_shoot_ID = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
925
926 // parent node index
927 node_string = "parent_node_index";
928 int parent_node_index = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
929
930 // parent petiole index
931 node_string = "parent_petiole_index";
932 int parent_petiole_index = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
933
934 // base rotation
935 node_string = "base_rotation";
936 vec3 base_rot = parse_xml_tag_vec3(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
937 AxisRotation base_rotation(deg2rad(base_rot.x), deg2rad(base_rot.y), deg2rad(base_rot.z));
938
939 for (pugi::xml_node phytomer = shoot.child("phytomer"); phytomer; phytomer = phytomer.next_sibling("phytomer")) {
940
941 pugi::xml_node internode = phytomer.child("internode");
942
943 // internode length
944 node_string = "internode_length";
945 float internode_length = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
946
947 // internode radius
948 node_string = "internode_radius";
949 float internode_radius = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
950
951 // internode pitch
952 node_string = "internode_pitch";
953 float internode_pitch = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
954
955 // internode phyllotactic angle
956 node_string = "internode_phyllotactic_angle";
957 float internode_phyllotactic_angle = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
958
959 // Read new parameters for reconstruction from parameters
960 // internode_length_max
961 float internode_length_max = internode_length; // default to current length
962 node_string = "internode_length_max";
963 if (internode.child(node_string.c_str())) {
964 internode_length_max = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
965 }
966
967 // internode_length_segments
968 uint internode_length_segments = 1; // default
969 node_string = "internode_length_segments";
970 if (internode.child(node_string.c_str())) {
971 internode_length_segments = (uint) parse_xml_tag_int(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
972 }
973
974 // curvature_perturbations (semicolon-delimited)
975 std::vector<float> curvature_perturbations;
976 node_string = "curvature_perturbations";
977 if (internode.child(node_string.c_str())) {
978 std::string perturbs_str = internode.child_value(node_string.c_str());
979 std::istringstream perturbs_stream(perturbs_str);
980 std::string pert_str;
981 while (std::getline(perturbs_stream, pert_str, ';')) {
982 curvature_perturbations.push_back(std::stof(pert_str));
983 }
984 }
985
986 // yaw_perturbations (semicolon-delimited)
987 std::vector<float> yaw_perturbations;
988 node_string = "yaw_perturbations";
989 if (internode.child(node_string.c_str())) {
990 std::string yaw_perturbs_str = internode.child_value(node_string.c_str());
991 std::istringstream yaw_stream(yaw_perturbs_str);
992 std::string yaw_str;
993 while (std::getline(yaw_stream, yaw_str, ';')) {
994 yaw_perturbations.push_back(std::stof(yaw_str));
995 }
996 }
997
998 // Read internode vertices if available (optional for backward compatibility - values are ignored, geometry reconstructed from parameters)
999 std::vector<vec3> internode_vertices;
1000 node_string = "internode_vertices";
1001 if (internode.child(node_string.c_str())) {
1002 std::string vertices_str = internode.child_value(node_string.c_str());
1003 std::istringstream verts_stream(vertices_str);
1004 std::string vertex_str;
1005 while (std::getline(verts_stream, vertex_str, ';')) {
1006 std::istringstream vertex_coords(vertex_str);
1007 float x, y, z;
1008 if (vertex_coords >> x >> y >> z) {
1009 internode_vertices.push_back(make_vec3(x, y, z));
1010 }
1011 }
1012 }
1013
1014 // Read internode radii if available (optional for backward compatibility - values are ignored, geometry reconstructed from parameters)
1015 std::vector<float> internode_radii;
1016 node_string = "internode_radii";
1017 if (internode.child(node_string.c_str())) {
1018 std::string radii_str = internode.child_value(node_string.c_str());
1019 std::istringstream radii_stream(radii_str);
1020 std::string radius_str;
1021 while (std::getline(radii_stream, radius_str, ';')) {
1022 float radius = std::stof(radius_str);
1023 internode_radii.push_back(radius);
1024 }
1025 }
1026
1027 float petiole_length;
1028 float petiole_radius;
1029 float petiole_pitch;
1030 float petiole_curvature;
1031 float current_leaf_scale_factor_value;
1032 float leaflet_scale;
1033 float leaflet_offset;
1034 std::vector<float> petiole_lengths; // actual length of each petiole within internode
1035 std::vector<float> petiole_radii_values; // actual radius of each petiole within internode
1036 std::vector<float> petiole_pitches; // pitch of each petiole within internode
1037 std::vector<float> petiole_curvatures; // curvature of each petiole within internode
1038 std::vector<vec3> petiole_base_positions; // actual base position of each petiole within internode
1039 std::vector<float> current_leaf_scale_factors; // scale factor of each petiole within internode
1040 std::vector<float> petiole_tapers; // taper value for each petiole
1041 std::vector<uint> petiole_length_segments_all; // number of segments for each petiole
1042 std::vector<uint> petiole_radial_subdivisions_all; // radial subdivisions for each petiole
1043 std::vector<std::vector<vec3>> saved_leaf_bases_all_petioles; // saved leaf attachment positions for each petiole (if saved in XML)
1044 std::vector<std::vector<float>> leaf_scale; // first index is petiole within internode; second index is leaf within petiole
1045 std::vector<std::vector<float>> leaf_pitch;
1046 std::vector<std::vector<float>> leaf_yaw;
1047 std::vector<std::vector<float>> leaf_roll;
1048
1049 // Floral bud data structures
1050 struct FloralBudData {
1051 int bud_state;
1052 uint parent_index;
1053 uint bud_index;
1054 bool is_terminal;
1055 float current_fruit_scale_factor;
1056 std::vector<vec3> inflorescence_bases_saved; // saved attachment points on peduncle
1057 std::vector<vec3> flower_positions; // saved flower/fruit center positions
1058 std::vector<AxisRotation> flower_rotations; // pitch, yaw, roll for each flower/fruit
1059 std::vector<float> flower_base_scales; // individual base scale for each flower/fruit
1060 // Inflorescence parameters
1061 float flower_offset = -1;
1062 // Peduncle parameters (actual sampled values)
1063 float peduncle_length = -1;
1064 float peduncle_radius = -1;
1065 float peduncle_pitch = 0;
1066 float peduncle_roll = 0;
1067 float peduncle_curvature = 0;
1068 };
1069 std::vector<std::vector<FloralBudData>> floral_bud_data; // first index is petiole within internode; second index is bud within petiole
1070 for (pugi::xml_node petiole = internode.child("petiole"); petiole; petiole = petiole.next_sibling("petiole")) {
1071
1072 // petiole length
1073 node_string = "petiole_length";
1074 petiole_length = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1075
1076 // petiole radius
1077 node_string = "petiole_radius";
1078 petiole_radius = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1079
1080 // petiole pitch
1081 node_string = "petiole_pitch";
1082 petiole_pitch = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1083
1084 // petiole curvature
1085 node_string = "petiole_curvature";
1086 petiole_curvature = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1087
1088 // petiole base position (optional for backward compatibility)
1089 vec3 petiole_base_pos = nullorigin;
1090 node_string = "petiole_base_position";
1091 if (petiole.child(node_string.c_str())) {
1092 petiole_base_pos = parse_xml_tag_vec3(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1093 }
1094
1095 // current leaf scale factor
1096 node_string = "current_leaf_scale_factor";
1097 current_leaf_scale_factor_value = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1098
1099 // petiole taper
1100 node_string = "petiole_taper";
1101 float petiole_taper = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1102
1103 // petiole length segments
1104 node_string = "petiole_length_segments";
1105 uint length_segments = (uint) parse_xml_tag_int(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1106
1107 // petiole radial subdivisions
1108 node_string = "petiole_radial_subdivisions";
1109 uint radial_subdivisions = (uint) parse_xml_tag_int(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1110
1111 // leaflet scale factor
1112 node_string = "leaflet_scale";
1113 leaflet_scale = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1114
1115 // leaflet offset (optional for backward compatibility with old XML files)
1116 node_string = "leaflet_offset";
1117 if (petiole.child(node_string.c_str())) {
1118 leaflet_offset = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1119 } else {
1120 leaflet_offset = 0.f; // Default for backward compatibility
1121 }
1122
1123 // Store petiole properties in vectors
1124 petiole_lengths.push_back(petiole_length);
1125 petiole_radii_values.push_back(petiole_radius);
1126 petiole_pitches.push_back(petiole_pitch);
1127 petiole_curvatures.push_back(petiole_curvature);
1128 current_leaf_scale_factors.push_back(current_leaf_scale_factor_value);
1129 petiole_base_positions.push_back(petiole_base_pos);
1130 petiole_tapers.push_back(petiole_taper);
1131 petiole_length_segments_all.push_back(length_segments);
1132 petiole_radial_subdivisions_all.push_back(radial_subdivisions);
1133
1134 leaf_scale.resize(leaf_scale.size() + 1);
1135 leaf_pitch.resize(leaf_pitch.size() + 1);
1136 leaf_yaw.resize(leaf_yaw.size() + 1);
1137 leaf_roll.resize(leaf_roll.size() + 1);
1138 floral_bud_data.resize(floral_bud_data.size() + 1); // Always resize to stay in sync with leaf vectors
1139
1140 std::vector<vec3> saved_leaf_bases; // Saved leaf attachment positions
1141
1142 for (pugi::xml_node leaf = petiole.child("leaf"); leaf; leaf = leaf.next_sibling("leaf")) {
1143
1144 // leaf scale factor
1145 node_string = "leaf_scale";
1146 leaf_scale.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1147
1148 // leaf base position (optional for backward compatibility - value is ignored, auto-calculated below)
1149 node_string = "leaf_base";
1150 if (leaf.child(node_string.c_str())) {
1151 std::string base_str = leaf.child_value(node_string.c_str());
1152 std::istringstream base_stream(base_str);
1153 float x, y, z;
1154 if (base_stream >> x >> y >> z) {
1155 saved_leaf_bases.push_back(make_vec3(x, y, z));
1156 }
1157 }
1158
1159 // leaf pitch
1160 node_string = "leaf_pitch";
1161 leaf_pitch.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1162
1163 // leaf yaw
1164 node_string = "leaf_yaw";
1165 leaf_yaw.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1166
1167 // leaf roll
1168 node_string = "leaf_roll";
1169 leaf_roll.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1170 }
1171
1172 // Store saved_leaf_bases for this petiole
1173 saved_leaf_bases_all_petioles.push_back(saved_leaf_bases);
1174
1175 // Read floral buds (if present)
1176 for (pugi::xml_node floral_bud = petiole.child("floral_bud"); floral_bud; floral_bud = floral_bud.next_sibling("floral_bud")) {
1177
1178 FloralBudData fbud_data;
1179
1180 // Read bud state
1181 node_string = "bud_state";
1182 fbud_data.bud_state = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1183
1184 // Read parent index
1185 node_string = "parent_index";
1186 fbud_data.parent_index = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1187
1188 // Read bud index
1189 node_string = "bud_index";
1190 fbud_data.bud_index = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1191
1192 // Read is_terminal
1193 node_string = "is_terminal";
1194 int is_terminal = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1195 fbud_data.is_terminal = (is_terminal != 0);
1196
1197 // Read current fruit scale factor
1198 node_string = "current_fruit_scale_factor";
1199 fbud_data.current_fruit_scale_factor = parse_xml_tag_float(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1200
1201 // Read peduncle parameters (if present)
1202 pugi::xml_node peduncle = floral_bud.child("peduncle");
1203 if (peduncle) {
1204 node_string = "length";
1205 if (peduncle.child(node_string.c_str())) {
1206 fbud_data.peduncle_length = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1207 }
1208 node_string = "radius";
1209 if (peduncle.child(node_string.c_str())) {
1210 fbud_data.peduncle_radius = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1211 }
1212 node_string = "pitch";
1213 if (peduncle.child(node_string.c_str())) {
1214 fbud_data.peduncle_pitch = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1215 }
1216 node_string = "roll";
1217 if (peduncle.child(node_string.c_str())) {
1218 fbud_data.peduncle_roll = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1219 }
1220 node_string = "curvature";
1221 if (peduncle.child(node_string.c_str())) {
1222 fbud_data.peduncle_curvature = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1223 }
1224 }
1225
1226 // Read inflorescence parameters and flower positions (if present)
1227 pugi::xml_node inflorescence = floral_bud.child("inflorescence");
1228 if (inflorescence) {
1229 // Read inflorescence parameters
1230 node_string = "flower_offset";
1231 if (inflorescence.child(node_string.c_str())) {
1232 fbud_data.flower_offset = parse_xml_tag_float(inflorescence.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1233 }
1234
1235 // Read individual flower/fruit data (base position and rotations)
1236 for (pugi::xml_node flower = inflorescence.child("flower"); flower; flower = flower.next_sibling("flower")) {
1237 // inflorescence_base (optional for backward compatibility - value is ignored, auto-calculated)
1238 pugi::xml_node base_node = flower.child("inflorescence_base");
1239 if (base_node) {
1240 vec3 base = parse_xml_tag_vec3(base_node, "inflorescence_base", "PlantArchitecture::readPlantStructureXML");
1241 fbud_data.inflorescence_bases_saved.push_back(base);
1242 }
1243
1244 // Read pitch, yaw, roll, and azimuth
1245 AxisRotation rotation;
1246 pugi::xml_node pitch_node = flower.child("flower_pitch");
1247 pugi::xml_node yaw_node = flower.child("flower_yaw");
1248 pugi::xml_node roll_node = flower.child("flower_roll");
1249 pugi::xml_node azimuth_node = flower.child("flower_azimuth");
1250
1251 if (pitch_node) {
1252 rotation.pitch = parse_xml_tag_float(pitch_node, "flower_pitch", "PlantArchitecture::readPlantStructureXML");
1253 } else {
1254 rotation.pitch = 0;
1255 }
1256
1257 if (yaw_node) {
1258 rotation.yaw = parse_xml_tag_float(yaw_node, "flower_yaw", "PlantArchitecture::readPlantStructureXML");
1259 } else {
1260 rotation.yaw = 0;
1261 }
1262
1263 if (roll_node) {
1264 rotation.roll = parse_xml_tag_float(roll_node, "flower_roll", "PlantArchitecture::readPlantStructureXML");
1265 } else {
1266 rotation.roll = 0;
1267 }
1268
1269 if (azimuth_node) {
1270 rotation.azimuth = parse_xml_tag_float(azimuth_node, "flower_azimuth", "PlantArchitecture::readPlantStructureXML");
1271 } else {
1272 rotation.azimuth = 0;
1273 }
1274
1275 fbud_data.flower_rotations.push_back(rotation);
1276
1277 // Read individual base scale for this flower/fruit
1278 pugi::xml_node scale_node = flower.child("flower_base_scale");
1279 if (scale_node) {
1280 float base_scale = parse_xml_tag_float(scale_node, "flower_base_scale", "PlantArchitecture::readPlantStructureXML");
1281 fbud_data.flower_base_scales.push_back(base_scale);
1282 } else {
1283 // For backward compatibility with old XML files, use -1 to indicate not set
1284 fbud_data.flower_base_scales.push_back(-1.0f);
1285 }
1286 }
1287 }
1288
1289 floral_bud_data.back().push_back(fbud_data);
1290 }
1291 } // petioles
1292
1293 if (shoot_types.find(shoot_type_label) == shoot_types.end()) {
1294 helios_runtime_error("ERROR (PlantArchitecture::readPlantStructureXML): Shoot type " + shoot_type_label + " not found in shoot types.");
1295 }
1296
1297
1298 ShootParameters shoot_parameters = getCurrentShootParameters(shoot_type_label);
1299
1300 shoot_parameters.phytomer_parameters.phytomer_creation_function = nullptr;
1301
1302 shoot_parameters.phytomer_parameters.internode.pitch = internode_pitch;
1303 shoot_parameters.phytomer_parameters.internode.phyllotactic_angle = internode_phyllotactic_angle;
1304
1305 shoot_parameters.phytomer_parameters.petiole.length = petiole_length;
1306 shoot_parameters.phytomer_parameters.petiole.radius = petiole_radius;
1307 shoot_parameters.phytomer_parameters.petiole.pitch = petiole_pitch;
1308 shoot_parameters.phytomer_parameters.petiole.curvature = petiole_curvature;
1309
1310 shoot_parameters.phytomer_parameters.leaf.prototype_scale = 1.f; // leaf_scale.front().at(tip_ind);
1311 shoot_parameters.phytomer_parameters.leaf.pitch = 0;
1312 shoot_parameters.phytomer_parameters.leaf.yaw = 0;
1313 shoot_parameters.phytomer_parameters.leaf.roll = 0;
1314 shoot_parameters.phytomer_parameters.leaf.leaflet_scale = leaflet_scale;
1315 shoot_parameters.phytomer_parameters.leaf.leaflet_offset = leaflet_offset;
1316
1317 if (base_shoot) {
1318
1319 if (parent_shoot_ID < 0) { // this is the first shoot of the plant
1320 current_shoot_ID = addBaseStemShoot(plantID, 1, base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, shoot_type_label);
1321 shoot_ID_mapping[shootID] = current_shoot_ID;
1322 } else { // this is a child of an existing shoot
1323 current_shoot_ID = addChildShoot(plantID, shoot_ID_mapping.at(parent_shoot_ID), parent_node_index, 1, base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, shoot_type_label, parent_petiole_index);
1324 shoot_ID_mapping[shootID] = current_shoot_ID;
1325 }
1326
1327 base_shoot = false;
1328 } else {
1329 appendPhytomerToShoot(plantID, current_shoot_ID, shoot_parameters.phytomer_parameters, internode_radius, internode_length, 1, 1);
1330 }
1331
1332 // Get pointer to the newly created phytomer
1333 auto phytomer_ptr = plant_instances.at(plantID).shoot_tree.at(current_shoot_ID)->phytomers.back();
1334
1335 // Restore internode properties from saved values
1336 phytomer_ptr->internode_pitch = deg2rad(internode_pitch);
1337 phytomer_ptr->internode_phyllotactic_angle = deg2rad(internode_phyllotactic_angle);
1338
1339 // Get shoot pointer for internode geometry restoration
1340 auto shoot_ptr = plant_instances.at(plantID).shoot_tree.at(current_shoot_ID);
1341 uint phytomer_index_in_shoot = shoot_ptr->phytomers.size() - 1;
1342
1343 // Helper function to recompute internode orientation vectors from parent phytomer context
1344 auto recomputeInternodeOrientationVectors_local = [this, plantID](std::shared_ptr<Phytomer> phytomer_ptr, uint phytomer_index_in_shoot, float internode_pitch_rad, float internode_phyllotactic_angle_rad,
1345 helios::vec3 &out_internode_axis_initial, helios::vec3 &out_petiole_rotation_axis, helios::vec3 &out_shoot_bending_axis) {
1346 using helios::make_vec3;
1348 using helios::nullorigin;
1349 using helios::cross;
1350
1351 // Get parent axes
1352 helios::vec3 parent_internode_axis = make_vec3(0, 0, 1);
1353 helios::vec3 parent_petiole_axis = make_vec3(0, -1, 0);
1354
1355 if (phytomer_index_in_shoot > 0) {
1356 auto prev_phytomer = phytomer_ptr->parent_shoot_ptr->phytomers.at(phytomer_index_in_shoot - 1);
1357 parent_internode_axis = prev_phytomer->getInternodeAxisVector(1.0f);
1358 parent_petiole_axis = prev_phytomer->getPetioleAxisVector(0.f, 0);
1359 } else if (phytomer_ptr->parent_shoot_ptr->parent_shoot_ID >= 0) {
1360 int parent_shoot_id = phytomer_ptr->parent_shoot_ptr->parent_shoot_ID;
1361 uint parent_node_index = phytomer_ptr->parent_shoot_ptr->parent_node_index;
1362 uint parent_petiole_index = phytomer_ptr->parent_shoot_ptr->parent_petiole_index;
1363 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1364 auto parent_phytomer = parent_shoot->phytomers.at(parent_node_index);
1365 parent_internode_axis = parent_phytomer->getInternodeAxisVector(1.0f);
1366 parent_petiole_axis = parent_phytomer->getPetioleAxisVector(0.f, parent_petiole_index);
1367 }
1368
1369 helios::vec3 petiole_rotation_axis = cross(parent_internode_axis, parent_petiole_axis);
1370 if (petiole_rotation_axis.magnitude() < 1e-6f) {
1371 petiole_rotation_axis = make_vec3(1, 0, 0);
1372 } else {
1373 petiole_rotation_axis.normalize();
1374 }
1375
1376 helios::vec3 internode_axis = parent_internode_axis;
1377
1378 if (phytomer_index_in_shoot == 0) {
1379 AxisRotation shoot_base_rotation = phytomer_ptr->parent_shoot_ptr->base_rotation;
1380 if (internode_pitch_rad != 0.f) {
1381 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, 0.5f * internode_pitch_rad);
1382 }
1383 if (shoot_base_rotation.roll != 0.f) {
1384 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1385 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1386 }
1387 if (shoot_base_rotation.pitch != 0.f) {
1388 helios::vec3 base_pitch_axis = -1.0f * cross(parent_internode_axis, parent_petiole_axis);
1389 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1390 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1391 }
1392 if (shoot_base_rotation.yaw != 0.f) {
1393 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1394 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1395 }
1396 } else {
1397 if (internode_pitch_rad != 0.f) {
1398 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, -1.25f * internode_pitch_rad);
1399 }
1400 }
1401
1402 helios::vec3 shoot_bending_axis = cross(internode_axis, make_vec3(0, 0, 1));
1403 if (internode_axis == make_vec3(0, 0, 1)) {
1404 shoot_bending_axis = make_vec3(0, 1, 0);
1405 } else {
1406 shoot_bending_axis.normalize();
1407 }
1408
1409 out_internode_axis_initial = internode_axis;
1410 out_petiole_rotation_axis = petiole_rotation_axis;
1411 out_shoot_bending_axis = shoot_bending_axis;
1412 };
1413
1414 // Reconstruct internode geometry from parameters
1415 if (internode_length_segments > 0) {
1416 // Step 1: Compute base position
1417 helios::vec3 internode_base;
1418 if (phytomer_index_in_shoot == 0) {
1419 // First phytomer in this shoot
1420 if (shoot_ptr->parent_shoot_ID < 0) {
1421 // Base shoot: use plant base (origin)
1422 internode_base = make_vec3(0, 0, 0);
1423 } else {
1424 // Child shoot: get base from SAVED parent internode tip (petioles not reconstructed yet)
1425 // Note: Cannot use parent petiole tip because petioles are reconstructed later in the loop
1426 int parent_shoot_id = shoot_ptr->parent_shoot_ID;
1427 uint parent_node_index = shoot_ptr->parent_node_index;
1428 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1429
1430 // Use the saved internode tip from the parent phytomer
1431 internode_base = parent_shoot->shoot_internode_vertices.at(parent_node_index).back();
1432 }
1433 } else {
1434 // Subsequent phytomers: use previous phytomer's tip
1435 internode_base = shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot - 1].back();
1436 }
1437
1438 // Step 2: Recompute orientation vectors from parent context
1439 helios::vec3 internode_axis_initial;
1440 helios::vec3 petiole_rotation_axis;
1441 helios::vec3 shoot_bending_axis;
1442
1443 recomputeInternodeOrientationVectors_local(phytomer_ptr, phytomer_index_in_shoot, deg2rad(internode_pitch), deg2rad(internode_phyllotactic_angle), internode_axis_initial, petiole_rotation_axis, shoot_bending_axis);
1444
1445 // Step 3: Reconstruct geometry segment-by-segment
1446 std::vector<helios::vec3> reconstructed_vertices(internode_length_segments + 1);
1447 std::vector<float> reconstructed_radii(internode_length_segments + 1);
1448
1449 reconstructed_vertices[0] = internode_base;
1450 reconstructed_radii[0] = internode_radius;
1451
1452 float dr = internode_length / float(internode_length_segments);
1453 float dr_max = internode_length_max / float(internode_length_segments);
1454
1455 helios::vec3 internode_axis = internode_axis_initial;
1456
1457 for (int i = 1; i <= internode_length_segments; i++) {
1458 // Apply gravitropic curvature + SAVED perturbations
1459 if (phytomer_index_in_shoot > 0 && !curvature_perturbations.empty()) {
1460 // Compute curvature factor (lines 1633-1636 in PlantArchitecture.cpp)
1461 float current_curvature_fact = 0.5f - internode_axis.z / 2.0f;
1462 if (internode_axis.z < 0) {
1463 current_curvature_fact *= 2.0f;
1464 }
1465
1466 // Get gravitropic curvature from shoot
1467 float gravitropic_curvature = shoot_ptr->gravitropic_curvature;
1468
1469 // Apply curvature with SAVED perturbation (matches line 1646-1647)
1470 float curvature_angle = deg2rad(gravitropic_curvature * current_curvature_fact * dr_max + curvature_perturbations[i - 1]);
1471 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, shoot_bending_axis, curvature_angle);
1472
1473 // Apply yaw perturbation if available (matches line 1651-1652)
1474 if (!yaw_perturbations.empty() && (i - 1) < yaw_perturbations.size()) {
1475 float yaw_angle = deg2rad(yaw_perturbations[i - 1]);
1476 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, make_vec3(0, 0, 1), yaw_angle);
1477 }
1478 }
1479
1480 // NOTE: Skip collision avoidance and attraction (environment-dependent, lines 1655-1693)
1481
1482 // Position next vertex
1483 reconstructed_vertices[i] = reconstructed_vertices[i - 1] + dr * internode_axis;
1484 reconstructed_radii[i] = internode_radius;
1485 }
1486
1487 // Use reconstructed geometry
1488 shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot] = reconstructed_vertices;
1489 shoot_ptr->shoot_internode_radii[phytomer_index_in_shoot] = reconstructed_radii;
1490 } else if (!internode_vertices.empty() && !internode_radii.empty()) {
1491 // Fallback: use saved geometry if no reconstruction parameters available (backward compatibility)
1492 shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot] = internode_vertices;
1493 shoot_ptr->shoot_internode_radii[phytomer_index_in_shoot] = internode_radii;
1494 }
1495
1496 // Helper function to recompute petiole orientation vectors from parent phytomer context
1497 auto recomputePetioleOrientationVectors = [this, plantID](std::shared_ptr<Phytomer> phytomer_ptr, uint petiole_index, uint phytomer_index_in_shoot, float petiole_pitch_rad, float internode_phyllotactic_angle_rad,
1498 helios::vec3 &out_petiole_axis_initial, helios::vec3 &out_petiole_rotation_axis) {
1499 using helios::make_vec3;
1501 using helios::nullorigin;
1502 using helios::cross;
1503
1504 // Step 1: Get parent axes for rotation axis computation
1505 helios::vec3 parent_internode_axis = make_vec3(0, 0, 1); // default for base shoot
1506 helios::vec3 parent_petiole_axis = make_vec3(0, -1, 0); // default for base shoot
1507
1508 // Get actual parent axes if available
1509 if (phytomer_index_in_shoot > 0) {
1510 // Previous phytomer in same shoot
1511 auto prev_phytomer = phytomer_ptr->parent_shoot_ptr->phytomers.at(phytomer_index_in_shoot - 1);
1512 parent_internode_axis = prev_phytomer->getInternodeAxisVector(1.0f);
1513 parent_petiole_axis = prev_phytomer->getPetioleAxisVector(0.f, 0);
1514 } else if (phytomer_ptr->parent_shoot_ptr->parent_shoot_ID >= 0) {
1515 // First phytomer of child shoot - get from parent phytomer via shoot_tree
1516 int parent_shoot_id = phytomer_ptr->parent_shoot_ptr->parent_shoot_ID;
1517 uint parent_node_index = phytomer_ptr->parent_shoot_ptr->parent_node_index;
1518 uint parent_petiole_index = phytomer_ptr->parent_shoot_ptr->parent_petiole_index;
1519
1520 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1521 auto parent_phytomer = parent_shoot->phytomers.at(parent_node_index);
1522 parent_internode_axis = parent_phytomer->getInternodeAxisVector(1.0f);
1523 parent_petiole_axis = parent_phytomer->getPetioleAxisVector(0.f, parent_petiole_index);
1524 }
1525
1526 // Step 2: Compute base petiole_rotation_axis from parent axes (matches line 1530)
1527 helios::vec3 petiole_rotation_axis = cross(parent_internode_axis, parent_petiole_axis);
1528 if (petiole_rotation_axis.magnitude() < 1e-6f) {
1529 petiole_rotation_axis = make_vec3(1, 0, 0);
1530 } else {
1531 petiole_rotation_axis.normalize();
1532 }
1533
1534 // Step 3: Compute internode axis with CORRECT order (matches lines 1528-1578)
1535 helios::vec3 internode_axis = parent_internode_axis;
1536 float internode_pitch_rad = phytomer_ptr->internode_pitch;
1537
1538
1539 if (phytomer_index_in_shoot == 0) {
1540 // First phytomer: apply rotations in CORRECT order
1541
1542 // 1. Internode pitch FIRST (line 1539) using UNMODIFIED petiole_rotation_axis
1543 if (internode_pitch_rad != 0.f) {
1544 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, 0.5f * internode_pitch_rad);
1545 }
1546
1547 // 2. THEN apply shoot base rotations to BOTH axes (lines 1548-1564)
1548 AxisRotation shoot_base_rotation = phytomer_ptr->parent_shoot_ptr->base_rotation;
1549
1550 if (shoot_base_rotation.roll != 0.f) {
1551 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1552 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1553 }
1554
1555 if (shoot_base_rotation.pitch != 0.f) {
1556 helios::vec3 base_pitch_axis = -1.0f * cross(parent_internode_axis, parent_petiole_axis);
1557 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1558 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1559 }
1560
1561 if (shoot_base_rotation.yaw != 0.f) {
1562 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1563 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1564 }
1565 } else {
1566 // Non-first phytomer: apply internode pitch (scaled by -1.25, line 1576)
1567 if (internode_pitch_rad != 0.f) {
1568 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, -1.25f * internode_pitch_rad);
1569 }
1570 }
1571
1572
1573 // Step 5: Start with internode axis (matches line 1739)
1574 helios::vec3 petiole_axis = internode_axis;
1575
1576 // Step 6: Apply petiole pitch rotation (matches line 1753)
1577 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, petiole_rotation_axis, std::abs(petiole_pitch_rad));
1578
1579 // Step 7: Apply phyllotactic rotation (for non-first phytomers, matches line 1758-1760)
1580 if (phytomer_index_in_shoot != 0 && std::abs(internode_phyllotactic_angle_rad) > 0) {
1581 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, internode_axis, internode_phyllotactic_angle_rad);
1582 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, internode_axis, internode_phyllotactic_angle_rad);
1583 }
1584
1585 // Step 8: Apply bud rotation (for multi-petiole phytomers, matches line 1770-1773)
1586 if (petiole_index > 0) {
1587 uint petioles_per_internode = phytomer_ptr->phytomer_parameters.petiole.petioles_per_internode;
1588 float budrot = float(petiole_index) * 2.0f * float(M_PI) / float(petioles_per_internode);
1589 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, internode_axis, budrot);
1590 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, internode_axis, budrot);
1591 }
1592
1593 // Return computed vectors
1594 out_petiole_axis_initial = petiole_axis;
1595 out_petiole_rotation_axis = petiole_rotation_axis;
1596 };
1597
1598 // Lambda to recompute peduncle orientation vectors from parent context
1599 auto recomputePeduncleOrientationVectors = [this, plantID](std::shared_ptr<Phytomer> phytomer_ptr, uint petiole_index, uint phytomer_index_in_shoot, float peduncle_pitch_rad, float peduncle_roll_rad, const AxisRotation &base_rotation,
1600 vec3 &out_peduncle_axis_initial, vec3 &out_peduncle_rotation_axis) -> void {
1601 // Step 1: Get parent internode axis at tip (fraction=1.0)
1602 vec3 peduncle_axis = phytomer_ptr->getAxisVector(1.f, phytomer_ptr->getInternodeNodePositions());
1603
1604 // Step 2: Compute inflorescence_bending_axis from parent context
1605 // This logic mirrors PlantArchitecture.cpp lines 976-1009 for parent_internode_axis
1606 // and line 2041 for inflorescence_bending_axis
1607 vec3 parent_internode_axis = make_vec3(0, 0, 1); // default for base shoot
1608
1609 // Get actual parent internode axis if available
1610 if (phytomer_index_in_shoot > 0) {
1611 // Previous phytomer in same shoot
1612 auto prev_phytomer = phytomer_ptr->parent_shoot_ptr->phytomers.at(phytomer_index_in_shoot - 1);
1613 parent_internode_axis = prev_phytomer->getInternodeAxisVector(1.0f);
1614 } else if (phytomer_ptr->parent_shoot_ptr->parent_shoot_ID >= 0) {
1615 // First phytomer of child shoot - get from parent phytomer via shoot_tree
1616 int parent_shoot_id = phytomer_ptr->parent_shoot_ptr->parent_shoot_ID;
1617 uint parent_node_index = phytomer_ptr->parent_shoot_ptr->parent_node_index;
1618
1619 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1620 auto parent_phytomer = parent_shoot->phytomers.at(parent_node_index);
1621 parent_internode_axis = parent_phytomer->getInternodeAxisVector(1.0f);
1622 }
1623
1624 // Get current phytomer's petiole axis (NOT parent phytomer's petiole axis)
1625 vec3 current_petiole_axis;
1626 if (phytomer_ptr->petiole_vertices.empty()) {
1627 current_petiole_axis = parent_internode_axis;
1628 } else {
1629 // Use actual curved petiole geometry at base (fraction=0.f) to match generation behavior
1630 current_petiole_axis = phytomer_ptr->getPetioleAxisVector(0.f, petiole_index);
1631 }
1632
1633 vec3 inflorescence_bending_axis_actual = cross(parent_internode_axis, current_petiole_axis);
1634 if (inflorescence_bending_axis_actual.magnitude() < 0.001f) {
1635 inflorescence_bending_axis_actual = make_vec3(1, 0, 0);
1636 }
1637 inflorescence_bending_axis_actual.normalize();
1638
1639 // Step 3: Apply peduncle pitch rotation
1640 if (peduncle_pitch_rad != 0.f || base_rotation.pitch != 0.f) {
1641 peduncle_axis = rotatePointAboutLine(peduncle_axis, nullorigin, inflorescence_bending_axis_actual, peduncle_pitch_rad + base_rotation.pitch);
1642 }
1643
1644 // Step 4: Apply azimuthal alignment to parent petiole
1645 vec3 internode_axis = phytomer_ptr->getAxisVector(1.f, phytomer_ptr->getInternodeNodePositions());
1646 vec3 parent_petiole_base_axis = phytomer_ptr->petiole_vertices.empty() ? internode_axis : phytomer_ptr->getPetioleAxisVector(0.f, petiole_index);
1647
1648 float parent_petiole_azimuth = -std::atan2(parent_petiole_base_axis.y, parent_petiole_base_axis.x);
1649 float current_peduncle_azimuth = -std::atan2(peduncle_axis.y, peduncle_axis.x);
1650 float azimuthal_rotation = current_peduncle_azimuth - parent_petiole_azimuth;
1651
1652 peduncle_axis = rotatePointAboutLine(peduncle_axis, nullorigin, internode_axis, azimuthal_rotation);
1653 inflorescence_bending_axis_actual = rotatePointAboutLine(inflorescence_bending_axis_actual, nullorigin, internode_axis, azimuthal_rotation);
1654
1655 // Step 5: Return computed vectors
1656 out_peduncle_axis_initial = peduncle_axis;
1657 out_peduncle_rotation_axis = inflorescence_bending_axis_actual;
1658 };
1659
1660 // Restore petiole properties and reconstruct geometry from bulk parameters
1661 for (size_t p = 0; p < petiole_lengths.size(); p++) {
1662 if (p < phytomer_ptr->petiole_length.size()) {
1663
1664 // Store bulk parameters in phytomer
1665 phytomer_ptr->petiole_length.at(p) = petiole_lengths[p];
1666 phytomer_ptr->petiole_pitch.at(p) = deg2rad(petiole_pitches[p]);
1667 phytomer_ptr->petiole_curvature.at(p) = petiole_curvatures[p];
1668 phytomer_ptr->petiole_taper.at(p) = petiole_tapers[p];
1669 phytomer_ptr->current_leaf_scale_factor.at(p) = current_leaf_scale_factors[p];
1670
1671 // Update phytomer parameters for geometry construction
1672 phytomer_ptr->phytomer_parameters.petiole.length_segments = petiole_length_segments_all[p];
1673 phytomer_ptr->phytomer_parameters.petiole.radial_subdivisions = petiole_radial_subdivisions_all[p];
1674
1675 // Reconstruct petiole vertices and radii using exact construction algorithm
1676 uint Ndiv_petiole_length = std::max(uint(1), petiole_length_segments_all[p]);
1677 uint Ndiv_petiole_radius = std::max(uint(3), petiole_radial_subdivisions_all[p]);
1678
1679 phytomer_ptr->petiole_vertices.at(p).resize(Ndiv_petiole_length + 1);
1680 phytomer_ptr->petiole_radii.at(p).resize(Ndiv_petiole_length + 1);
1681
1682 // Set base vertex and radius
1683 // Auto-calculate petiole base from current phytomer's internode tip
1684 phytomer_ptr->petiole_vertices.at(p).at(0) = shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot].back();
1685 phytomer_ptr->petiole_radii.at(p).at(0) = petiole_radii_values[p];
1686
1687 // Compute segment length
1688 float dr_petiole = petiole_lengths[p] / float(Ndiv_petiole_length);
1689
1690 // Recompute orientation vectors from parent context
1691 vec3 recomputed_axis;
1692 vec3 recomputed_rotation_axis;
1693
1694 recomputePetioleOrientationVectors(phytomer_ptr,
1695 p, // petiole index
1696 phytomer_index_in_shoot,
1697 phytomer_ptr->petiole_pitch.at(p), // already in radians
1698 phytomer_ptr->internode_phyllotactic_angle, // already in radians
1699 recomputed_axis, recomputed_rotation_axis);
1700
1701 // Validate against saved vectors
1702
1703 // Use RECOMPUTED vectors for reconstruction (not saved ones)
1704 vec3 petiole_axis_actual = recomputed_axis;
1705 vec3 petiole_rotation_axis_actual = recomputed_rotation_axis;
1706
1707 // Create segments with curvature (matches construction algorithm at line 1830-1837)
1708 for (int j = 1; j <= Ndiv_petiole_length; j++) {
1709 // Apply curvature rotation
1710 if (fabs(petiole_curvatures[p]) > 0) {
1711 petiole_axis_actual = rotatePointAboutLine(petiole_axis_actual, nullorigin, petiole_rotation_axis_actual, -deg2rad(petiole_curvatures[p] * dr_petiole));
1712 }
1713
1714 // Position next vertex
1715 phytomer_ptr->petiole_vertices.at(p).at(j) = phytomer_ptr->petiole_vertices.at(p).at(j - 1) + dr_petiole * petiole_axis_actual;
1716
1717 // Apply taper to radius
1718 phytomer_ptr->petiole_radii.at(p).at(j) = current_leaf_scale_factors[p] * petiole_radii_values[p] * (1.0f - petiole_tapers[p] / float(Ndiv_petiole_length) * float(j));
1719 }
1720
1721 // Rebuild petiole Context geometry
1722 context_ptr->deleteObject(phytomer_ptr->petiole_objIDs[p]);
1723 std::vector<RGBcolor> petiole_colors(phytomer_ptr->petiole_radii[p].size(), phytomer_ptr->phytomer_parameters.petiole.color);
1724 phytomer_ptr->petiole_objIDs[p] = makeTubeFromCones(Ndiv_petiole_radius, phytomer_ptr->petiole_vertices[p], phytomer_ptr->petiole_radii[p], petiole_colors, context_ptr);
1725
1726 // Set primitive data labels
1727 if (!phytomer_ptr->petiole_objIDs[p].empty()) {
1728 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(phytomer_ptr->petiole_objIDs[p]), "object_label", "petiole");
1729 }
1730
1731 // Restore saved leaf base positions (already in saved_leaf_bases_all_petioles)
1732 if (p < saved_leaf_bases_all_petioles.size() && !saved_leaf_bases_all_petioles[p].empty()) {
1733 for (size_t leaf = 0; leaf < phytomer_ptr->leaf_bases[p].size(); leaf++) {
1734 if (leaf < saved_leaf_bases_all_petioles[p].size()) {
1735 phytomer_ptr->leaf_bases[p][leaf] = saved_leaf_bases_all_petioles[p][leaf];
1736 }
1737 }
1738 }
1739 }
1740 }
1741
1742 // Delete and recreate leaves with correct petiole/internode axes
1743 // This is necessary because leaves were created with wrong axes before petiole geometry was restored
1744
1745 // Step 1: Save object data from existing leaves
1746 std::vector<std::vector<std::map<std::string, int>>> saved_leaf_data_int;
1747 std::vector<std::vector<std::map<std::string, uint>>> saved_leaf_data_uint;
1748 std::vector<std::vector<std::map<std::string, float>>> saved_leaf_data_float;
1749 std::vector<std::vector<std::map<std::string, double>>> saved_leaf_data_double;
1750 std::vector<std::vector<std::map<std::string, vec2>>> saved_leaf_data_vec2;
1751 std::vector<std::vector<std::map<std::string, vec3>>> saved_leaf_data_vec3;
1752 std::vector<std::vector<std::map<std::string, vec4>>> saved_leaf_data_vec4;
1753 std::vector<std::vector<std::map<std::string, int2>>> saved_leaf_data_int2;
1754 std::vector<std::vector<std::map<std::string, int3>>> saved_leaf_data_int3;
1755 std::vector<std::vector<std::map<std::string, int4>>> saved_leaf_data_int4;
1756 std::vector<std::vector<std::map<std::string, std::string>>> saved_leaf_data_string;
1757
1758 saved_leaf_data_int.resize(phytomer_ptr->leaf_objIDs.size());
1759 saved_leaf_data_uint.resize(phytomer_ptr->leaf_objIDs.size());
1760 saved_leaf_data_float.resize(phytomer_ptr->leaf_objIDs.size());
1761 saved_leaf_data_double.resize(phytomer_ptr->leaf_objIDs.size());
1762 saved_leaf_data_vec2.resize(phytomer_ptr->leaf_objIDs.size());
1763 saved_leaf_data_vec3.resize(phytomer_ptr->leaf_objIDs.size());
1764 saved_leaf_data_vec4.resize(phytomer_ptr->leaf_objIDs.size());
1765 saved_leaf_data_int2.resize(phytomer_ptr->leaf_objIDs.size());
1766 saved_leaf_data_int3.resize(phytomer_ptr->leaf_objIDs.size());
1767 saved_leaf_data_int4.resize(phytomer_ptr->leaf_objIDs.size());
1768 saved_leaf_data_string.resize(phytomer_ptr->leaf_objIDs.size());
1769
1770 for (int petiole = 0; petiole < phytomer_ptr->leaf_objIDs.size(); petiole++) {
1771 saved_leaf_data_int[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1772 saved_leaf_data_uint[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1773 saved_leaf_data_float[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1774 saved_leaf_data_double[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1775 saved_leaf_data_vec2[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1776 saved_leaf_data_vec3[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1777 saved_leaf_data_vec4[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1778 saved_leaf_data_int2[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1779 saved_leaf_data_int3[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1780 saved_leaf_data_int4[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1781 saved_leaf_data_string[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1782
1783 for (int leaf = 0; leaf < phytomer_ptr->leaf_objIDs[petiole].size(); leaf++) {
1784 uint objID = phytomer_ptr->leaf_objIDs[petiole][leaf];
1785 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objID);
1786 if (!UUIDs.empty()) {
1787 // Get all primitive data labels
1788 std::vector<std::string> data_int = context_ptr->listPrimitiveData(UUIDs.front());
1789 for (const auto &label: data_int) {
1790 HeliosDataType type = context_ptr->getPrimitiveDataType(label.c_str());
1791 if (type == HELIOS_TYPE_INT) {
1792 int value;
1793 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1794 saved_leaf_data_int[petiole][leaf][label] = value;
1795 } else if (type == HELIOS_TYPE_UINT) {
1796 uint value;
1797 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1798 saved_leaf_data_uint[petiole][leaf][label] = value;
1799 } else if (type == HELIOS_TYPE_FLOAT) {
1800 float value;
1801 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1802 saved_leaf_data_float[petiole][leaf][label] = value;
1803 } else if (type == HELIOS_TYPE_DOUBLE) {
1804 double value;
1805 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1806 saved_leaf_data_double[petiole][leaf][label] = value;
1807 } else if (type == HELIOS_TYPE_VEC2) {
1808 vec2 value;
1809 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1810 saved_leaf_data_vec2[petiole][leaf][label] = value;
1811 } else if (type == HELIOS_TYPE_VEC3) {
1812 vec3 value;
1813 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1814 saved_leaf_data_vec3[petiole][leaf][label] = value;
1815 } else if (type == HELIOS_TYPE_VEC4) {
1816 vec4 value;
1817 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1818 saved_leaf_data_vec4[petiole][leaf][label] = value;
1819 } else if (type == HELIOS_TYPE_INT2) {
1820 int2 value;
1821 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1822 saved_leaf_data_int2[petiole][leaf][label] = value;
1823 } else if (type == HELIOS_TYPE_INT3) {
1824 int3 value;
1825 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1826 saved_leaf_data_int3[petiole][leaf][label] = value;
1827 } else if (type == HELIOS_TYPE_INT4) {
1828 int4 value;
1829 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1830 saved_leaf_data_int4[petiole][leaf][label] = value;
1831 } else if (type == HELIOS_TYPE_STRING) {
1832 std::string value;
1833 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1834 saved_leaf_data_string[petiole][leaf][label] = value;
1835 }
1836 }
1837 }
1838 }
1839 }
1840
1841 // Step 2: Delete existing leaves and clear data structures
1842 for (int petiole = 0; petiole < phytomer_ptr->leaf_objIDs.size(); petiole++) {
1843 for (uint objID: phytomer_ptr->leaf_objIDs[petiole]) {
1844 context_ptr->deleteObject(objID);
1845 }
1846 phytomer_ptr->leaf_objIDs[petiole].clear();
1847 phytomer_ptr->leaf_bases[petiole].clear();
1848 phytomer_ptr->leaf_rotation[petiole].clear();
1849 }
1850
1851 // Step 3: Recreate leaves using Phytomer constructor logic with corrected axes
1852 assert(leaf_scale.size() == leaf_pitch.size());
1853 float leaflet_offset_val = 0.f; // Will be set from saved data if available
1854
1855 // Create unique leaf prototypes if they don't exist (matching Phytomer constructor)
1856 if (leaf_scale.size() > 0) {
1857 // Find maximum leaves per petiole across all petioles
1858 int max_leaves_per_petiole = 0;
1859 for (size_t i = 0; i < leaf_scale.size(); i++) {
1860 max_leaves_per_petiole = std::max(max_leaves_per_petiole, (int) leaf_scale[i].size());
1861 }
1862 int leaves_per_petiole = max_leaves_per_petiole;
1863 assert(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier != 0);
1864
1865 if (phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes > 0 &&
1866 this->unique_leaf_prototype_objIDs.find(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier) == this->unique_leaf_prototype_objIDs.end()) {
1867 this->unique_leaf_prototype_objIDs[phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier].resize(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes);
1868 for (int prototype = 0; prototype < phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes; prototype++) {
1869 for (int leaf = 0; leaf < leaves_per_petiole; leaf++) {
1870 float ind_from_tip = float(leaf) - float(leaves_per_petiole - 1) / 2.f;
1871 uint objID_leaf = phytomer_ptr->phytomer_parameters.leaf.prototype.prototype_function(context_ptr, &phytomer_ptr->phytomer_parameters.leaf.prototype, ind_from_tip);
1872 if (phytomer_ptr->phytomer_parameters.leaf.prototype.prototype_function == GenericLeafPrototype) {
1873 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(objID_leaf), "object_label", "leaf");
1874 }
1875 this->unique_leaf_prototype_objIDs.at(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier).at(prototype).push_back(objID_leaf);
1876 std::vector<uint> petiolule_UUIDs = context_ptr->filterPrimitivesByData(context_ptr->getObjectPrimitiveUUIDs(objID_leaf), "object_label", "petiolule");
1877 context_ptr->setPrimitiveColor(petiolule_UUIDs, phytomer_ptr->phytomer_parameters.petiole.color);
1878 context_ptr->hideObject(objID_leaf);
1879 }
1880 }
1881 }
1882 }
1883
1884 for (int petiole = 0; petiole < leaf_scale.size(); petiole++) {
1885 int leaves_per_petiole = leaf_scale[petiole].size();
1886
1887 // Get CORRECT petiole axis (after geometry restoration)
1888 vec3 petiole_tip_axis = phytomer_ptr->getPetioleAxisVector(1.f, petiole);
1889 vec3 internode_axis = phytomer_ptr->getInternodeAxisVector(1.f);
1890
1891 // Resize leaf data structures
1892 phytomer_ptr->leaf_objIDs[petiole].resize(leaves_per_petiole);
1893 phytomer_ptr->leaf_bases[petiole].resize(leaves_per_petiole);
1894 phytomer_ptr->leaf_rotation[petiole].resize(leaves_per_petiole);
1895
1896 for (int leaf = 0; leaf < leaves_per_petiole; leaf++) {
1897 float ind_from_tip = float(leaf) - float(leaves_per_petiole - 1) / 2.f;
1898
1899 // Copy leaf from prototype (matching constructor logic)
1900 uint objID_leaf;
1901 if (phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes > 0) {
1902 int prototype = context_ptr->randu(0, phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes - 1);
1903 uint uid = phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier;
1904 assert(this->unique_leaf_prototype_objIDs.find(uid) != this->unique_leaf_prototype_objIDs.end());
1905 assert(this->unique_leaf_prototype_objIDs.at(uid).size() > prototype);
1906 assert(this->unique_leaf_prototype_objIDs.at(uid).at(prototype).size() > leaf);
1907 objID_leaf = context_ptr->copyObject(this->unique_leaf_prototype_objIDs.at(uid).at(prototype).at(leaf));
1908 } else {
1909 objID_leaf = phytomer_ptr->phytomer_parameters.leaf.prototype.prototype_function(context_ptr, &phytomer_ptr->phytomer_parameters.leaf.prototype, ind_from_tip);
1910 }
1911
1912 // Scale leaf
1913 vec3 leaf_scale_vec = leaf_scale[petiole][leaf] * make_vec3(1, 1, 1);
1914 context_ptr->scaleObject(objID_leaf, leaf_scale_vec);
1915
1916 // Calculate compound rotation (matching constructor logic)
1917 float compound_rotation = 0;
1918 if (leaves_per_petiole > 1) {
1919 if (leaf == float(leaves_per_petiole - 1) / 2.f) {
1920 compound_rotation = 0; // tip leaf
1921 } else if (leaf < float(leaves_per_petiole - 1) / 2.f) {
1922 compound_rotation = -0.5 * M_PI; // left lateral
1923 } else {
1924 compound_rotation = 0.5 * M_PI; // right lateral
1925 }
1926 }
1927
1928 // Apply saved rotations (matching constructor logic)
1929 float saved_pitch = deg2rad(leaf_pitch[petiole][leaf]);
1930 float saved_yaw = deg2rad(leaf_yaw[petiole][leaf]);
1931 float saved_roll = deg2rad(leaf_roll[petiole][leaf]);
1932
1933 // Roll rotation
1934 float roll_rot = 0;
1935 if (leaves_per_petiole == 1) {
1936 int sign = 1; // Simplified, constructor uses shoot_index
1937 roll_rot = (acos_safe(internode_axis.z) - saved_roll) * sign;
1938 } else if (ind_from_tip != 0) {
1939 roll_rot = (asin_safe(petiole_tip_axis.z) + saved_roll) * compound_rotation / std::fabs(compound_rotation);
1940 }
1941 phytomer_ptr->leaf_rotation[petiole][leaf].roll = saved_roll;
1942 context_ptr->rotateObject(objID_leaf, roll_rot, "x");
1943
1944 // Pitch rotation
1945 phytomer_ptr->leaf_rotation[petiole][leaf].pitch = saved_pitch;
1946 float pitch_rot = saved_pitch;
1947 if (ind_from_tip == 0) {
1948 pitch_rot += asin_safe(petiole_tip_axis.z);
1949 }
1950 context_ptr->rotateObject(objID_leaf, -pitch_rot, "y");
1951
1952 // Yaw rotation
1953 if (ind_from_tip != 0) {
1954 phytomer_ptr->leaf_rotation[petiole][leaf].yaw = saved_yaw;
1955 context_ptr->rotateObject(objID_leaf, saved_yaw, "z");
1956 } else {
1957 phytomer_ptr->leaf_rotation[petiole][leaf].yaw = 0;
1958 }
1959
1960 // Rotate to petiole azimuth
1961 context_ptr->rotateObject(objID_leaf, -std::atan2(petiole_tip_axis.y, petiole_tip_axis.x) + compound_rotation, "z");
1962
1963 // Auto-calculate leaf base from petiole geometry (handles compound leaves)
1964 vec3 leaf_base = phytomer_ptr->petiole_vertices[petiole].back(); // Default: petiole tip
1965
1966 int leaves_per_petiole = phytomer_ptr->phytomer_parameters.leaf.leaves_per_petiole.val();
1967 float leaflet_offset_val = clampOffset(leaves_per_petiole, phytomer_ptr->phytomer_parameters.leaf.leaflet_offset.val());
1968
1969 if (leaves_per_petiole > 1 && leaflet_offset_val > 0) {
1970 // Compound leaf: calculate lateral leaflet positions along petiole
1971 int ind_from_tip = leaf - int(floor(float(leaves_per_petiole - 1) / 2.f));
1972 if (ind_from_tip != 0) { // Terminal leaflet stays at tip
1973 float offset = (fabs(ind_from_tip) - 0.5f) * leaflet_offset_val * phytomer_ptr->phytomer_parameters.petiole.length.val();
1974 leaf_base = PlantArchitecture::interpolateTube(phytomer_ptr->petiole_vertices[petiole], 1.f - offset / phytomer_ptr->phytomer_parameters.petiole.length.val());
1975 }
1976 }
1977 context_ptr->translateObject(objID_leaf, leaf_base);
1978
1979 phytomer_ptr->leaf_objIDs[petiole][leaf] = objID_leaf;
1980 phytomer_ptr->leaf_bases[petiole][leaf] = leaf_base;
1981 }
1982 }
1983
1984 // Step 4: Restore saved object data
1985 for (int petiole = 0; petiole < phytomer_ptr->leaf_objIDs.size(); petiole++) {
1986 for (int leaf = 0; leaf < phytomer_ptr->leaf_objIDs[petiole].size(); leaf++) {
1987 uint objID = phytomer_ptr->leaf_objIDs[petiole][leaf];
1988 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objID);
1989
1990 for (const auto &pair: saved_leaf_data_int[petiole][leaf]) {
1991 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
1992 }
1993 for (const auto &pair: saved_leaf_data_uint[petiole][leaf]) {
1994 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
1995 }
1996 for (const auto &pair: saved_leaf_data_float[petiole][leaf]) {
1997 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
1998 }
1999 for (const auto &pair: saved_leaf_data_double[petiole][leaf]) {
2000 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2001 }
2002 for (const auto &pair: saved_leaf_data_vec2[petiole][leaf]) {
2003 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2004 }
2005 for (const auto &pair: saved_leaf_data_vec3[petiole][leaf]) {
2006 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2007 }
2008 for (const auto &pair: saved_leaf_data_vec4[petiole][leaf]) {
2009 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2010 }
2011 for (const auto &pair: saved_leaf_data_int2[petiole][leaf]) {
2012 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2013 }
2014 for (const auto &pair: saved_leaf_data_int3[petiole][leaf]) {
2015 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2016 }
2017 for (const auto &pair: saved_leaf_data_int4[petiole][leaf]) {
2018 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2019 }
2020 for (const auto &pair: saved_leaf_data_string[petiole][leaf]) {
2021 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2022 }
2023 }
2024 }
2025
2026 // Apply floral bud data
2027 if (!floral_bud_data.empty() && floral_bud_data.size() <= phytomer_ptr->petiole_length.size()) {
2028 // Ensure the floral_buds vector is properly sized
2029 phytomer_ptr->floral_buds.resize(floral_bud_data.size());
2030
2031 for (size_t petiole = 0; petiole < floral_bud_data.size(); petiole++) {
2032 phytomer_ptr->floral_buds.at(petiole).resize(floral_bud_data.at(petiole).size());
2033
2034 for (size_t bud = 0; bud < floral_bud_data.at(petiole).size(); bud++) {
2035 const FloralBudData &fbud_data = floral_bud_data.at(petiole).at(bud);
2036
2037 // Create and populate FloralBud struct
2038 FloralBud &fbud = phytomer_ptr->floral_buds.at(petiole).at(bud);
2039 // Note: Do NOT set fbud.state here - setFloralBudState() will set it and create geometry
2040 fbud.parent_index = fbud_data.parent_index;
2041 fbud.bud_index = fbud_data.bud_index;
2042 fbud.isterminal = fbud_data.is_terminal;
2043 fbud.current_fruit_scale_factor = fbud_data.current_fruit_scale_factor;
2044
2045 // Auto-calculate floral bud base_position and base_rotation from parent geometry
2046 // Position: terminal buds attach at internode tip, axillary buds at petiole base
2047 // Rotation: computed from bud_index and number of buds (matches growth algorithm)
2048 if (fbud.isterminal) {
2049 // Terminal buds attach at the tip of this phytomer's internode
2050 fbud.base_position = phytomer_ptr->parent_shoot_ptr->shoot_internode_vertices.back().back();
2051 } else {
2052 // Axillary buds attach at the base of the parent petiole
2053 if (petiole < phytomer_ptr->petiole_vertices.size()) {
2054 fbud.base_position = phytomer_ptr->petiole_vertices.at(petiole).front();
2055 } else {
2056 helios_runtime_error("ERROR (PlantArchitecture::readPlantStructureXML): Floral bud parent_index " + std::to_string(fbud.parent_index) + " exceeds number of petioles.");
2057 }
2058 }
2059
2060 // Auto-calculate base_rotation from bud configuration
2061 // Rotation depends on bud_index and total number of buds per petiole/shoot
2062 uint Nbuds = floral_bud_data.at(petiole).size();
2063 if (fbud.isterminal) {
2064 // Terminal bud rotation formula (from PlantArchitecture.cpp:1243-1249)
2065 float pitch_adjustment = (Nbuds > 1) ? deg2rad(30.f) : 0.f;
2066 float yaw_adjustment = static_cast<float>(fbud.bud_index) * 2.f * M_PI / float(Nbuds);
2067 fbud.base_rotation = make_AxisRotation(pitch_adjustment, yaw_adjustment, 0.f);
2068 } else {
2069 // Axillary bud rotation formula (from PlantArchitecture.cpp:1904-1906)
2070 float pitch_adjustment = static_cast<float>(fbud.bud_index) * 0.1f * M_PI / float(Nbuds);
2071 float yaw_adjustment = -0.25f * M_PI + static_cast<float>(fbud.bud_index) * 0.5f * M_PI / float(Nbuds);
2072 fbud.base_rotation = make_AxisRotation(pitch_adjustment, yaw_adjustment, 0.f);
2073 }
2074
2075 // Set the floral bud state to trigger geometry creation
2076 // This will call updateInflorescence() which uses prototype functions from shoot parameters
2077 // Only rebuild geometry if the shoot has prototype functions defined
2078 BudState desired_state = static_cast<BudState>(fbud_data.bud_state);
2079
2080 if (desired_state != BUD_DORMANT && desired_state != BUD_DEAD && desired_state != BUD_ACTIVE) {
2081 // Apply saved peduncle parameters before creating geometry
2082 if (fbud_data.peduncle_length >= 0) {
2083 phytomer_ptr->phytomer_parameters.peduncle.length = fbud_data.peduncle_length;
2084 }
2085 if (fbud_data.peduncle_radius >= 0) {
2086 phytomer_ptr->phytomer_parameters.peduncle.radius = fbud_data.peduncle_radius;
2087 }
2088 if (fbud_data.peduncle_pitch != 0) {
2089 phytomer_ptr->phytomer_parameters.peduncle.pitch = fbud_data.peduncle_pitch;
2090 }
2091 if (fbud_data.peduncle_roll != 0) {
2092 phytomer_ptr->phytomer_parameters.peduncle.roll = fbud_data.peduncle_roll;
2093 }
2094 if (fbud_data.peduncle_curvature != 0) {
2095 phytomer_ptr->phytomer_parameters.peduncle.curvature = fbud_data.peduncle_curvature;
2096 }
2097
2098 // Apply saved inflorescence parameters before creating geometry
2099 if (fbud_data.flower_offset >= 0) {
2100 phytomer_ptr->phytomer_parameters.inflorescence.flower_offset = fbud_data.flower_offset;
2101 }
2102
2103 if ((desired_state == BUD_FLOWER_CLOSED || desired_state == BUD_FLOWER_OPEN) && phytomer_ptr->phytomer_parameters.inflorescence.flower_prototype_function != nullptr) {
2104 FloralBud &fbud = phytomer_ptr->floral_buds.at(petiole).at(bud);
2105
2106 // Manually set up floral bud state without calling updateInflorescence
2107 context_ptr->deleteObject(fbud.inflorescence_objIDs);
2108 fbud.inflorescence_objIDs.clear();
2109 fbud.inflorescence_bases.clear();
2110 fbud.inflorescence_rotation.clear();
2111
2112 if (phytomer_ptr->build_context_geometry_peduncle) {
2113 context_ptr->deleteObject(fbud.peduncle_objIDs);
2114 fbud.peduncle_objIDs.clear();
2115 }
2116
2117 // Set the state without calling updateInflorescence
2118 fbud.state = desired_state;
2119 fbud.time_counter = 0;
2120
2121 // --- PEDUNCLE GEOMETRY RECONSTRUCTION ---
2122 if (fbud_data.peduncle_length > 0 && fbud_data.peduncle_radius > 0) {
2123
2124 // Compute number of segments
2125 uint Ndiv_peduncle_length = std::max(uint(1), phytomer_ptr->phytomer_parameters.peduncle.length_segments);
2126 uint Ndiv_peduncle_radius = std::max(uint(3), phytomer_ptr->phytomer_parameters.peduncle.radial_subdivisions);
2127
2128 // Initialize arrays
2129 std::vector<vec3> peduncle_vertices_computed(Ndiv_peduncle_length + 1);
2130 std::vector<float> peduncle_radii_computed(Ndiv_peduncle_length + 1);
2131
2132 // Set base position and radius
2133 peduncle_vertices_computed.at(0) = fbud.base_position;
2134 peduncle_radii_computed.at(0) = fbud_data.peduncle_radius;
2135
2136 float dr_peduncle = fbud_data.peduncle_length / float(Ndiv_peduncle_length);
2137
2138 // Compute peduncle axis from parameters
2139 vec3 peduncle_axis_computed;
2140 vec3 peduncle_rotation_axis_computed;
2141
2142 recomputePeduncleOrientationVectors(phytomer_ptr, fbud.parent_index, phytomer_index_in_shoot, deg2rad(fbud_data.peduncle_pitch), deg2rad(fbud_data.peduncle_roll), fbud.base_rotation, peduncle_axis_computed,
2143 peduncle_rotation_axis_computed);
2144
2145 // Use computed value for reconstruction
2146 vec3 peduncle_axis_actual = peduncle_axis_computed;
2147
2148 // Generate vertices with peduncle curvature algorithm
2149 for (int i = 1; i <= Ndiv_peduncle_length; i++) {
2150 if (fabs(fbud_data.peduncle_curvature) > 0) {
2151 float curvature_value = fbud_data.peduncle_curvature;
2152
2153 // Horizontal bending axis perpendicular to peduncle direction
2154 vec3 horizontal_bending_axis = cross(peduncle_axis_actual, make_vec3(0, 0, 1));
2155 float axis_magnitude = horizontal_bending_axis.magnitude();
2156
2157 if (axis_magnitude > 0.001f) {
2158 horizontal_bending_axis = horizontal_bending_axis / axis_magnitude;
2159
2160 float theta_curvature = deg2rad(curvature_value * dr_peduncle);
2161 float theta_from_target = (curvature_value > 0) ? std::acos(std::min(1.0f, std::max(-1.0f, peduncle_axis_actual.z))) : std::acos(std::min(1.0f, std::max(-1.0f, -peduncle_axis_actual.z)));
2162
2163 if (fabs(theta_curvature) >= theta_from_target) {
2164 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2165 } else {
2166 peduncle_axis_actual = rotatePointAboutLine(peduncle_axis_actual, nullorigin, horizontal_bending_axis, theta_curvature);
2167 peduncle_axis_actual.normalize();
2168 }
2169 } else {
2170 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2171 }
2172 }
2173
2174 peduncle_vertices_computed.at(i) = peduncle_vertices_computed.at(i - 1) + dr_peduncle * peduncle_axis_actual;
2175 peduncle_radii_computed.at(i) = fbud_data.peduncle_radius;
2176 }
2177
2178 // Store computed geometry
2179 if (petiole < phytomer_ptr->peduncle_vertices.size()) {
2180 if (phytomer_ptr->peduncle_vertices.at(petiole).size() <= bud) {
2181 phytomer_ptr->peduncle_vertices.at(petiole).resize(bud + 1);
2182 phytomer_ptr->peduncle_radii.at(petiole).resize(bud + 1);
2183 }
2184 phytomer_ptr->peduncle_vertices.at(petiole).at(bud) = peduncle_vertices_computed;
2185 phytomer_ptr->peduncle_radii.at(petiole).at(bud) = peduncle_radii_computed;
2186 }
2187
2188 // Rebuild Context geometry with COMPUTED vertices/radii
2189 if (phytomer_ptr->build_context_geometry_peduncle) {
2190 std::vector<RGBcolor> colors(peduncle_vertices_computed.size(), phytomer_ptr->phytomer_parameters.peduncle.color);
2191 fbud.peduncle_objIDs.push_back(context_ptr->addTubeObject(Ndiv_peduncle_radius, peduncle_vertices_computed, peduncle_radii_computed, colors));
2192 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(fbud.peduncle_objIDs.back()), "object_label", "peduncle");
2193 }
2194 }
2195
2196 // Restore flower geometry using saved rotations (base positions auto-computed)
2197 if (!fbud_data.flower_rotations.empty()) {
2198
2199 // Ensure prototype maps are initialized before creating geometry
2200 ensureInflorescencePrototypesInitialized(phytomer_ptr->phytomer_parameters);
2201
2202 for (size_t i = 0; i < fbud_data.flower_rotations.size(); i++) {
2203 // Get saved base if available (for backward compatibility), otherwise will compute
2204 vec3 flower_base_saved = (i < fbud_data.inflorescence_bases_saved.size()) ? fbud_data.inflorescence_bases_saved.at(i) : make_vec3(0, 0, 0);
2205 float saved_pitch = deg2rad(fbud_data.flower_rotations.at(i).pitch);
2206 float saved_yaw = deg2rad(fbud_data.flower_rotations.at(i).yaw);
2207 float saved_roll = deg2rad(fbud_data.flower_rotations.at(i).roll);
2208 float saved_azimuth = deg2rad(fbud_data.flower_rotations.at(i).azimuth);
2209
2210 // Compute flower base position from parameters
2211 int flowers_per_peduncle = fbud_data.flower_rotations.size();
2212 int petioles_per_internode = phytomer_ptr->phytomer_parameters.petiole.petioles_per_internode;
2213
2214 // Clamp offset and compute position
2215 float flower_offset_clamped = clampOffset(flowers_per_peduncle, fbud_data.flower_offset);
2216 float ind_from_tip_computed = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2217
2218 // Default position: peduncle tip
2219 vec3 flower_base_computed = phytomer_ptr->peduncle_vertices.at(petiole).at(bud).back();
2220
2221 // Compute position along peduncle if offset is non-zero
2222 if (flowers_per_peduncle > 1 && flower_offset_clamped > 0) {
2223 if (ind_from_tip_computed != 0) {
2224 float offset_computed = (ind_from_tip_computed - 0.5f) * flower_offset_clamped * fbud_data.peduncle_length;
2225 float frac_computed = 1.0f;
2226 if (fbud_data.peduncle_length > 0) {
2227 frac_computed = 1.f - offset_computed / fbud_data.peduncle_length;
2228 }
2229 flower_base_computed = interpolateTube(phytomer_ptr->peduncle_vertices.at(petiole).at(bud), frac_computed);
2230 }
2231 }
2232
2233 // Recalculate peduncle_axis from saved peduncle geometry and flower position
2234 float flower_offset_val = fbud_data.flower_offset;
2235 if (flowers_per_peduncle > 2) {
2236 float denom = 0.5f * float(flowers_per_peduncle) - 1.f;
2237 if (flower_offset_val * denom > 1.f) {
2238 flower_offset_val = 1.f / denom;
2239 }
2240 }
2241
2242 float ind_from_tip = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2243 float frac = 1.0f;
2244 if (flowers_per_peduncle > 1 && flower_offset_val > 0 && ind_from_tip != 0) {
2245 float offset = (ind_from_tip - 0.5f) * flower_offset_val * fbud_data.peduncle_length;
2246 if (fbud_data.peduncle_length > 0) {
2247 frac = 1.f - offset / fbud_data.peduncle_length;
2248 }
2249 }
2250 vec3 recalculated_peduncle_axis = phytomer_ptr->getAxisVector(frac, phytomer_ptr->peduncle_vertices.at(petiole).at(bud));
2251
2252 // Use individual base scale if available, otherwise use default parameter
2253 float scale_factor;
2254 if (i < fbud_data.flower_base_scales.size() && fbud_data.flower_base_scales.at(i) >= 0) {
2255 scale_factor = fbud_data.flower_base_scales.at(i);
2256 } else {
2257 scale_factor = phytomer_ptr->phytomer_parameters.inflorescence.flower_prototype_scale.val();
2258 }
2259
2260 // Determine if flower is open
2261 bool is_open_flower = (desired_state == BUD_FLOWER_OPEN);
2262
2263 // Create flower geometry with computed base, saved rotations, and recalculated peduncle axis
2264 phytomer_ptr->createInflorescenceGeometry(fbud, flower_base_computed, recalculated_peduncle_axis, saved_pitch, saved_roll, saved_azimuth, saved_yaw, scale_factor, is_open_flower);
2265 }
2266 }
2267
2268 } else if (desired_state == BUD_FRUITING && phytomer_ptr->phytomer_parameters.inflorescence.fruit_prototype_function != nullptr) {
2269 FloralBud &fbud = phytomer_ptr->floral_buds.at(petiole).at(bud);
2270
2271 // Manually set up floral bud state without calling updateInflorescence
2272 context_ptr->deleteObject(fbud.inflorescence_objIDs);
2273 fbud.inflorescence_objIDs.clear();
2274 fbud.inflorescence_bases.clear();
2275 fbud.inflorescence_rotation.clear();
2276
2277 if (phytomer_ptr->build_context_geometry_peduncle) {
2278 context_ptr->deleteObject(fbud.peduncle_objIDs);
2279 fbud.peduncle_objIDs.clear();
2280 }
2281
2282 fbud.state = desired_state;
2283 fbud.time_counter = 0;
2284
2285 // --- PEDUNCLE GEOMETRY RECONSTRUCTION ---
2286 if (fbud_data.peduncle_length > 0 && fbud_data.peduncle_radius > 0) {
2287
2288 // Compute number of segments
2289 uint Ndiv_peduncle_length = std::max(uint(1), phytomer_ptr->phytomer_parameters.peduncle.length_segments);
2290 uint Ndiv_peduncle_radius = std::max(uint(3), phytomer_ptr->phytomer_parameters.peduncle.radial_subdivisions);
2291
2292 // Initialize arrays
2293 std::vector<vec3> peduncle_vertices_computed(Ndiv_peduncle_length + 1);
2294 std::vector<float> peduncle_radii_computed(Ndiv_peduncle_length + 1);
2295
2296 // Set base position and radius
2297 peduncle_vertices_computed.at(0) = fbud.base_position;
2298 peduncle_radii_computed.at(0) = fbud_data.peduncle_radius;
2299
2300 float dr_peduncle = fbud_data.peduncle_length / float(Ndiv_peduncle_length);
2301
2302 // Compute peduncle axis from parameters
2303 vec3 peduncle_axis_computed;
2304 vec3 peduncle_rotation_axis_computed;
2305
2306 recomputePeduncleOrientationVectors(phytomer_ptr, fbud.parent_index, phytomer_index_in_shoot, deg2rad(fbud_data.peduncle_pitch), deg2rad(fbud_data.peduncle_roll), fbud.base_rotation, peduncle_axis_computed,
2307 peduncle_rotation_axis_computed);
2308
2309 // Use computed value for reconstruction
2310 vec3 peduncle_axis_actual = peduncle_axis_computed;
2311
2312 // Generate vertices with peduncle curvature algorithm
2313 for (int i = 1; i <= Ndiv_peduncle_length; i++) {
2314 if (fabs(fbud_data.peduncle_curvature) > 0) {
2315 float curvature_value = fbud_data.peduncle_curvature;
2316
2317 // Horizontal bending axis perpendicular to peduncle direction
2318 vec3 horizontal_bending_axis = cross(peduncle_axis_actual, make_vec3(0, 0, 1));
2319 float axis_magnitude = horizontal_bending_axis.magnitude();
2320
2321 if (axis_magnitude > 0.001f) {
2322 horizontal_bending_axis = horizontal_bending_axis / axis_magnitude;
2323
2324 float theta_curvature = deg2rad(curvature_value * dr_peduncle);
2325 float theta_from_target = (curvature_value > 0) ? std::acos(std::min(1.0f, std::max(-1.0f, peduncle_axis_actual.z))) : std::acos(std::min(1.0f, std::max(-1.0f, -peduncle_axis_actual.z)));
2326
2327 if (fabs(theta_curvature) >= theta_from_target) {
2328 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2329 } else {
2330 peduncle_axis_actual = rotatePointAboutLine(peduncle_axis_actual, nullorigin, horizontal_bending_axis, theta_curvature);
2331 peduncle_axis_actual.normalize();
2332 }
2333 } else {
2334 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2335 }
2336 }
2337
2338 peduncle_vertices_computed.at(i) = peduncle_vertices_computed.at(i - 1) + dr_peduncle * peduncle_axis_actual;
2339 peduncle_radii_computed.at(i) = fbud_data.peduncle_radius;
2340 }
2341
2342 // Store computed geometry
2343 if (petiole < phytomer_ptr->peduncle_vertices.size()) {
2344 if (phytomer_ptr->peduncle_vertices.at(petiole).size() <= bud) {
2345 phytomer_ptr->peduncle_vertices.at(petiole).resize(bud + 1);
2346 phytomer_ptr->peduncle_radii.at(petiole).resize(bud + 1);
2347 }
2348 phytomer_ptr->peduncle_vertices.at(petiole).at(bud) = peduncle_vertices_computed;
2349 phytomer_ptr->peduncle_radii.at(petiole).at(bud) = peduncle_radii_computed;
2350 }
2351
2352 // Rebuild Context geometry with COMPUTED vertices/radii
2353 if (phytomer_ptr->build_context_geometry_peduncle) {
2354 std::vector<RGBcolor> colors(peduncle_vertices_computed.size(), phytomer_ptr->phytomer_parameters.peduncle.color);
2355 fbud.peduncle_objIDs.push_back(context_ptr->addTubeObject(Ndiv_peduncle_radius, peduncle_vertices_computed, peduncle_radii_computed, colors));
2356 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(fbud.peduncle_objIDs.back()), "object_label", "peduncle");
2357 }
2358 }
2359
2360 // Restore fruit geometry using saved rotations (base positions auto-computed)
2361 if (!fbud_data.flower_rotations.empty()) {
2362
2363 // Ensure prototype maps are initialized before creating geometry
2364 ensureInflorescencePrototypesInitialized(phytomer_ptr->phytomer_parameters);
2365
2366 for (size_t i = 0; i < fbud_data.flower_rotations.size(); i++) {
2367 // Get saved base if available (for backward compatibility), otherwise will compute
2368 vec3 fruit_base_saved = (i < fbud_data.inflorescence_bases_saved.size()) ? fbud_data.inflorescence_bases_saved.at(i) : make_vec3(0, 0, 0);
2369 float saved_pitch = deg2rad(fbud_data.flower_rotations.at(i).pitch);
2370 float saved_yaw = deg2rad(fbud_data.flower_rotations.at(i).yaw);
2371 float saved_roll = deg2rad(fbud_data.flower_rotations.at(i).roll);
2372 float saved_azimuth = deg2rad(fbud_data.flower_rotations.at(i).azimuth);
2373
2374 // Compute fruit base position from parameters
2375 int flowers_per_peduncle = fbud_data.flower_rotations.size();
2376 int petioles_per_internode = phytomer_ptr->phytomer_parameters.petiole.petioles_per_internode;
2377
2378 // Clamp offset and compute position
2379 float flower_offset_clamped = clampOffset(flowers_per_peduncle, fbud_data.flower_offset);
2380 float ind_from_tip_computed = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2381
2382 // Default position: peduncle tip
2383 vec3 fruit_base_computed = phytomer_ptr->peduncle_vertices.at(petiole).at(bud).back();
2384
2385 // Compute position along peduncle if offset is non-zero
2386 if (flowers_per_peduncle > 1 && flower_offset_clamped > 0) {
2387 if (ind_from_tip_computed != 0) {
2388 float offset_computed = (ind_from_tip_computed - 0.5f) * flower_offset_clamped * fbud_data.peduncle_length;
2389 float frac_computed = 1.0f;
2390 if (fbud_data.peduncle_length > 0) {
2391 frac_computed = 1.f - offset_computed / fbud_data.peduncle_length;
2392 }
2393 fruit_base_computed = interpolateTube(phytomer_ptr->peduncle_vertices.at(petiole).at(bud), frac_computed);
2394 }
2395 }
2396
2397 // Verification complete - computation matches saved values within tolerance
2398 // (Verification code removed after Phase 1 testing confirmed correctness)
2399
2400 // Recalculate peduncle_axis from saved peduncle geometry and fruit position
2401 float flower_offset_val = fbud_data.flower_offset;
2402 if (flowers_per_peduncle > 2) {
2403 float denom = 0.5f * float(flowers_per_peduncle) - 1.f;
2404 if (flower_offset_val * denom > 1.f) {
2405 flower_offset_val = 1.f / denom;
2406 }
2407 }
2408
2409 float ind_from_tip = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2410 float frac = 1.0f;
2411 if (flowers_per_peduncle > 1 && flower_offset_val > 0 && ind_from_tip != 0) {
2412 float offset = (ind_from_tip - 0.5f) * flower_offset_val * fbud_data.peduncle_length;
2413 if (fbud_data.peduncle_length > 0) {
2414 frac = 1.f - offset / fbud_data.peduncle_length;
2415 }
2416 }
2417 vec3 recalculated_peduncle_axis = phytomer_ptr->getAxisVector(frac, phytomer_ptr->peduncle_vertices.at(petiole).at(bud));
2418
2419 // Use individual base scale if available, then apply growth scaling
2420 float base_fruit_scale;
2421 if (i < fbud_data.flower_base_scales.size() && fbud_data.flower_base_scales.at(i) >= 0) {
2422 base_fruit_scale = fbud_data.flower_base_scales.at(i);
2423 } else {
2424 base_fruit_scale = phytomer_ptr->phytomer_parameters.inflorescence.fruit_prototype_scale.val();
2425 }
2426 float scale_factor = base_fruit_scale * fbud_data.current_fruit_scale_factor;
2427
2428 // Create fruit geometry with computed base, saved rotations, and recalculated peduncle axis
2429 phytomer_ptr->createInflorescenceGeometry(fbud, fruit_base_computed, recalculated_peduncle_axis, saved_pitch, saved_roll, saved_azimuth, saved_yaw, scale_factor, false);
2430 }
2431 }
2432 }
2433 } else {
2434 // For states that don't create geometry (DORMANT, DEAD, ACTIVE), just set the state
2435 fbud.state = desired_state;
2436 }
2437 }
2438 }
2439 }
2440
2441 phytomer_count++;
2442 } // phytomers
2443
2444 } // shoots
2445
2446 } // plant instances
2447
2448 if (!quiet) {
2449 std::cout << "done." << std::endl;
2450 }
2451 return plantIDs;
2452}