1.3.72
 
Loading...
Searching...
No Matches
InputOutput.cpp
Go to the documentation of this file.
1
16#include "PlantArchitecture.h"
17
18using namespace helios;
19
20static void renameAutoMaterial(helios::Context *context_ptr, uint objID, const std::string &desired_base_name) {
21 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objID);
22 if (UUIDs.empty()) return;
23
24 std::string current_label = context_ptr->getPrimitiveMaterialLabel(UUIDs.front());
25 if (current_label.substr(0, 7) != "__auto_") return;
26
27 if (!context_ptr->doesMaterialExist(desired_base_name)) {
28 context_ptr->renameMaterial(current_label, desired_base_name);
29 } else {
30 uint existing_id = context_ptr->getMaterialIDFromLabel(desired_base_name);
31 uint current_id = context_ptr->getMaterialIDFromLabel(current_label);
32 if (existing_id == current_id) return;
33
34 int suffix = 1;
35 std::string candidate;
36 do {
37 candidate = desired_base_name + "_" + std::to_string(suffix++);
38 } while (context_ptr->doesMaterialExist(candidate));
39 context_ptr->renameMaterial(current_label, candidate);
40 }
41}
42
43static void renameAutoMaterial(helios::Context *context_ptr, const std::vector<uint> &objIDs, const std::string &desired_base_name) {
44 for (uint objID : objIDs) {
45 renameAutoMaterial(context_ptr, objID, desired_base_name);
46 }
47}
48
49// Helper function for calculating leaf base positions in compound leaves
50static float clampOffset(int count_per_axis, float offset) {
51 if (count_per_axis > 2) {
52 float denom = 0.5f * float(count_per_axis) - 1.f;
53 if (offset * denom > 1.f) {
54 offset = 1.f / denom;
55 }
56 }
57 return offset;
58}
59
60std::string PlantArchitecture::makeShootString(const std::string &current_string, const std::shared_ptr<Shoot> &shoot, const std::vector<std::shared_ptr<Shoot>> &shoot_tree) const {
61
62 std::string outstring = current_string;
63
64 if (shoot->parent_shoot_ID != -1) {
65 outstring += "[";
66 }
67
68 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)) + "," +
69 std::to_string(shoot->shoot_parameters.gravitropic_curvature.val() /*\todo */) + "," + shoot->shoot_type_label + "}";
70
71 uint node_number = 0;
72 for (auto &phytomer: shoot->phytomers) {
73
74 float length = phytomer->getInternodeLength();
75 float radius = phytomer->getInternodeRadius();
76
77 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)) + ")";
78
79 for (uint petiole = 0; petiole < phytomer->petiole_length.size(); petiole++) {
80
81 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))) + ")";
82
83 //\todo If leaf is compound, just using rotation for the first leaf for now rather than adding multiple 'Leaf()' strings for each leaflet.
84 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)) + "," +
85 std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().yaw)) + "," + std::to_string(rad2deg(phytomer->leaf_rotation.at(petiole).front().roll)) + ")";
86
87 if (shoot->childIDs.find(node_number) != shoot->childIDs.end()) {
88 for (int childID: shoot->childIDs.at(node_number)) {
89 outstring = makeShootString(outstring, shoot_tree.at(childID), shoot_tree);
90 }
91 }
92 }
93
94 node_number++;
95 }
96
97 if (shoot->parent_shoot_ID != -1) {
98 outstring += "]";
99 }
100
101 return outstring;
102}
103
104std::string PlantArchitecture::getPlantString(uint plantID) const {
105
106 auto plant_shoot_tree = &plant_instances.at(plantID).shoot_tree;
107
108 std::string out_string;
109
110 for (auto &shoot: *plant_shoot_tree) {
111 out_string = makeShootString(out_string, shoot, *plant_shoot_tree);
112 }
113
114 return out_string;
115}
116
117void 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) {
118
119 // shoot argument order {}:
120 // 1. shoot base pitch/insertion angle (degrees)
121 // 2. shoot base yaw angle (degrees)
122 // 3. shoot roll angle (degrees)
123 // 4. gravitropic curvature (degrees/meter)
124 // 5. [optional] phytomer parameters string
125
126 size_t pos_shoot_start = 0;
127
128 std::string s_argument = shoot_argument;
129
130 pos_shoot_start = s_argument.find(',');
131 if (pos_shoot_start == std::string::npos) {
132 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
133 }
134 float insertion_angle = std::stof(s_argument.substr(0, pos_shoot_start));
135 s_argument.erase(0, pos_shoot_start + 1);
136 base_rotation.pitch = deg2rad(insertion_angle);
137
138 pos_shoot_start = s_argument.find(',');
139 if (pos_shoot_start == std::string::npos) {
140 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
141 }
142 float shoot_yaw = std::stof(s_argument.substr(0, pos_shoot_start));
143 s_argument.erase(0, pos_shoot_start + 1);
144 base_rotation.yaw = deg2rad(shoot_yaw);
145
146 pos_shoot_start = s_argument.find(',');
147 if (pos_shoot_start == std::string::npos) {
148 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
149 }
150 float shoot_roll = std::stof(s_argument.substr(0, pos_shoot_start));
151 s_argument.erase(0, pos_shoot_start + 1);
152 base_rotation.roll = deg2rad(shoot_roll);
153
154 pos_shoot_start = s_argument.find(',');
155 if (pos_shoot_start == std::string::npos) {
156 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Shoot brackets '{}' does not have the correct number of values given.");
157 }
158 float shoot_curvature = std::stof(s_argument.substr(0, pos_shoot_start));
159 s_argument.erase(0, pos_shoot_start + 1);
160 shoot_parameters.gravitropic_curvature = shoot_curvature;
161
162 if (pos_shoot_start != std::string::npos) { // shoot type argument was given
163 pos_shoot_start = s_argument.find(',');
164 phytomer_label = s_argument.substr(0, pos_shoot_start);
165 s_argument.erase(0, pos_shoot_start + 1);
166 if (phytomer_parameters.find(phytomer_label) == phytomer_parameters.end()) {
167 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Phytomer parameters with label " + phytomer_label + " was not provided to PlantArchitecture::generatePlantFromString().");
168 }
169 shoot_parameters.phytomer_parameters = phytomer_parameters.at(phytomer_label);
170 } else { // shoot type argument not given - use first phytomer parameters in the map
171 phytomer_label = phytomer_parameters.begin()->first;
172 shoot_parameters.phytomer_parameters = phytomer_parameters.begin()->second;
173 }
174}
175
176void PlantArchitecture::parseInternodeArgument(const std::string &internode_argument, float &internode_radius, float &internode_length, PhytomerParameters &phytomer_parameters) {
177
178 // internode argument order Internode():
179 // 1. internode length (m)
180 // 2. internode radius (m)
181 // 3. internode pitch (degrees)
182 // 4. phyllotactic angle (degrees)
183
184 size_t pos_inode_start = 0;
185
186 std::string inode_argument = internode_argument;
187
188 pos_inode_start = inode_argument.find(',');
189 if (pos_inode_start == std::string::npos) {
190 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
191 }
192 internode_length = std::stof(inode_argument.substr(0, pos_inode_start));
193 inode_argument.erase(0, pos_inode_start + 1);
194
195 pos_inode_start = inode_argument.find(',');
196 if (pos_inode_start == std::string::npos) {
197 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
198 }
199 internode_radius = std::stof(inode_argument.substr(0, pos_inode_start));
200 inode_argument.erase(0, pos_inode_start + 1);
201
202 pos_inode_start = inode_argument.find(',');
203 if (pos_inode_start == std::string::npos) {
204 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
205 }
206 float internode_pitch = std::stof(inode_argument.substr(0, pos_inode_start));
207 inode_argument.erase(0, pos_inode_start + 1);
208 phytomer_parameters.internode.pitch = internode_pitch;
209
210 pos_inode_start = inode_argument.find(',');
211 if (pos_inode_start != std::string::npos) {
212 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Internode()' does not have the correct number of values given.");
213 }
214 float internode_phyllotaxis = std::stof(inode_argument.substr(0, pos_inode_start));
215 inode_argument.erase(0, pos_inode_start + 1);
216 phytomer_parameters.internode.phyllotactic_angle = internode_phyllotaxis;
217}
218
219void PlantArchitecture::parsePetioleArgument(const std::string &petiole_argument, PhytomerParameters &phytomer_parameters) {
220
221 // petiole argument order Petiole():
222 // 1. petiole length (m)
223 // 2. petiole radius (m)
224 // 3. petiole pitch (degrees)
225
226 if (petiole_argument.empty()) {
227 phytomer_parameters.petiole.length = 0;
228 return;
229 }
230
231 size_t pos_petiole_start = 0;
232
233 std::string pet_argument = petiole_argument;
234
235 pos_petiole_start = pet_argument.find(',');
236 if (pos_petiole_start == std::string::npos) {
237 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
238 }
239 float petiole_length = std::stof(pet_argument.substr(0, pos_petiole_start));
240 pet_argument.erase(0, pos_petiole_start + 1);
241 phytomer_parameters.petiole.length = petiole_length;
242
243 pos_petiole_start = pet_argument.find(',');
244 if (pos_petiole_start == std::string::npos) {
245 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
246 }
247 float petiole_radius = std::stof(pet_argument.substr(0, pos_petiole_start));
248 pet_argument.erase(0, pos_petiole_start + 1);
249 phytomer_parameters.petiole.radius = petiole_radius;
250
251 pos_petiole_start = pet_argument.find(',');
252 if (pos_petiole_start != std::string::npos) {
253 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Petiole()' does not have the correct number of values given.");
254 }
255 float petiole_pitch = std::stof(pet_argument.substr(0, pos_petiole_start));
256 pet_argument.erase(0, pos_petiole_start + 1);
257 phytomer_parameters.petiole.pitch = petiole_pitch;
258}
259
260void PlantArchitecture::parseLeafArgument(const std::string &leaf_argument, PhytomerParameters &phytomer_parameters) {
261
262 // leaf argument order Leaf():
263 // 1. leaf scale factor
264 // 2. leaf pitch (degrees)
265 // 3. leaf yaw (degrees)
266 // 4. leaf roll (degrees)
267
268 if (leaf_argument.empty()) {
269 phytomer_parameters.leaf.prototype_scale = 0;
270 return;
271 }
272
273 size_t pos_leaf_start = 0;
274
275 std::string l_argument = leaf_argument;
276
277 pos_leaf_start = l_argument.find(',');
278 if (pos_leaf_start == std::string::npos) {
279 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
280 }
281 float leaf_scale = std::stof(l_argument.substr(0, pos_leaf_start));
282 l_argument.erase(0, pos_leaf_start + 1);
283 phytomer_parameters.leaf.prototype_scale = leaf_scale;
284
285 pos_leaf_start = l_argument.find(',');
286 if (pos_leaf_start == std::string::npos) {
287 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
288 }
289 float leaf_pitch = std::stof(l_argument.substr(0, pos_leaf_start));
290 l_argument.erase(0, pos_leaf_start + 1);
291 phytomer_parameters.leaf.pitch = leaf_pitch;
292
293 pos_leaf_start = l_argument.find(',');
294 if (pos_leaf_start == std::string::npos) {
295 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
296 }
297 float leaf_yaw = std::stof(l_argument.substr(0, pos_leaf_start));
298 l_argument.erase(0, pos_leaf_start + 1);
299 phytomer_parameters.leaf.yaw = leaf_yaw;
300
301 pos_leaf_start = l_argument.find(',');
302 if (pos_leaf_start != std::string::npos) {
303 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): 'Leaf()' does not have the correct number of values given.");
304 }
305 float leaf_roll = std::stof(l_argument.substr(0, pos_leaf_start));
306 l_argument.erase(0, pos_leaf_start + 1);
307 phytomer_parameters.leaf.roll = leaf_roll;
308}
309
310size_t findShootClosingBracket(const std::string &lstring) {
311
312 size_t pos_close = std::string::npos;
313 size_t pos_open = lstring.find_first_of('[', 0);
314 if (pos_open == std::string::npos) {
315 return pos_close;
316 }
317
318 size_t pos = pos_open;
319 int count = 1;
320 while (count > 0) {
321 pos++;
322 if (lstring[pos] == '[') {
323 count++;
324 } else if (lstring[pos] == ']') {
325 count--;
326 }
327 if (pos == lstring.length()) {
328 return pos_close;
329 }
330 }
331 if (count == 0) {
332 pos_close = pos;
333 }
334 return pos_close; // Return the position of the closing bracket
335}
336
337void 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) {
338
339 std::string lstring_tobeparsed = LString_shoot;
340
341 size_t pos_inode_start = 0;
342 std::string inode_delimiter = "Internode(";
343 std::string petiole_delimiter = "Petiole(";
344 std::string leaf_delimiter = "Leaf(";
345 bool base_shoot = true;
346 uint baseID;
347 AxisRotation shoot_base_rotation;
348
349 // parse shoot arguments
350 if (LString_shoot.front() != '{') {
351 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}.");
352 }
353 size_t pos_shoot_end = lstring_tobeparsed.find('}');
354 std::string shoot_argument = lstring_tobeparsed.substr(1, pos_shoot_end - 1);
355 std::string phytomer_label;
356 parseShootArgument(shoot_argument, phytomer_parameters, shoot_parameters, shoot_base_rotation, phytomer_label);
357 lstring_tobeparsed.erase(0, pos_shoot_end + 1);
358
359 uint shoot_node_count = 0;
360 while ((pos_inode_start = lstring_tobeparsed.find(inode_delimiter)) != std::string::npos) {
361
362 if (pos_inode_start != 0) {
363 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly.");
364 }
365
366 size_t pos_inode_end = lstring_tobeparsed.find(')');
367 std::string inode_argument = lstring_tobeparsed.substr(pos_inode_start + inode_delimiter.length(), pos_inode_end - pos_inode_start - inode_delimiter.length());
368 float internode_radius = 0;
369 float internode_length = 0;
370 parseInternodeArgument(inode_argument, internode_radius, internode_length, shoot_parameters.phytomer_parameters);
371 lstring_tobeparsed.erase(0, pos_inode_end + 1);
372
373 size_t pos_petiole_start = lstring_tobeparsed.find(petiole_delimiter);
374 size_t pos_petiole_end = lstring_tobeparsed.find(')');
375 std::string petiole_argument;
376 if (pos_petiole_start == 0) {
377 petiole_argument = lstring_tobeparsed.substr(pos_petiole_start + petiole_delimiter.length(), pos_petiole_end - pos_petiole_start - petiole_delimiter.length());
378 } else {
379 petiole_argument = "";
380 }
381 parsePetioleArgument(petiole_argument, shoot_parameters.phytomer_parameters);
382 if (pos_petiole_start == 0) {
383 lstring_tobeparsed.erase(0, pos_petiole_end + 1);
384 }
385
386 size_t pos_leaf_start = lstring_tobeparsed.find(leaf_delimiter);
387 size_t pos_leaf_end = lstring_tobeparsed.find(')');
388 std::string leaf_argument;
389 if (pos_leaf_start == 0) {
390 leaf_argument = lstring_tobeparsed.substr(pos_leaf_start + leaf_delimiter.length(), pos_leaf_end - pos_leaf_start - leaf_delimiter.length());
391 } else {
392 leaf_argument = "";
393 }
394 parseLeafArgument(leaf_argument, shoot_parameters.phytomer_parameters);
395 if (pos_leaf_start == 0) {
396 lstring_tobeparsed.erase(0, pos_leaf_end + 1);
397 }
398
399 // override phytomer creation function
400 shoot_parameters.phytomer_parameters.phytomer_creation_function = nullptr;
401
402 if (base_shoot) { // this is the first phytomer of the shoot
403 // defineShootType("shoot_"+phytomer_label, shoot_parameters);
404 defineShootType(phytomer_label, shoot_parameters); //*testing*
405
406 if (parentID < 0) { // this is the first shoot of the plant
407 // baseID = addBaseStemShoot(plantID, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, "shoot_" + phytomer_label);
408 baseID = addBaseStemShoot(plantID, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, phytomer_label); //*testing*
409 } else { // this is a child of an existing shoot
410 // baseID = addChildShoot(plantID, parentID, parent_node, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, "shoot_" + phytomer_label, 0);
411 baseID = addChildShoot(plantID, parentID, parent_node, 1, shoot_base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, phytomer_label, 0); //*testing*
412 }
413
414 base_shoot = false;
415 } else {
416 appendPhytomerToShoot(plantID, baseID, shoot_parameters.phytomer_parameters, internode_radius, internode_length, 1, 1);
417 }
418
419 while (!lstring_tobeparsed.empty() && lstring_tobeparsed.substr(0, 1) == "[") {
420 size_t pos_shoot_bracket_end = findShootClosingBracket(lstring_tobeparsed);
421 if (pos_shoot_bracket_end == std::string::npos) {
422 helios_runtime_error("ERROR (PlantArchitecture::parseStringShoot): Shoot string is not formatted correctly. Shoots must be closed with a ']'.");
423 }
424 std::string lstring_child = lstring_tobeparsed.substr(1, pos_shoot_bracket_end - 1);
425 parseStringShoot(lstring_child, plantID, (int) baseID, shoot_node_count, phytomer_parameters, shoot_parameters);
426 lstring_tobeparsed.erase(0, pos_shoot_bracket_end + 1);
427 }
428
429 shoot_node_count++;
430 }
431}
432
433uint PlantArchitecture::generatePlantFromString(const std::string &generation_string, const PhytomerParameters &phytomer_parameters) {
434 std::map<std::string, PhytomerParameters> phytomer_parameters_map;
435 phytomer_parameters_map["default"] = phytomer_parameters;
436 return generatePlantFromString(generation_string, phytomer_parameters_map);
437}
438
439uint PlantArchitecture::generatePlantFromString(const std::string &generation_string, const std::map<std::string, PhytomerParameters> &phytomer_parameters) {
440
441 // check that first characters are 'Internode'
442 if (generation_string.front() != '{') {
443 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): First character of string must be '{'");
444 }
445
446 ShootParameters shoot_parameters(context_ptr->getRandomGenerator());
447 shoot_parameters.max_nodes = 200;
448 shoot_parameters.vegetative_bud_break_probability_min = 0;
449 shoot_parameters.vegetative_bud_break_time = 0;
450 shoot_parameters.phyllochron_min = 0;
451
452 // assign default phytomer parameters. This can be changed later if the optional phytomer parameters label is provided in the shoot argument '{}'.
453 if (phytomer_parameters.empty()) {
454 helios_runtime_error("ERROR (PlantArchitecture::generatePlantFromString): Phytomer parameters must be provided.");
455 }
456
457 uint plantID;
458
459 plantID = addPlantInstance(nullorigin, 0);
460
461 size_t pos_first_child_shoot = generation_string.find('[');
462
463 if (pos_first_child_shoot == std::string::npos) {
464 pos_first_child_shoot = generation_string.length();
465 }
466
467 parseStringShoot(generation_string, plantID, -1, 0, phytomer_parameters, shoot_parameters);
468
469
470 return plantID;
471}
472
473void PlantArchitecture::writePlantStructureXML(uint plantID, const std::string &filename) const {
474
475 if (plant_instances.find(plantID) == plant_instances.end()) {
476 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Plant ID " + std::to_string(plantID) + " does not exist.");
477 }
478
479 std::string output_file = filename;
480 if (!validateOutputPath(output_file, {".xml", ".XML"})) {
481 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
482 } else if (getFileName(output_file).empty()) {
483 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.");
484 }
485
486 std::ofstream output_xml(filename);
487
488 if (!output_xml.is_open()) {
489 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureXML): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
490 }
491
492 output_xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
493 output_xml << "<helios>" << std::endl;
494 output_xml << "\t<plant_instance ID=\"" << plantID << "\">" << std::endl;
495
496 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;
497 output_xml << "\t\t<plant_age> " << plant_instances.at(plantID).current_age << " </plant_age>" << std::endl;
498
499 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
500
501 output_xml << "\t\t<shoot ID=\"" << shoot->ID << "\">" << std::endl;
502 output_xml << "\t\t\t<shoot_type_label> " << shoot->shoot_type_label << " </shoot_type_label>" << std::endl;
503 output_xml << "\t\t\t<parent_shoot_ID> " << shoot->parent_shoot_ID << " </parent_shoot_ID>" << std::endl;
504 output_xml << "\t\t\t<parent_node_index> " << shoot->parent_node_index << " </parent_node_index>" << std::endl;
505 output_xml << "\t\t\t<parent_petiole_index> " << shoot->parent_petiole_index << " </parent_petiole_index>" << std::endl;
506 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;
507
508 uint phytomer_index = 0;
509 for (auto &phytomer: shoot->phytomers) {
510
511 output_xml << "\t\t\t<phytomer>" << std::endl;
512 output_xml << "\t\t\t\t<internode>" << std::endl;
513 output_xml << "\t\t\t\t\t<internode_length>" << phytomer->getInternodeLength() << "</internode_length>" << std::endl;
514 output_xml << "\t\t\t\t\t<internode_radius>" << phytomer->getInternodeRadius() << "</internode_radius>" << std::endl;
515 output_xml << "\t\t\t\t\t<internode_pitch>" << rad2deg(phytomer->internode_pitch) << "</internode_pitch>" << std::endl;
516 output_xml << "\t\t\t\t\t<internode_phyllotactic_angle>" << rad2deg(phytomer->internode_phyllotactic_angle) << "</internode_phyllotactic_angle>" << std::endl;
517
518 // Additional parameters for reconstruction from parameters
519 output_xml << "\t\t\t\t\t<internode_length_max>" << phytomer->internode_length_max << "</internode_length_max>" << std::endl;
520
521 // Length segments should match perturbation count
522 uint length_segments = phytomer->internode_curvature_perturbations.size();
523 if (length_segments > 0) {
524 output_xml << "\t\t\t\t\t<internode_length_segments>" << length_segments << "</internode_length_segments>" << std::endl;
525 } else if (phytomer_index < shoot->shoot_internode_vertices.size()) {
526 // Fallback: infer from vertex count (for phytomers with no perturbations)
527 if (phytomer_index == 0) {
528 length_segments = shoot->shoot_internode_vertices[phytomer_index].size() - 1;
529 } else {
530 length_segments = shoot->shoot_internode_vertices[phytomer_index].size();
531 }
532 output_xml << "\t\t\t\t\t<internode_length_segments>" << length_segments << "</internode_length_segments>" << std::endl;
533 }
534
535 // Radial subdivisions - use default from Context geometry (stored during tube creation)
536 // For now, we'll read this from the tube object during reconstruction
537
538 // Save curvature perturbations (semicolon-delimited)
539 if (!phytomer->internode_curvature_perturbations.empty()) {
540 output_xml << "\t\t\t\t\t<curvature_perturbations>";
541 for (size_t i = 0; i < phytomer->internode_curvature_perturbations.size(); i++) {
542 output_xml << phytomer->internode_curvature_perturbations[i];
543 if (i < phytomer->internode_curvature_perturbations.size() - 1)
544 output_xml << ";";
545 }
546 output_xml << "</curvature_perturbations>" << std::endl;
547 }
548
549 // Save yaw perturbations (semicolon-delimited)
550 if (!phytomer->internode_yaw_perturbations.empty()) {
551 output_xml << "\t\t\t\t\t<yaw_perturbations>";
552 for (size_t i = 0; i < phytomer->internode_yaw_perturbations.size(); i++) {
553 output_xml << phytomer->internode_yaw_perturbations[i];
554 if (i < phytomer->internode_yaw_perturbations.size() - 1)
555 output_xml << ";";
556 }
557 output_xml << "</yaw_perturbations>" << std::endl;
558 }
559
560 // Note: internode_vertices and internode_radii are no longer written to XML
561 // Internodes are now reconstructed from parameters (length, pitch, phyllotactic_angle, length_max, segments)
562 // and stochastic state (curvature_perturbations, yaw_perturbations) during XML reading
563
564 // Removed in Phase 3 - internodes now reconstructed from parameters:
565 // if (phytomer_index < shoot->shoot_internode_vertices.size()) {
566 // const auto &vertices = shoot->shoot_internode_vertices[phytomer_index];
567 // output_xml << "\t\t\t\t\t<internode_vertices>";
568 // for (size_t v = 0; v < vertices.size(); v++) {
569 // output_xml << vertices[v].x << " " << vertices[v].y << " " << vertices[v].z;
570 // if (v < vertices.size() - 1) output_xml << ";";
571 // }
572 // output_xml << "</internode_vertices>" << std::endl;
573 // }
574 // if (phytomer_index < shoot->shoot_internode_radii.size()) {
575 // const auto &radii = shoot->shoot_internode_radii[phytomer_index];
576 // output_xml << "\t\t\t\t\t<internode_radii>";
577 // for (size_t r = 0; r < radii.size(); r++) {
578 // output_xml << radii[r];
579 // if (r < radii.size() - 1) output_xml << ";";
580 // }
581 // output_xml << "</internode_radii>" << std::endl;
582 // }
583
584 for (uint petiole = 0; petiole < phytomer->petiole_length.size(); petiole++) {
585
586 output_xml << "\t\t\t\t\t<petiole>" << std::endl;
587 output_xml << "\t\t\t\t\t\t<petiole_length>" << phytomer->petiole_length.at(petiole) << "</petiole_length>" << std::endl;
588 output_xml << "\t\t\t\t\t\t<petiole_radius>" << phytomer->petiole_radii.at(petiole).front() << "</petiole_radius>" << std::endl;
589 output_xml << "\t\t\t\t\t\t<petiole_pitch>" << rad2deg(phytomer->petiole_pitch.at(petiole)) << "</petiole_pitch>" << std::endl;
590 output_xml << "\t\t\t\t\t\t<petiole_curvature>" << phytomer->petiole_curvature.at(petiole) << "</petiole_curvature>" << std::endl;
591 // Note: petiole_base_position is no longer written to XML - it is auto-calculated from the parent internode tip during XML reading
592 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;
593
594 // Bulk parameters for exact petiole reconstruction
595 output_xml << "\t\t\t\t\t\t<petiole_taper>" << phytomer->petiole_taper.at(petiole) << "</petiole_taper>" << std::endl;
596 output_xml << "\t\t\t\t\t\t<petiole_length_segments>" << phytomer->phytomer_parameters.petiole.length_segments << "</petiole_length_segments>" << std::endl;
597 output_xml << "\t\t\t\t\t\t<petiole_radial_subdivisions>" << phytomer->phytomer_parameters.petiole.radial_subdivisions << "</petiole_radial_subdivisions>" << std::endl;
598
599 if (phytomer->leaf_rotation.at(petiole).size() == 1) { // not compound leaf
600 output_xml << "\t\t\t\t\t\t<leaflet_scale>" << 1.0 << "</leaflet_scale>" << std::endl;
601 } else {
602 float tip_ind = floor(float(phytomer->leaf_rotation.at(petiole).size() - 1) / 2.f);
603 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;
604 }
605 output_xml << "\t\t\t\t\t\t<leaflet_offset>" << phytomer->phytomer_parameters.leaf.leaflet_offset.val() << "</leaflet_offset>" << std::endl;
606
607 for (uint leaf = 0; leaf < phytomer->leaf_rotation.at(petiole).size(); leaf++) {
608 output_xml << "\t\t\t\t\t\t<leaf>" << std::endl;
609 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;
610 // Note: leaf_base is no longer written to XML - it is auto-calculated from the petiole geometry and leaflet_offset during XML reading
611 // 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>" <<
612 // std::endl;
613 output_xml << "\t\t\t\t\t\t\t<leaf_pitch>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).pitch) << "</leaf_pitch>" << std::endl;
614 output_xml << "\t\t\t\t\t\t\t<leaf_yaw>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).yaw) << "</leaf_yaw>" << std::endl;
615 output_xml << "\t\t\t\t\t\t\t<leaf_roll>" << rad2deg(phytomer->leaf_rotation.at(petiole).at(leaf).roll) << "</leaf_roll>" << std::endl;
616 output_xml << "\t\t\t\t\t\t</leaf>" << std::endl;
617 }
618
619 // Write floral buds
620 if (petiole < phytomer->floral_buds.size()) {
621 for (uint bud = 0; bud < phytomer->floral_buds.at(petiole).size(); bud++) {
622 const FloralBud &fbud = phytomer->floral_buds.at(petiole).at(bud);
623
624 output_xml << "\t\t\t\t\t\t<floral_bud>" << std::endl;
625
626 // Write bud state and indices
627 output_xml << "\t\t\t\t\t\t\t<bud_state>" << static_cast<int>(fbud.state) << "</bud_state>" << std::endl;
628 output_xml << "\t\t\t\t\t\t\t<parent_index>" << fbud.parent_index << "</parent_index>" << std::endl;
629 output_xml << "\t\t\t\t\t\t\t<bud_index>" << fbud.bud_index << "</bud_index>" << std::endl;
630 output_xml << "\t\t\t\t\t\t\t<is_terminal>" << (fbud.isterminal ? 1 : 0) << "</is_terminal>" << std::endl;
631
632 // Write fruit scale factor
633 output_xml << "\t\t\t\t\t\t\t<current_fruit_scale_factor>" << fbud.current_fruit_scale_factor << "</current_fruit_scale_factor>" << std::endl;
634
635 // Write peduncle parameters
636 // Note: Write the STORED VALUES that were actually used for this peduncle geometry
637 output_xml << "\t\t\t\t\t\t\t<peduncle>" << std::endl;
638 if (petiole < phytomer->peduncle_length.size() && bud < phytomer->peduncle_length.at(petiole).size()) {
639 output_xml << "\t\t\t\t\t\t\t\t<length>" << phytomer->peduncle_length.at(petiole).at(bud) << "</length>" << std::endl;
640 output_xml << "\t\t\t\t\t\t\t\t<radius>" << phytomer->peduncle_radius.at(petiole).at(bud) << "</radius>" << std::endl;
641 output_xml << "\t\t\t\t\t\t\t\t<pitch>" << phytomer->peduncle_pitch.at(petiole).at(bud) << "</pitch>" << std::endl;
642 output_xml << "\t\t\t\t\t\t\t\t<curvature>" << phytomer->peduncle_curvature.at(petiole).at(bud) << "</curvature>" << std::endl;
643 } else {
644 // Fallback to parameter values if stored values not available
645 output_xml << "\t\t\t\t\t\t\t\t<length>" << phytomer->phytomer_parameters.peduncle.length.val() << "</length>" << std::endl;
646 output_xml << "\t\t\t\t\t\t\t\t<radius>" << phytomer->phytomer_parameters.peduncle.radius.val() << "</radius>" << std::endl;
647 output_xml << "\t\t\t\t\t\t\t\t<pitch>" << phytomer->phytomer_parameters.peduncle.pitch.val() << "</pitch>" << std::endl;
648 output_xml << "\t\t\t\t\t\t\t\t<curvature>" << phytomer->phytomer_parameters.peduncle.curvature.val() << "</curvature>" << std::endl;
649 }
650 output_xml << "\t\t\t\t\t\t\t\t<roll>" << phytomer->phytomer_parameters.peduncle.roll.val() << "</roll>" << std::endl;
651
652 output_xml << "\t\t\t\t\t\t\t</peduncle>" << std::endl;
653
654 // Write inflorescence parameters
655 output_xml << "\t\t\t\t\t\t\t<inflorescence>" << std::endl;
656 output_xml << "\t\t\t\t\t\t\t\t<flower_offset>" << phytomer->phytomer_parameters.inflorescence.flower_offset.val() << "</flower_offset>" << std::endl;
657
658 // Write individual flower/fruit positions and rotations
659 for (uint i = 0; i < fbud.inflorescence_bases.size(); i++) {
660 output_xml << "\t\t\t\t\t\t\t\t<flower>" << std::endl;
661 // inflorescence_base is now auto-computed during XML reading - not saved to XML
662 // Save pitch, yaw, roll, and azimuth for this flower/fruit
663 if (i < fbud.inflorescence_rotation.size()) {
664 output_xml << "\t\t\t\t\t\t\t\t\t<flower_pitch>" << rad2deg(fbud.inflorescence_rotation.at(i).pitch) << "</flower_pitch>" << std::endl;
665 output_xml << "\t\t\t\t\t\t\t\t\t<flower_yaw>" << rad2deg(fbud.inflorescence_rotation.at(i).yaw) << "</flower_yaw>" << std::endl;
666 output_xml << "\t\t\t\t\t\t\t\t\t<flower_roll>" << rad2deg(fbud.inflorescence_rotation.at(i).roll) << "</flower_roll>" << std::endl;
667 output_xml << "\t\t\t\t\t\t\t\t\t<flower_azimuth>" << rad2deg(fbud.inflorescence_rotation.at(i).azimuth) << "</flower_azimuth>" << std::endl;
668 }
669 // Save individual base scale for this flower/fruit
670 if (i < fbud.inflorescence_base_scales.size()) {
671 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;
672 }
673 output_xml << "\t\t\t\t\t\t\t\t</flower>" << std::endl;
674 }
675
676 output_xml << "\t\t\t\t\t\t\t</inflorescence>" << std::endl;
677 output_xml << "\t\t\t\t\t\t</floral_bud>" << std::endl;
678 }
679 }
680
681 output_xml << "\t\t\t\t\t</petiole>" << std::endl;
682 }
683 output_xml << "\t\t\t\t</internode>" << std::endl;
684 output_xml << "\t\t\t</phytomer>" << std::endl;
685
686 phytomer_index++;
687 }
688 output_xml << "\t\t</shoot>" << std::endl;
689 }
690 output_xml << "\t</plant_instance>" << std::endl;
691 output_xml << "</helios>" << std::endl;
692 output_xml.close();
693}
694
695void PlantArchitecture::writeQSMCylinderFile(uint plantID, const std::string &filename) const {
696
697 if (plant_instances.find(plantID) == plant_instances.end()) {
698 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Plant ID " + std::to_string(plantID) + " does not exist.");
699 }
700
701 std::string output_file = filename;
702 if (!validateOutputPath(output_file, {".txt", ".TXT"})) {
703 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
704 } else if (getFileName(output_file).empty()) {
705 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.");
706 }
707
708 std::ofstream output_qsm(filename);
709
710 if (!output_qsm.is_open()) {
711 helios_runtime_error("ERROR (PlantArchitecture::writeQSMCylinderFile): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
712 }
713
714 // Write header line
715 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;
716
717 const auto &plant = plant_instances.at(plantID);
718
719 // Cylinder ID counter (row number = cylinder ID in TreeQSM format)
720 uint cylinder_id = 1;
721
722 // Maps to track relationships between shoots and cylinders
723 std::map<int, std::vector<uint>> shoot_cylinder_ids; // shoot ID -> cylinder IDs
724 std::map<int, uint> shoot_branch_id; // shoot ID -> branch ID
725 std::map<int, uint> shoot_branch_order; // shoot ID -> branch order
726
727 // Assign branch IDs and orders to shoots
728 uint branch_id_counter = 1;
729 for (const auto &shoot: plant.shoot_tree) {
730 shoot_branch_id[shoot->ID] = branch_id_counter++;
731
732 // Determine branch order based on parent
733 if (shoot->parent_shoot_ID == -1) {
734 // Base shoot is order 0 (trunk)
735 shoot_branch_order[shoot->ID] = 0;
736 } else {
737 // Child shoot has order = parent order + 1
738 shoot_branch_order[shoot->ID] = shoot_branch_order[shoot->parent_shoot_ID] + 1;
739 }
740 }
741
742 // Process each shoot
743 for (const auto &shoot: plant.shoot_tree) {
744
745 // Get shoot properties
746 uint branch_id = shoot_branch_id[shoot->ID];
747 uint branch_order = shoot_branch_order[shoot->ID];
748 uint position_in_branch = 1;
749 // Track the last vertex position for vertex sharing between phytomers
750 helios::vec3 last_vertex_position;
751 bool has_last_vertex = false;
752
753 // Process each phytomer in the shoot
754 for (uint phytomer_idx = 0; phytomer_idx < shoot->phytomers.size(); phytomer_idx++) {
755
756 const auto &vertices = shoot->shoot_internode_vertices[phytomer_idx];
757 const auto &radii = shoot->shoot_internode_radii[phytomer_idx];
758
759 // Handle vertex sharing for single-segment phytomer tubes
760 if (vertices.size() == 1 && has_last_vertex) {
761 // This phytomer has only one vertex - use the last vertex from previous phytomer as start
762 helios::vec3 start = last_vertex_position;
763 helios::vec3 current_end = vertices[0];
764 float current_radius = radii[0];
765
766 // Calculate initial length
767 helios::vec3 axis = current_end - start;
768 float length = axis.magnitude();
769
770 axis = axis / length; // Normalize axis
771
772 // Process this as a single cylinder
773 // Determine parent cylinder ID
774 uint parent_id = 0;
775 if (cylinder_id > 1) {
776 parent_id = cylinder_id - 1; // Parent is previous cylinder
777 }
778
779 // Extension cylinder (next cylinder in same branch) - will be updated later if needed
780 uint extension_id = 0;
781
782 // Write cylinder data
783 output_qsm << std::fixed << std::setprecision(4);
784 output_qsm << current_radius << "\t";
785 output_qsm << length << "\t";
786 output_qsm << start.x << "\t" << start.y << "\t" << start.z << "\t";
787 output_qsm << axis.x << "\t" << axis.y << "\t" << axis.z << "\t";
788 output_qsm << parent_id << "\t";
789 output_qsm << extension_id << "\t";
790 output_qsm << branch_id << "\t";
791 output_qsm << branch_order << "\t";
792 output_qsm << position_in_branch << "\t";
793 output_qsm << "0.0002" << "\t"; // mad (using default value)
794 output_qsm << "1" << "\t"; // SurfCov (using default value)
795 output_qsm << "0" << "\t"; // added flag
796 output_qsm << current_radius << std::endl; // UnmodRadius
797
798 // Store cylinder ID for this shoot
799 shoot_cylinder_ids[shoot->ID].push_back(cylinder_id);
800
801 cylinder_id++;
802 position_in_branch++;
803
804 // Update last vertex for next phytomer
805 last_vertex_position = current_end;
806
807 } else {
808 // Normal processing for phytomers with multiple vertices
809 for (int seg = 0; seg < vertices.size() - 1; seg++) {
810
811 // Start with the current segment
812 helios::vec3 start = vertices[seg];
813 helios::vec3 current_end = vertices[seg + 1];
814 float current_radius = radii[seg];
815
816 // Calculate initial length
817 helios::vec3 axis = current_end - start;
818 float length = axis.magnitude();
819
820 axis = axis / length; // Normalize axis
821
822 // Determine parent cylinder ID
823 uint parent_id = 0;
824 if (cylinder_id > 1) {
825 if (seg == 0 && phytomer_idx == 0 && shoot->parent_shoot_ID != -1) {
826 // First cylinder of child shoot - parent is last cylinder of connection point
827 // For simplicity, using previous cylinder as parent
828 parent_id = cylinder_id - 1;
829 } else if (seg == 0 && phytomer_idx > 0) {
830 // First segment of new phytomer - parent is last segment of previous phytomer
831 parent_id = cylinder_id - 1;
832 } else {
833 // Continuation within phytomer - parent is previous segment
834 parent_id = cylinder_id - 1;
835 }
836 }
837
838 // Extension cylinder (next cylinder in same branch) - will be updated later if needed
839 uint extension_id = 0;
840
841 // Write cylinder data
842 output_qsm << std::fixed << std::setprecision(4);
843 output_qsm << current_radius << "\t";
844 output_qsm << length << "\t";
845 output_qsm << start.x << "\t" << start.y << "\t" << start.z << "\t";
846 output_qsm << axis.x << "\t" << axis.y << "\t" << axis.z << "\t";
847 output_qsm << parent_id << "\t";
848 output_qsm << extension_id << "\t";
849 output_qsm << branch_id << "\t";
850 output_qsm << branch_order << "\t";
851 output_qsm << position_in_branch << "\t";
852 output_qsm << "0.0002" << "\t"; // mad (using default value)
853 output_qsm << "1" << "\t"; // SurfCov (using default value)
854 output_qsm << "0" << "\t"; // added flag
855 output_qsm << current_radius << std::endl; // UnmodRadius
856
857 // Store cylinder ID for this shoot
858 shoot_cylinder_ids[shoot->ID].push_back(cylinder_id);
859
860 cylinder_id++;
861 position_in_branch++;
862 }
863
864 // Update last vertex position for vertex sharing
865 if (vertices.size() >= 2) {
866 last_vertex_position = vertices.back();
867 has_last_vertex = true;
868 } else if (vertices.size() == 1) {
869 // This shouldn't happen in normal processing, but handle it for completeness
870 last_vertex_position = vertices[0];
871 has_last_vertex = true;
872 }
873 }
874 }
875 }
876
877 output_qsm.close();
878}
879
880std::vector<uint> PlantArchitecture::readPlantStructureXML(const std::string &filename, bool quiet) {
881
882 if (!quiet) {
883 std::cout << "Loading plant architecture XML file: " << filename << "..." << std::flush;
884 }
885
886 std::string fn = filename;
887 std::string ext = getFileExtension(filename);
888 if (ext != ".xml" && ext != ".XML") {
889 helios_runtime_error("failed.\n File " + fn + " is not XML format.");
890 }
891
892 std::vector<uint> plantIDs;
893
894 // Using "pugixml" parser. See pugixml.org
895 pugi::xml_document xmldoc;
896
897 // Resolve file path using project-based resolution
898 std::filesystem::path resolved_path = resolveProjectFile(filename);
899 std::string resolved_filename = resolved_path.string();
900
901 // load file
902 pugi::xml_parse_result load_result = xmldoc.load_file(resolved_filename.c_str());
903
904 // error checking
905 if (!load_result) {
906 helios_runtime_error("ERROR (Context::readPlantStructureXML): Could not parse " + std::string(filename) + ":\nError description: " + load_result.description());
907 }
908
909 pugi::xml_node helios = xmldoc.child("helios");
910
911 pugi::xml_node node;
912 std::string node_string;
913
914 if (helios.empty()) {
915 if (!quiet) {
916 std::cout << "failed." << std::endl;
917 }
918 helios_runtime_error("ERROR (Context::readPlantStructureXML): XML file must have tag '<helios> ... </helios>' bounding all other tags.");
919 }
920
921 size_t phytomer_count = 0;
922
923 std::map<int, int> shoot_ID_mapping;
924
925 for (pugi::xml_node plant = helios.child("plant_instance"); plant; plant = plant.next_sibling("plant_instance")) {
926
927 int plantID = std::stoi(plant.attribute("ID").value());
928
929 // base position
930 node_string = "base_position";
931 vec3 base_position = parse_xml_tag_vec3(plant.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
932
933 // plant age
934 node_string = "plant_age";
935 float plant_age = parse_xml_tag_float(plant.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
936
937 plantID = addPlantInstance(base_position, plant_age);
938 plantIDs.push_back(plantID);
939
940 int current_shoot_ID;
941
942 for (pugi::xml_node shoot = plant.child("shoot"); shoot; shoot = shoot.next_sibling("shoot")) {
943
944 int shootID = std::stoi(shoot.attribute("ID").value());
945 bool base_shoot = true;
946
947 // shoot type
948 node_string = "shoot_type_label";
949 std::string shoot_type_label = parse_xml_tag_string(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
950
951 // parent shoot ID
952 node_string = "parent_shoot_ID";
953 int parent_shoot_ID = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
954
955 // parent node index
956 node_string = "parent_node_index";
957 int parent_node_index = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
958
959 // parent petiole index
960 node_string = "parent_petiole_index";
961 int parent_petiole_index = parse_xml_tag_int(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
962
963 // base rotation
964 node_string = "base_rotation";
965 vec3 base_rot = parse_xml_tag_vec3(shoot.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
966 AxisRotation base_rotation(deg2rad(base_rot.x), deg2rad(base_rot.y), deg2rad(base_rot.z));
967
968 for (pugi::xml_node phytomer = shoot.child("phytomer"); phytomer; phytomer = phytomer.next_sibling("phytomer")) {
969
970 pugi::xml_node internode = phytomer.child("internode");
971
972 // internode length
973 node_string = "internode_length";
974 float internode_length = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
975
976 // internode radius
977 node_string = "internode_radius";
978 float internode_radius = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
979
980 // internode pitch
981 node_string = "internode_pitch";
982 float internode_pitch = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
983
984 // internode phyllotactic angle
985 node_string = "internode_phyllotactic_angle";
986 float internode_phyllotactic_angle = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
987
988 // Read new parameters for reconstruction from parameters
989 // internode_length_max
990 float internode_length_max = internode_length; // default to current length
991 node_string = "internode_length_max";
992 if (internode.child(node_string.c_str())) {
993 internode_length_max = parse_xml_tag_float(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
994 }
995
996 // internode_length_segments
997 uint internode_length_segments = 1; // default
998 node_string = "internode_length_segments";
999 if (internode.child(node_string.c_str())) {
1000 internode_length_segments = (uint) parse_xml_tag_int(internode.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1001 }
1002
1003 // curvature_perturbations (semicolon-delimited)
1004 std::vector<float> curvature_perturbations;
1005 node_string = "curvature_perturbations";
1006 if (internode.child(node_string.c_str())) {
1007 std::string perturbs_str = internode.child_value(node_string.c_str());
1008 std::istringstream perturbs_stream(perturbs_str);
1009 std::string pert_str;
1010 while (std::getline(perturbs_stream, pert_str, ';')) {
1011 curvature_perturbations.push_back(std::stof(pert_str));
1012 }
1013 }
1014
1015 // yaw_perturbations (semicolon-delimited)
1016 std::vector<float> yaw_perturbations;
1017 node_string = "yaw_perturbations";
1018 if (internode.child(node_string.c_str())) {
1019 std::string yaw_perturbs_str = internode.child_value(node_string.c_str());
1020 std::istringstream yaw_stream(yaw_perturbs_str);
1021 std::string yaw_str;
1022 while (std::getline(yaw_stream, yaw_str, ';')) {
1023 yaw_perturbations.push_back(std::stof(yaw_str));
1024 }
1025 }
1026
1027 // Read internode vertices if available (optional for backward compatibility - values are ignored, geometry reconstructed from parameters)
1028 std::vector<vec3> internode_vertices;
1029 node_string = "internode_vertices";
1030 if (internode.child(node_string.c_str())) {
1031 std::string vertices_str = internode.child_value(node_string.c_str());
1032 std::istringstream verts_stream(vertices_str);
1033 std::string vertex_str;
1034 while (std::getline(verts_stream, vertex_str, ';')) {
1035 std::istringstream vertex_coords(vertex_str);
1036 float x, y, z;
1037 if (vertex_coords >> x >> y >> z) {
1038 internode_vertices.push_back(make_vec3(x, y, z));
1039 }
1040 }
1041 }
1042
1043 // Read internode radii if available (optional for backward compatibility - values are ignored, geometry reconstructed from parameters)
1044 std::vector<float> internode_radii;
1045 node_string = "internode_radii";
1046 if (internode.child(node_string.c_str())) {
1047 std::string radii_str = internode.child_value(node_string.c_str());
1048 std::istringstream radii_stream(radii_str);
1049 std::string radius_str;
1050 while (std::getline(radii_stream, radius_str, ';')) {
1051 float radius = std::stof(radius_str);
1052 internode_radii.push_back(radius);
1053 }
1054 }
1055
1056 float petiole_length;
1057 float petiole_radius;
1058 float petiole_pitch;
1059 float petiole_curvature;
1060 float current_leaf_scale_factor_value;
1061 float leaflet_scale;
1062 float leaflet_offset;
1063 std::vector<float> petiole_lengths; // actual length of each petiole within internode
1064 std::vector<float> petiole_radii_values; // actual radius of each petiole within internode
1065 std::vector<float> petiole_pitches; // pitch of each petiole within internode
1066 std::vector<float> petiole_curvatures; // curvature of each petiole within internode
1067 std::vector<vec3> petiole_base_positions; // actual base position of each petiole within internode
1068 std::vector<float> current_leaf_scale_factors; // scale factor of each petiole within internode
1069 std::vector<float> petiole_tapers; // taper value for each petiole
1070 std::vector<uint> petiole_length_segments_all; // number of segments for each petiole
1071 std::vector<uint> petiole_radial_subdivisions_all; // radial subdivisions for each petiole
1072 std::vector<std::vector<vec3>> saved_leaf_bases_all_petioles; // saved leaf attachment positions for each petiole (if saved in XML)
1073 std::vector<std::vector<float>> leaf_scale; // first index is petiole within internode; second index is leaf within petiole
1074 std::vector<std::vector<float>> leaf_pitch;
1075 std::vector<std::vector<float>> leaf_yaw;
1076 std::vector<std::vector<float>> leaf_roll;
1077
1078 // Floral bud data structures
1079 struct FloralBudData {
1080 int bud_state;
1081 uint parent_index;
1082 uint bud_index;
1083 bool is_terminal;
1084 float current_fruit_scale_factor;
1085 std::vector<vec3> inflorescence_bases_saved; // saved attachment points on peduncle
1086 std::vector<vec3> flower_positions; // saved flower/fruit center positions
1087 std::vector<AxisRotation> flower_rotations; // pitch, yaw, roll for each flower/fruit
1088 std::vector<float> flower_base_scales; // individual base scale for each flower/fruit
1089 // Inflorescence parameters
1090 float flower_offset = -1;
1091 // Peduncle parameters (actual sampled values)
1092 float peduncle_length = -1;
1093 float peduncle_radius = -1;
1094 float peduncle_pitch = 0;
1095 float peduncle_roll = 0;
1096 float peduncle_curvature = 0;
1097 };
1098 std::vector<std::vector<FloralBudData>> floral_bud_data; // first index is petiole within internode; second index is bud within petiole
1099 for (pugi::xml_node petiole = internode.child("petiole"); petiole; petiole = petiole.next_sibling("petiole")) {
1100
1101 // petiole length
1102 node_string = "petiole_length";
1103 petiole_length = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1104
1105 // petiole radius
1106 node_string = "petiole_radius";
1107 petiole_radius = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1108
1109 // petiole pitch
1110 node_string = "petiole_pitch";
1111 petiole_pitch = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1112
1113 // petiole curvature
1114 node_string = "petiole_curvature";
1115 petiole_curvature = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1116
1117 // petiole base position (optional for backward compatibility)
1118 vec3 petiole_base_pos = nullorigin;
1119 node_string = "petiole_base_position";
1120 if (petiole.child(node_string.c_str())) {
1121 petiole_base_pos = parse_xml_tag_vec3(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1122 }
1123
1124 // current leaf scale factor
1125 node_string = "current_leaf_scale_factor";
1126 current_leaf_scale_factor_value = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1127
1128 // petiole taper
1129 node_string = "petiole_taper";
1130 float petiole_taper = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1131
1132 // petiole length segments
1133 node_string = "petiole_length_segments";
1134 uint length_segments = (uint) parse_xml_tag_int(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1135
1136 // petiole radial subdivisions
1137 node_string = "petiole_radial_subdivisions";
1138 uint radial_subdivisions = (uint) parse_xml_tag_int(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1139
1140 // leaflet scale factor
1141 node_string = "leaflet_scale";
1142 leaflet_scale = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1143
1144 // leaflet offset (optional for backward compatibility with old XML files)
1145 node_string = "leaflet_offset";
1146 if (petiole.child(node_string.c_str())) {
1147 leaflet_offset = parse_xml_tag_float(petiole.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1148 } else {
1149 leaflet_offset = 0.f; // Default for backward compatibility
1150 }
1151
1152 // Store petiole properties in vectors
1153 petiole_lengths.push_back(petiole_length);
1154 petiole_radii_values.push_back(petiole_radius);
1155 petiole_pitches.push_back(petiole_pitch);
1156 petiole_curvatures.push_back(petiole_curvature);
1157 current_leaf_scale_factors.push_back(current_leaf_scale_factor_value);
1158 petiole_base_positions.push_back(petiole_base_pos);
1159 petiole_tapers.push_back(petiole_taper);
1160 petiole_length_segments_all.push_back(length_segments);
1161 petiole_radial_subdivisions_all.push_back(radial_subdivisions);
1162
1163 leaf_scale.resize(leaf_scale.size() + 1);
1164 leaf_pitch.resize(leaf_pitch.size() + 1);
1165 leaf_yaw.resize(leaf_yaw.size() + 1);
1166 leaf_roll.resize(leaf_roll.size() + 1);
1167 floral_bud_data.resize(floral_bud_data.size() + 1); // Always resize to stay in sync with leaf vectors
1168
1169 std::vector<vec3> saved_leaf_bases; // Saved leaf attachment positions
1170
1171 for (pugi::xml_node leaf = petiole.child("leaf"); leaf; leaf = leaf.next_sibling("leaf")) {
1172
1173 // leaf scale factor
1174 node_string = "leaf_scale";
1175 leaf_scale.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1176
1177 // leaf base position (optional for backward compatibility - value is ignored, auto-calculated below)
1178 node_string = "leaf_base";
1179 if (leaf.child(node_string.c_str())) {
1180 std::string base_str = leaf.child_value(node_string.c_str());
1181 std::istringstream base_stream(base_str);
1182 float x, y, z;
1183 if (base_stream >> x >> y >> z) {
1184 saved_leaf_bases.push_back(make_vec3(x, y, z));
1185 }
1186 }
1187
1188 // leaf pitch
1189 node_string = "leaf_pitch";
1190 leaf_pitch.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1191
1192 // leaf yaw
1193 node_string = "leaf_yaw";
1194 leaf_yaw.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1195
1196 // leaf roll
1197 node_string = "leaf_roll";
1198 leaf_roll.back().push_back(parse_xml_tag_float(leaf.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML"));
1199 }
1200
1201 // Store saved_leaf_bases for this petiole
1202 saved_leaf_bases_all_petioles.push_back(saved_leaf_bases);
1203
1204 // Read floral buds (if present)
1205 for (pugi::xml_node floral_bud = petiole.child("floral_bud"); floral_bud; floral_bud = floral_bud.next_sibling("floral_bud")) {
1206
1207 FloralBudData fbud_data;
1208
1209 // Read bud state
1210 node_string = "bud_state";
1211 fbud_data.bud_state = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1212
1213 // Read parent index
1214 node_string = "parent_index";
1215 fbud_data.parent_index = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1216
1217 // Read bud index
1218 node_string = "bud_index";
1219 fbud_data.bud_index = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1220
1221 // Read is_terminal
1222 node_string = "is_terminal";
1223 int is_terminal = parse_xml_tag_int(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1224 fbud_data.is_terminal = (is_terminal != 0);
1225
1226 // Read current fruit scale factor
1227 node_string = "current_fruit_scale_factor";
1228 fbud_data.current_fruit_scale_factor = parse_xml_tag_float(floral_bud.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1229
1230 // Read peduncle parameters (if present)
1231 pugi::xml_node peduncle = floral_bud.child("peduncle");
1232 if (peduncle) {
1233 node_string = "length";
1234 if (peduncle.child(node_string.c_str())) {
1235 fbud_data.peduncle_length = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1236 }
1237 node_string = "radius";
1238 if (peduncle.child(node_string.c_str())) {
1239 fbud_data.peduncle_radius = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1240 }
1241 node_string = "pitch";
1242 if (peduncle.child(node_string.c_str())) {
1243 fbud_data.peduncle_pitch = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1244 }
1245 node_string = "roll";
1246 if (peduncle.child(node_string.c_str())) {
1247 fbud_data.peduncle_roll = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1248 }
1249 node_string = "curvature";
1250 if (peduncle.child(node_string.c_str())) {
1251 fbud_data.peduncle_curvature = parse_xml_tag_float(peduncle.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1252 }
1253 }
1254
1255 // Read inflorescence parameters and flower positions (if present)
1256 pugi::xml_node inflorescence = floral_bud.child("inflorescence");
1257 if (inflorescence) {
1258 // Read inflorescence parameters
1259 node_string = "flower_offset";
1260 if (inflorescence.child(node_string.c_str())) {
1261 fbud_data.flower_offset = parse_xml_tag_float(inflorescence.child(node_string.c_str()), node_string, "PlantArchitecture::readPlantStructureXML");
1262 }
1263
1264 // Read individual flower/fruit data (base position and rotations)
1265 for (pugi::xml_node flower = inflorescence.child("flower"); flower; flower = flower.next_sibling("flower")) {
1266 // inflorescence_base (optional for backward compatibility - value is ignored, auto-calculated)
1267 pugi::xml_node base_node = flower.child("inflorescence_base");
1268 if (base_node) {
1269 vec3 base = parse_xml_tag_vec3(base_node, "inflorescence_base", "PlantArchitecture::readPlantStructureXML");
1270 fbud_data.inflorescence_bases_saved.push_back(base);
1271 }
1272
1273 // Read pitch, yaw, roll, and azimuth
1274 AxisRotation rotation;
1275 pugi::xml_node pitch_node = flower.child("flower_pitch");
1276 pugi::xml_node yaw_node = flower.child("flower_yaw");
1277 pugi::xml_node roll_node = flower.child("flower_roll");
1278 pugi::xml_node azimuth_node = flower.child("flower_azimuth");
1279
1280 if (pitch_node) {
1281 rotation.pitch = parse_xml_tag_float(pitch_node, "flower_pitch", "PlantArchitecture::readPlantStructureXML");
1282 } else {
1283 rotation.pitch = 0;
1284 }
1285
1286 if (yaw_node) {
1287 rotation.yaw = parse_xml_tag_float(yaw_node, "flower_yaw", "PlantArchitecture::readPlantStructureXML");
1288 } else {
1289 rotation.yaw = 0;
1290 }
1291
1292 if (roll_node) {
1293 rotation.roll = parse_xml_tag_float(roll_node, "flower_roll", "PlantArchitecture::readPlantStructureXML");
1294 } else {
1295 rotation.roll = 0;
1296 }
1297
1298 if (azimuth_node) {
1299 rotation.azimuth = parse_xml_tag_float(azimuth_node, "flower_azimuth", "PlantArchitecture::readPlantStructureXML");
1300 } else {
1301 rotation.azimuth = 0;
1302 }
1303
1304 fbud_data.flower_rotations.push_back(rotation);
1305
1306 // Read individual base scale for this flower/fruit
1307 pugi::xml_node scale_node = flower.child("flower_base_scale");
1308 if (scale_node) {
1309 float base_scale = parse_xml_tag_float(scale_node, "flower_base_scale", "PlantArchitecture::readPlantStructureXML");
1310 fbud_data.flower_base_scales.push_back(base_scale);
1311 } else {
1312 // For backward compatibility with old XML files, use -1 to indicate not set
1313 fbud_data.flower_base_scales.push_back(-1.0f);
1314 }
1315 }
1316 }
1317
1318 floral_bud_data.back().push_back(fbud_data);
1319 }
1320 } // petioles
1321
1322 if (shoot_types.find(shoot_type_label) == shoot_types.end()) {
1323 helios_runtime_error("ERROR (PlantArchitecture::readPlantStructureXML): Shoot type " + shoot_type_label + " not found in shoot types.");
1324 }
1325
1326
1327 ShootParameters shoot_parameters = getCurrentShootParameters(shoot_type_label);
1328
1329 shoot_parameters.phytomer_parameters.phytomer_creation_function = nullptr;
1330
1331 shoot_parameters.phytomer_parameters.internode.pitch = internode_pitch;
1332 shoot_parameters.phytomer_parameters.internode.phyllotactic_angle = internode_phyllotactic_angle;
1333
1334 shoot_parameters.phytomer_parameters.petiole.length = petiole_length;
1335 shoot_parameters.phytomer_parameters.petiole.radius = petiole_radius;
1336 shoot_parameters.phytomer_parameters.petiole.pitch = petiole_pitch;
1337 shoot_parameters.phytomer_parameters.petiole.curvature = petiole_curvature;
1338
1339 shoot_parameters.phytomer_parameters.leaf.prototype_scale = 1.f; // leaf_scale.front().at(tip_ind);
1340 shoot_parameters.phytomer_parameters.leaf.pitch = 0;
1341 shoot_parameters.phytomer_parameters.leaf.yaw = 0;
1342 shoot_parameters.phytomer_parameters.leaf.roll = 0;
1343 shoot_parameters.phytomer_parameters.leaf.leaflet_scale = leaflet_scale;
1344 shoot_parameters.phytomer_parameters.leaf.leaflet_offset = leaflet_offset;
1345
1346 if (base_shoot) {
1347
1348 if (parent_shoot_ID < 0) { // this is the first shoot of the plant
1349 current_shoot_ID = addBaseStemShoot(plantID, 1, base_rotation, internode_radius, internode_length, 1.f, 1.f, 0, shoot_type_label);
1350 shoot_ID_mapping[shootID] = current_shoot_ID;
1351 } else { // this is a child of an existing shoot
1352 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);
1353 shoot_ID_mapping[shootID] = current_shoot_ID;
1354 }
1355
1356 base_shoot = false;
1357 } else {
1358 appendPhytomerToShoot(plantID, current_shoot_ID, shoot_parameters.phytomer_parameters, internode_radius, internode_length, 1, 1);
1359 }
1360
1361 // Get pointer to the newly created phytomer
1362 auto phytomer_ptr = plant_instances.at(plantID).shoot_tree.at(current_shoot_ID)->phytomers.back();
1363
1364 // Restore internode properties from saved values
1365 phytomer_ptr->internode_pitch = deg2rad(internode_pitch);
1366 phytomer_ptr->internode_phyllotactic_angle = deg2rad(internode_phyllotactic_angle);
1367
1368 // Get shoot pointer for internode geometry restoration
1369 auto shoot_ptr = plant_instances.at(plantID).shoot_tree.at(current_shoot_ID);
1370 uint phytomer_index_in_shoot = shoot_ptr->phytomers.size() - 1;
1371
1372 // Helper function to recompute internode orientation vectors from parent phytomer context
1373 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,
1374 helios::vec3 &out_internode_axis_initial, helios::vec3 &out_petiole_rotation_axis, helios::vec3 &out_shoot_bending_axis) {
1375 using helios::make_vec3;
1377 using helios::nullorigin;
1378 using helios::cross;
1379
1380 // Get parent axes
1381 helios::vec3 parent_internode_axis = make_vec3(0, 0, 1);
1382 helios::vec3 parent_petiole_axis = make_vec3(0, -1, 0);
1383
1384 if (phytomer_index_in_shoot > 0) {
1385 auto prev_phytomer = phytomer_ptr->parent_shoot_ptr->phytomers.at(phytomer_index_in_shoot - 1);
1386 parent_internode_axis = prev_phytomer->getInternodeAxisVector(1.0f);
1387 parent_petiole_axis = prev_phytomer->getPetioleAxisVector(0.f, 0);
1388 } else if (phytomer_ptr->parent_shoot_ptr->parent_shoot_ID >= 0) {
1389 int parent_shoot_id = phytomer_ptr->parent_shoot_ptr->parent_shoot_ID;
1390 uint parent_node_index = phytomer_ptr->parent_shoot_ptr->parent_node_index;
1391 uint parent_petiole_index = phytomer_ptr->parent_shoot_ptr->parent_petiole_index;
1392 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1393 auto parent_phytomer = parent_shoot->phytomers.at(parent_node_index);
1394 parent_internode_axis = parent_phytomer->getInternodeAxisVector(1.0f);
1395 parent_petiole_axis = parent_phytomer->getPetioleAxisVector(0.f, parent_petiole_index);
1396 }
1397
1398 helios::vec3 petiole_rotation_axis = cross(parent_internode_axis, parent_petiole_axis);
1399 if (petiole_rotation_axis.magnitude() < 1e-6f) {
1400 petiole_rotation_axis = make_vec3(1, 0, 0);
1401 } else {
1402 petiole_rotation_axis.normalize();
1403 }
1404
1405 helios::vec3 internode_axis = parent_internode_axis;
1406
1407 if (phytomer_index_in_shoot == 0) {
1408 AxisRotation shoot_base_rotation = phytomer_ptr->parent_shoot_ptr->base_rotation;
1409 if (internode_pitch_rad != 0.f) {
1410 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, 0.5f * internode_pitch_rad);
1411 }
1412 if (shoot_base_rotation.roll != 0.f) {
1413 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1414 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1415 }
1416 if (shoot_base_rotation.pitch != 0.f) {
1417 helios::vec3 base_pitch_axis = -1.0f * cross(parent_internode_axis, parent_petiole_axis);
1418 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1419 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1420 }
1421 if (shoot_base_rotation.yaw != 0.f) {
1422 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1423 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1424 }
1425 } else {
1426 if (internode_pitch_rad != 0.f) {
1427 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, -1.25f * internode_pitch_rad);
1428 }
1429 }
1430
1431 helios::vec3 shoot_bending_axis = cross(internode_axis, make_vec3(0, 0, 1));
1432 if (internode_axis == make_vec3(0, 0, 1)) {
1433 shoot_bending_axis = make_vec3(0, 1, 0);
1434 } else {
1435 shoot_bending_axis.normalize();
1436 }
1437
1438 out_internode_axis_initial = internode_axis;
1439 out_petiole_rotation_axis = petiole_rotation_axis;
1440 out_shoot_bending_axis = shoot_bending_axis;
1441 };
1442
1443 // Reconstruct internode geometry from parameters
1444 if (internode_length_segments > 0) {
1445 // Step 1: Compute base position
1446 helios::vec3 internode_base;
1447 if (phytomer_index_in_shoot == 0) {
1448 // First phytomer in this shoot
1449 if (shoot_ptr->parent_shoot_ID < 0) {
1450 // Base shoot: use plant base position from plant instance
1451 internode_base = plant_instances.at(plantID).base_position;
1452 } else {
1453 // Child shoot: get base from SAVED parent internode tip (petioles not reconstructed yet)
1454 // Note: Cannot use parent petiole tip because petioles are reconstructed later in the loop
1455 int parent_shoot_id = shoot_ptr->parent_shoot_ID;
1456 uint parent_node_index = shoot_ptr->parent_node_index;
1457 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1458
1459 // Use the saved internode tip from the parent phytomer
1460 internode_base = parent_shoot->shoot_internode_vertices.at(parent_node_index).back();
1461 }
1462 } else {
1463 // Subsequent phytomers: use previous phytomer's tip
1464 internode_base = shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot - 1].back();
1465 }
1466
1467 // Step 2: Recompute orientation vectors from parent context
1468 helios::vec3 internode_axis_initial;
1469 helios::vec3 petiole_rotation_axis;
1470 helios::vec3 shoot_bending_axis;
1471
1472 recomputeInternodeOrientationVectors_local(phytomer_ptr, phytomer_index_in_shoot, deg2rad(internode_pitch), deg2rad(internode_phyllotactic_angle), internode_axis_initial, petiole_rotation_axis, shoot_bending_axis);
1473
1474 // Step 3: Reconstruct geometry segment-by-segment
1475 std::vector<helios::vec3> reconstructed_vertices(internode_length_segments + 1);
1476 std::vector<float> reconstructed_radii(internode_length_segments + 1);
1477
1478 reconstructed_vertices[0] = internode_base;
1479 reconstructed_radii[0] = internode_radius;
1480
1481 float dr = internode_length / float(internode_length_segments);
1482 float dr_max = internode_length_max / float(internode_length_segments);
1483
1484 helios::vec3 internode_axis = internode_axis_initial;
1485
1486 for (int i = 1; i <= internode_length_segments; i++) {
1487 // Apply gravitropic curvature + SAVED perturbations
1488 if (phytomer_index_in_shoot > 0 && !curvature_perturbations.empty()) {
1489 // Compute curvature factor (lines 1633-1636 in PlantArchitecture.cpp)
1490 float current_curvature_fact = 0.5f - internode_axis.z / 2.0f;
1491 if (internode_axis.z < 0) {
1492 current_curvature_fact *= 2.0f;
1493 }
1494
1495 // Get gravitropic curvature from shoot
1496 float gravitropic_curvature = shoot_ptr->gravitropic_curvature;
1497
1498 // Apply curvature with SAVED perturbation (matches line 1646-1647)
1499 float curvature_angle = deg2rad(gravitropic_curvature * current_curvature_fact * dr_max + curvature_perturbations[i - 1]);
1500 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, shoot_bending_axis, curvature_angle);
1501
1502 // Apply yaw perturbation if available (matches line 1651-1652)
1503 if (!yaw_perturbations.empty() && (i - 1) < yaw_perturbations.size()) {
1504 float yaw_angle = deg2rad(yaw_perturbations[i - 1]);
1505 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, make_vec3(0, 0, 1), yaw_angle);
1506 }
1507 }
1508
1509 // NOTE: Skip collision avoidance and attraction (environment-dependent, lines 1655-1693)
1510
1511 // Position next vertex
1512 reconstructed_vertices[i] = reconstructed_vertices[i - 1] + dr * internode_axis;
1513 reconstructed_radii[i] = internode_radius;
1514 }
1515
1516 // Use reconstructed geometry
1517 shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot] = reconstructed_vertices;
1518 shoot_ptr->shoot_internode_radii[phytomer_index_in_shoot] = reconstructed_radii;
1519 } else if (!internode_vertices.empty() && !internode_radii.empty()) {
1520 // Fallback: use saved geometry if no reconstruction parameters available (backward compatibility)
1521 shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot] = internode_vertices;
1522 shoot_ptr->shoot_internode_radii[phytomer_index_in_shoot] = internode_radii;
1523 }
1524
1525 // Helper function to recompute petiole orientation vectors from parent phytomer context
1526 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,
1527 helios::vec3 &out_petiole_axis_initial, helios::vec3 &out_petiole_rotation_axis) {
1528 using helios::make_vec3;
1530 using helios::nullorigin;
1531 using helios::cross;
1532
1533 // Step 1: Get parent axes for rotation axis computation
1534 helios::vec3 parent_internode_axis = make_vec3(0, 0, 1); // default for base shoot
1535 helios::vec3 parent_petiole_axis = make_vec3(0, -1, 0); // default for base shoot
1536
1537 // Get actual parent axes if available
1538 if (phytomer_index_in_shoot > 0) {
1539 // Previous phytomer in same shoot
1540 auto prev_phytomer = phytomer_ptr->parent_shoot_ptr->phytomers.at(phytomer_index_in_shoot - 1);
1541 parent_internode_axis = prev_phytomer->getInternodeAxisVector(1.0f);
1542 parent_petiole_axis = prev_phytomer->getPetioleAxisVector(0.f, 0);
1543 } else if (phytomer_ptr->parent_shoot_ptr->parent_shoot_ID >= 0) {
1544 // First phytomer of child shoot - get from parent phytomer via shoot_tree
1545 int parent_shoot_id = phytomer_ptr->parent_shoot_ptr->parent_shoot_ID;
1546 uint parent_node_index = phytomer_ptr->parent_shoot_ptr->parent_node_index;
1547 uint parent_petiole_index = phytomer_ptr->parent_shoot_ptr->parent_petiole_index;
1548
1549 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1550 auto parent_phytomer = parent_shoot->phytomers.at(parent_node_index);
1551 parent_internode_axis = parent_phytomer->getInternodeAxisVector(1.0f);
1552 parent_petiole_axis = parent_phytomer->getPetioleAxisVector(0.f, parent_petiole_index);
1553 }
1554
1555 // Step 2: Compute base petiole_rotation_axis from parent axes (matches line 1530)
1556 helios::vec3 petiole_rotation_axis = cross(parent_internode_axis, parent_petiole_axis);
1557 if (petiole_rotation_axis.magnitude() < 1e-6f) {
1558 petiole_rotation_axis = make_vec3(1, 0, 0);
1559 } else {
1560 petiole_rotation_axis.normalize();
1561 }
1562
1563 // Step 3: Compute internode axis with CORRECT order (matches lines 1528-1578)
1564 helios::vec3 internode_axis = parent_internode_axis;
1565 float internode_pitch_rad = phytomer_ptr->internode_pitch;
1566
1567
1568 if (phytomer_index_in_shoot == 0) {
1569 // First phytomer: apply rotations in CORRECT order
1570
1571 // 1. Internode pitch FIRST (line 1539) using UNMODIFIED petiole_rotation_axis
1572 if (internode_pitch_rad != 0.f) {
1573 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, 0.5f * internode_pitch_rad);
1574 }
1575
1576 // 2. THEN apply shoot base rotations to BOTH axes (lines 1548-1564)
1577 AxisRotation shoot_base_rotation = phytomer_ptr->parent_shoot_ptr->base_rotation;
1578
1579 if (shoot_base_rotation.roll != 0.f) {
1580 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1581 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.roll);
1582 }
1583
1584 if (shoot_base_rotation.pitch != 0.f) {
1585 helios::vec3 base_pitch_axis = -1.0f * cross(parent_internode_axis, parent_petiole_axis);
1586 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1587 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, base_pitch_axis, -shoot_base_rotation.pitch);
1588 }
1589
1590 if (shoot_base_rotation.yaw != 0.f) {
1591 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1592 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, parent_internode_axis, shoot_base_rotation.yaw);
1593 }
1594 } else {
1595 // Non-first phytomer: apply internode pitch (scaled by -1.25, line 1576)
1596 if (internode_pitch_rad != 0.f) {
1597 internode_axis = rotatePointAboutLine(internode_axis, nullorigin, petiole_rotation_axis, -1.25f * internode_pitch_rad);
1598 }
1599 }
1600
1601
1602 // Step 5: Start with internode axis (matches line 1739)
1603 helios::vec3 petiole_axis = internode_axis;
1604
1605 // Step 6: Apply petiole pitch rotation (matches line 1753)
1606 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, petiole_rotation_axis, std::abs(petiole_pitch_rad));
1607
1608 // Step 7: Apply phyllotactic rotation (for non-first phytomers, matches line 1758-1760)
1609 if (phytomer_index_in_shoot != 0 && std::abs(internode_phyllotactic_angle_rad) > 0) {
1610 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, internode_axis, internode_phyllotactic_angle_rad);
1611 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, internode_axis, internode_phyllotactic_angle_rad);
1612 }
1613
1614 // Step 8: Apply bud rotation (for multi-petiole phytomers, matches line 1770-1773)
1615 if (petiole_index > 0) {
1616 uint petioles_per_internode = phytomer_ptr->phytomer_parameters.petiole.petioles_per_internode;
1617 float budrot = float(petiole_index) * 2.0f * float(M_PI) / float(petioles_per_internode);
1618 petiole_axis = rotatePointAboutLine(petiole_axis, nullorigin, internode_axis, budrot);
1619 petiole_rotation_axis = rotatePointAboutLine(petiole_rotation_axis, nullorigin, internode_axis, budrot);
1620 }
1621
1622 // Return computed vectors
1623 out_petiole_axis_initial = petiole_axis;
1624 out_petiole_rotation_axis = petiole_rotation_axis;
1625 };
1626
1627 // Lambda to recompute peduncle orientation vectors from parent context
1628 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,
1629 vec3 &out_peduncle_axis_initial, vec3 &out_peduncle_rotation_axis) -> void {
1630 // Step 1: Get parent internode axis at tip (fraction=1.0)
1631 vec3 peduncle_axis = phytomer_ptr->getAxisVector(1.f, phytomer_ptr->getInternodeNodePositions());
1632
1633 // Step 2: Compute inflorescence_bending_axis from parent context
1634 // This logic mirrors PlantArchitecture.cpp lines 976-1009 for parent_internode_axis
1635 // and line 2041 for inflorescence_bending_axis
1636 vec3 parent_internode_axis = make_vec3(0, 0, 1); // default for base shoot
1637
1638 // Get actual parent internode axis if available
1639 if (phytomer_index_in_shoot > 0) {
1640 // Previous phytomer in same shoot
1641 auto prev_phytomer = phytomer_ptr->parent_shoot_ptr->phytomers.at(phytomer_index_in_shoot - 1);
1642 parent_internode_axis = prev_phytomer->getInternodeAxisVector(1.0f);
1643 } else if (phytomer_ptr->parent_shoot_ptr->parent_shoot_ID >= 0) {
1644 // First phytomer of child shoot - get from parent phytomer via shoot_tree
1645 int parent_shoot_id = phytomer_ptr->parent_shoot_ptr->parent_shoot_ID;
1646 uint parent_node_index = phytomer_ptr->parent_shoot_ptr->parent_node_index;
1647
1648 auto &parent_shoot = plant_instances.at(plantID).shoot_tree.at(parent_shoot_id);
1649 auto parent_phytomer = parent_shoot->phytomers.at(parent_node_index);
1650 parent_internode_axis = parent_phytomer->getInternodeAxisVector(1.0f);
1651 }
1652
1653 // Get current phytomer's petiole axis (NOT parent phytomer's petiole axis)
1654 vec3 current_petiole_axis;
1655 if (phytomer_ptr->petiole_vertices.empty()) {
1656 current_petiole_axis = parent_internode_axis;
1657 } else {
1658 // Use actual curved petiole geometry at base (fraction=0.f) to match generation behavior
1659 current_petiole_axis = phytomer_ptr->getPetioleAxisVector(0.f, petiole_index);
1660 }
1661
1662 vec3 inflorescence_bending_axis_actual = cross(parent_internode_axis, current_petiole_axis);
1663 if (inflorescence_bending_axis_actual.magnitude() < 0.001f) {
1664 inflorescence_bending_axis_actual = make_vec3(1, 0, 0);
1665 }
1666 inflorescence_bending_axis_actual.normalize();
1667
1668 // Step 3: Apply peduncle pitch rotation
1669 if (peduncle_pitch_rad != 0.f || base_rotation.pitch != 0.f) {
1670 peduncle_axis = rotatePointAboutLine(peduncle_axis, nullorigin, inflorescence_bending_axis_actual, peduncle_pitch_rad + base_rotation.pitch);
1671 }
1672
1673 // Step 4: Apply azimuthal alignment to parent petiole
1674 vec3 internode_axis = phytomer_ptr->getAxisVector(1.f, phytomer_ptr->getInternodeNodePositions());
1675 vec3 parent_petiole_base_axis = phytomer_ptr->petiole_vertices.empty() ? internode_axis : phytomer_ptr->getPetioleAxisVector(0.f, petiole_index);
1676
1677 float parent_petiole_azimuth = -std::atan2(parent_petiole_base_axis.y, parent_petiole_base_axis.x);
1678 float current_peduncle_azimuth = -std::atan2(peduncle_axis.y, peduncle_axis.x);
1679 float azimuthal_rotation = current_peduncle_azimuth - parent_petiole_azimuth;
1680
1681 peduncle_axis = rotatePointAboutLine(peduncle_axis, nullorigin, internode_axis, azimuthal_rotation);
1682 inflorescence_bending_axis_actual = rotatePointAboutLine(inflorescence_bending_axis_actual, nullorigin, internode_axis, azimuthal_rotation);
1683
1684 // Step 5: Return computed vectors
1685 out_peduncle_axis_initial = peduncle_axis;
1686 out_peduncle_rotation_axis = inflorescence_bending_axis_actual;
1687 };
1688
1689 // Restore petiole properties and reconstruct geometry from bulk parameters
1690 for (size_t p = 0; p < petiole_lengths.size(); p++) {
1691 if (p < phytomer_ptr->petiole_length.size()) {
1692
1693 // Store bulk parameters in phytomer
1694 phytomer_ptr->petiole_length.at(p) = petiole_lengths[p];
1695 phytomer_ptr->petiole_pitch.at(p) = deg2rad(petiole_pitches[p]);
1696 phytomer_ptr->petiole_curvature.at(p) = petiole_curvatures[p];
1697 phytomer_ptr->petiole_taper.at(p) = petiole_tapers[p];
1698 phytomer_ptr->current_leaf_scale_factor.at(p) = current_leaf_scale_factors[p];
1699
1700 // Update phytomer parameters for geometry construction
1701 phytomer_ptr->phytomer_parameters.petiole.length_segments = petiole_length_segments_all[p];
1702 phytomer_ptr->phytomer_parameters.petiole.radial_subdivisions = petiole_radial_subdivisions_all[p];
1703
1704 // Reconstruct petiole vertices and radii using exact construction algorithm
1705 uint Ndiv_petiole_length = std::max(uint(1), petiole_length_segments_all[p]);
1706 uint Ndiv_petiole_radius = std::max(uint(3), petiole_radial_subdivisions_all[p]);
1707
1708 phytomer_ptr->petiole_vertices.at(p).resize(Ndiv_petiole_length + 1);
1709 phytomer_ptr->petiole_radii.at(p).resize(Ndiv_petiole_length + 1);
1710
1711 // Set base vertex and radius
1712 // Auto-calculate petiole base from current phytomer's internode tip
1713 phytomer_ptr->petiole_vertices.at(p).at(0) = shoot_ptr->shoot_internode_vertices[phytomer_index_in_shoot].back();
1714 phytomer_ptr->petiole_radii.at(p).at(0) = petiole_radii_values[p];
1715
1716 // Compute segment length
1717 float dr_petiole = petiole_lengths[p] / float(Ndiv_petiole_length);
1718
1719 // Recompute orientation vectors from parent context
1720 vec3 recomputed_axis;
1721 vec3 recomputed_rotation_axis;
1722
1723 recomputePetioleOrientationVectors(phytomer_ptr,
1724 p, // petiole index
1725 phytomer_index_in_shoot,
1726 phytomer_ptr->petiole_pitch.at(p), // already in radians
1727 phytomer_ptr->internode_phyllotactic_angle, // already in radians
1728 recomputed_axis, recomputed_rotation_axis);
1729
1730 // Validate against saved vectors
1731
1732 // Use RECOMPUTED vectors for reconstruction (not saved ones)
1733 vec3 petiole_axis_actual = recomputed_axis;
1734 vec3 petiole_rotation_axis_actual = recomputed_rotation_axis;
1735
1736 // Create segments with curvature (matches construction algorithm at line 1830-1837)
1737 for (int j = 1; j <= Ndiv_petiole_length; j++) {
1738 // Apply curvature rotation
1739 if (fabs(petiole_curvatures[p]) > 0) {
1740 petiole_axis_actual = rotatePointAboutLine(petiole_axis_actual, nullorigin, petiole_rotation_axis_actual, -deg2rad(petiole_curvatures[p] * dr_petiole));
1741 }
1742
1743 // Position next vertex
1744 phytomer_ptr->petiole_vertices.at(p).at(j) = phytomer_ptr->petiole_vertices.at(p).at(j - 1) + dr_petiole * petiole_axis_actual;
1745
1746 // Apply taper to radius
1747 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));
1748 }
1749
1750 // Rebuild petiole Context geometry
1751 context_ptr->deleteObject(phytomer_ptr->petiole_objIDs[p]);
1752 std::vector<RGBcolor> petiole_colors(phytomer_ptr->petiole_radii[p].size(), phytomer_ptr->phytomer_parameters.petiole.color);
1753 phytomer_ptr->petiole_objIDs[p] = makeTubeFromCones(Ndiv_petiole_radius, phytomer_ptr->petiole_vertices[p], phytomer_ptr->petiole_radii[p], petiole_colors, context_ptr);
1754
1755 // Set primitive data labels
1756 if (!phytomer_ptr->petiole_objIDs[p].empty()) {
1757 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(phytomer_ptr->petiole_objIDs[p]), "object_label", "petiole");
1758 std::string petiole_material_name = plant_instances.at(plantID).plant_name + "_" + shoot_type_label + "_petiole";
1759 renameAutoMaterial(context_ptr, phytomer_ptr->petiole_objIDs[p], petiole_material_name);
1760 }
1761
1762 // Restore saved leaf base positions (already in saved_leaf_bases_all_petioles)
1763 if (p < saved_leaf_bases_all_petioles.size() && !saved_leaf_bases_all_petioles[p].empty()) {
1764 for (size_t leaf = 0; leaf < phytomer_ptr->leaf_bases[p].size(); leaf++) {
1765 if (leaf < saved_leaf_bases_all_petioles[p].size()) {
1766 phytomer_ptr->leaf_bases[p][leaf] = saved_leaf_bases_all_petioles[p][leaf];
1767 }
1768 }
1769 }
1770 }
1771 }
1772
1773 // Delete and recreate leaves with correct petiole/internode axes
1774 // This is necessary because leaves were created with wrong axes before petiole geometry was restored
1775
1776 // Step 1: Save object data from existing leaves
1777 std::vector<std::vector<std::map<std::string, int>>> saved_leaf_data_int;
1778 std::vector<std::vector<std::map<std::string, uint>>> saved_leaf_data_uint;
1779 std::vector<std::vector<std::map<std::string, float>>> saved_leaf_data_float;
1780 std::vector<std::vector<std::map<std::string, double>>> saved_leaf_data_double;
1781 std::vector<std::vector<std::map<std::string, vec2>>> saved_leaf_data_vec2;
1782 std::vector<std::vector<std::map<std::string, vec3>>> saved_leaf_data_vec3;
1783 std::vector<std::vector<std::map<std::string, vec4>>> saved_leaf_data_vec4;
1784 std::vector<std::vector<std::map<std::string, int2>>> saved_leaf_data_int2;
1785 std::vector<std::vector<std::map<std::string, int3>>> saved_leaf_data_int3;
1786 std::vector<std::vector<std::map<std::string, int4>>> saved_leaf_data_int4;
1787 std::vector<std::vector<std::map<std::string, std::string>>> saved_leaf_data_string;
1788
1789 saved_leaf_data_int.resize(phytomer_ptr->leaf_objIDs.size());
1790 saved_leaf_data_uint.resize(phytomer_ptr->leaf_objIDs.size());
1791 saved_leaf_data_float.resize(phytomer_ptr->leaf_objIDs.size());
1792 saved_leaf_data_double.resize(phytomer_ptr->leaf_objIDs.size());
1793 saved_leaf_data_vec2.resize(phytomer_ptr->leaf_objIDs.size());
1794 saved_leaf_data_vec3.resize(phytomer_ptr->leaf_objIDs.size());
1795 saved_leaf_data_vec4.resize(phytomer_ptr->leaf_objIDs.size());
1796 saved_leaf_data_int2.resize(phytomer_ptr->leaf_objIDs.size());
1797 saved_leaf_data_int3.resize(phytomer_ptr->leaf_objIDs.size());
1798 saved_leaf_data_int4.resize(phytomer_ptr->leaf_objIDs.size());
1799 saved_leaf_data_string.resize(phytomer_ptr->leaf_objIDs.size());
1800
1801 for (int petiole = 0; petiole < phytomer_ptr->leaf_objIDs.size(); petiole++) {
1802 saved_leaf_data_int[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1803 saved_leaf_data_uint[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1804 saved_leaf_data_float[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1805 saved_leaf_data_double[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1806 saved_leaf_data_vec2[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1807 saved_leaf_data_vec3[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1808 saved_leaf_data_vec4[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1809 saved_leaf_data_int2[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1810 saved_leaf_data_int3[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1811 saved_leaf_data_int4[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1812 saved_leaf_data_string[petiole].resize(phytomer_ptr->leaf_objIDs[petiole].size());
1813
1814 for (int leaf = 0; leaf < phytomer_ptr->leaf_objIDs[petiole].size(); leaf++) {
1815 uint objID = phytomer_ptr->leaf_objIDs[petiole][leaf];
1816 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objID);
1817 if (!UUIDs.empty()) {
1818 // Get all primitive data labels
1819 std::vector<std::string> data_int = context_ptr->listPrimitiveData(UUIDs.front());
1820 for (const auto &label: data_int) {
1821 HeliosDataType type = context_ptr->getPrimitiveDataType(label.c_str());
1822 if (type == HELIOS_TYPE_INT) {
1823 int value;
1824 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1825 saved_leaf_data_int[petiole][leaf][label] = value;
1826 } else if (type == HELIOS_TYPE_UINT) {
1827 uint value;
1828 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1829 saved_leaf_data_uint[petiole][leaf][label] = value;
1830 } else if (type == HELIOS_TYPE_FLOAT) {
1831 float value;
1832 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1833 saved_leaf_data_float[petiole][leaf][label] = value;
1834 } else if (type == HELIOS_TYPE_DOUBLE) {
1835 double value;
1836 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1837 saved_leaf_data_double[petiole][leaf][label] = value;
1838 } else if (type == HELIOS_TYPE_VEC2) {
1839 vec2 value;
1840 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1841 saved_leaf_data_vec2[petiole][leaf][label] = value;
1842 } else if (type == HELIOS_TYPE_VEC3) {
1843 vec3 value;
1844 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1845 saved_leaf_data_vec3[petiole][leaf][label] = value;
1846 } else if (type == HELIOS_TYPE_VEC4) {
1847 vec4 value;
1848 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1849 saved_leaf_data_vec4[petiole][leaf][label] = value;
1850 } else if (type == HELIOS_TYPE_INT2) {
1851 int2 value;
1852 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1853 saved_leaf_data_int2[petiole][leaf][label] = value;
1854 } else if (type == HELIOS_TYPE_INT3) {
1855 int3 value;
1856 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1857 saved_leaf_data_int3[petiole][leaf][label] = value;
1858 } else if (type == HELIOS_TYPE_INT4) {
1859 int4 value;
1860 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1861 saved_leaf_data_int4[petiole][leaf][label] = value;
1862 } else if (type == HELIOS_TYPE_STRING) {
1863 std::string value;
1864 context_ptr->getPrimitiveData(UUIDs.front(), label.c_str(), value);
1865 saved_leaf_data_string[petiole][leaf][label] = value;
1866 }
1867 }
1868 }
1869 }
1870 }
1871
1872 // Step 2: Delete existing leaves and clear data structures
1873 for (int petiole = 0; petiole < phytomer_ptr->leaf_objIDs.size(); petiole++) {
1874 for (uint objID: phytomer_ptr->leaf_objIDs[petiole]) {
1875 context_ptr->deleteObject(objID);
1876 }
1877 phytomer_ptr->leaf_objIDs[petiole].clear();
1878 phytomer_ptr->leaf_bases[petiole].clear();
1879 phytomer_ptr->leaf_rotation[petiole].clear();
1880 }
1881
1882 // Step 3: Recreate leaves using Phytomer constructor logic with corrected axes
1883 assert(leaf_scale.size() == leaf_pitch.size());
1884 float leaflet_offset_val = 0.f; // Will be set from saved data if available
1885
1886 // Create unique leaf prototypes if they don't exist (matching Phytomer constructor)
1887 if (leaf_scale.size() > 0) {
1888 // Find maximum leaves per petiole across all petioles
1889 int max_leaves_per_petiole = 0;
1890 for (size_t i = 0; i < leaf_scale.size(); i++) {
1891 max_leaves_per_petiole = std::max(max_leaves_per_petiole, (int) leaf_scale[i].size());
1892 }
1893 int leaves_per_petiole = max_leaves_per_petiole;
1894 assert(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier != 0);
1895
1896 if (phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes > 0 &&
1897 this->unique_leaf_prototype_objIDs.find(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier) == this->unique_leaf_prototype_objIDs.end()) {
1898 this->unique_leaf_prototype_objIDs[phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier].resize(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes);
1899 for (int prototype = 0; prototype < phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes; prototype++) {
1900 for (int leaf = 0; leaf < leaves_per_petiole; leaf++) {
1901 float ind_from_tip = float(leaf) - float(leaves_per_petiole - 1) / 2.f;
1902 uint objID_leaf = phytomer_ptr->phytomer_parameters.leaf.prototype.prototype_function(context_ptr, &phytomer_ptr->phytomer_parameters.leaf.prototype, ind_from_tip);
1903 if (phytomer_ptr->phytomer_parameters.leaf.prototype.prototype_function == GenericLeafPrototype) {
1904 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(objID_leaf), "object_label", "leaf");
1905 }
1906 this->unique_leaf_prototype_objIDs.at(phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier).at(prototype).push_back(objID_leaf);
1907 std::string leaf_material_name = plant_instances.at(plantID).plant_name + "_" + shoot_type_label + "_leaf";
1908 renameAutoMaterial(context_ptr, objID_leaf, leaf_material_name);
1909 std::vector<uint> petiolule_UUIDs = context_ptr->filterPrimitivesByData(context_ptr->getObjectPrimitiveUUIDs(objID_leaf), "object_label", "petiolule");
1910 context_ptr->setPrimitiveColor(petiolule_UUIDs, phytomer_ptr->phytomer_parameters.petiole.color);
1911 context_ptr->hideObject(objID_leaf);
1912 }
1913 }
1914 }
1915 }
1916
1917 for (int petiole = 0; petiole < leaf_scale.size(); petiole++) {
1918 int leaves_per_petiole = leaf_scale[petiole].size();
1919
1920 // Get CORRECT petiole axis (after geometry restoration)
1921 vec3 petiole_tip_axis = phytomer_ptr->getPetioleAxisVector(1.f, petiole);
1922 vec3 internode_axis = phytomer_ptr->getInternodeAxisVector(1.f);
1923
1924 // Resize leaf data structures
1925 phytomer_ptr->leaf_objIDs[petiole].resize(leaves_per_petiole);
1926 phytomer_ptr->leaf_bases[petiole].resize(leaves_per_petiole);
1927 phytomer_ptr->leaf_rotation[petiole].resize(leaves_per_petiole);
1928
1929 for (int leaf = 0; leaf < leaves_per_petiole; leaf++) {
1930 float ind_from_tip = float(leaf) - float(leaves_per_petiole - 1) / 2.f;
1931
1932 // Copy leaf from prototype (matching constructor logic)
1933 uint objID_leaf;
1934 if (phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes > 0) {
1935 int prototype = context_ptr->randu(0, phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototypes - 1);
1936 uint uid = phytomer_ptr->phytomer_parameters.leaf.prototype.unique_prototype_identifier;
1937 assert(this->unique_leaf_prototype_objIDs.find(uid) != this->unique_leaf_prototype_objIDs.end());
1938 assert(this->unique_leaf_prototype_objIDs.at(uid).size() > prototype);
1939 assert(this->unique_leaf_prototype_objIDs.at(uid).at(prototype).size() > leaf);
1940 objID_leaf = context_ptr->copyObject(this->unique_leaf_prototype_objIDs.at(uid).at(prototype).at(leaf));
1941 } else {
1942 objID_leaf = phytomer_ptr->phytomer_parameters.leaf.prototype.prototype_function(context_ptr, &phytomer_ptr->phytomer_parameters.leaf.prototype, ind_from_tip);
1943 }
1944
1945 // Scale leaf
1946 vec3 leaf_scale_vec = leaf_scale[petiole][leaf] * make_vec3(1, 1, 1);
1947 context_ptr->scaleObject(objID_leaf, leaf_scale_vec);
1948
1949 // Calculate compound rotation (matching constructor logic)
1950 float compound_rotation = 0;
1951 if (leaves_per_petiole > 1) {
1952 if (leaf == float(leaves_per_petiole - 1) / 2.f) {
1953 compound_rotation = 0; // tip leaf
1954 } else if (leaf < float(leaves_per_petiole - 1) / 2.f) {
1955 compound_rotation = -0.5 * M_PI; // left lateral
1956 } else {
1957 compound_rotation = 0.5 * M_PI; // right lateral
1958 }
1959 }
1960
1961 // Apply saved rotations (matching constructor logic)
1962 float saved_pitch = deg2rad(leaf_pitch[petiole][leaf]);
1963 float saved_yaw = deg2rad(leaf_yaw[petiole][leaf]);
1964 float saved_roll = deg2rad(leaf_roll[petiole][leaf]);
1965
1966 // Roll rotation
1967 float roll_rot = 0;
1968 if (leaves_per_petiole == 1) {
1969 int sign = 1; // Simplified, constructor uses shoot_index
1970 roll_rot = (acos_safe(internode_axis.z) - saved_roll) * sign;
1971 } else if (ind_from_tip != 0) {
1972 roll_rot = (asin_safe(petiole_tip_axis.z) + saved_roll) * compound_rotation / std::fabs(compound_rotation);
1973 }
1974 phytomer_ptr->leaf_rotation[petiole][leaf].roll = saved_roll;
1975 context_ptr->rotateObject(objID_leaf, roll_rot, "x");
1976
1977 // Pitch rotation
1978 phytomer_ptr->leaf_rotation[petiole][leaf].pitch = saved_pitch;
1979 float pitch_rot = saved_pitch;
1980 if (ind_from_tip == 0) {
1981 pitch_rot += asin_safe(petiole_tip_axis.z);
1982 }
1983 context_ptr->rotateObject(objID_leaf, -pitch_rot, "y");
1984
1985 // Yaw rotation
1986 if (ind_from_tip != 0) {
1987 phytomer_ptr->leaf_rotation[petiole][leaf].yaw = saved_yaw;
1988 context_ptr->rotateObject(objID_leaf, saved_yaw, "z");
1989 } else {
1990 phytomer_ptr->leaf_rotation[petiole][leaf].yaw = 0;
1991 }
1992
1993 // Rotate to petiole azimuth
1994 context_ptr->rotateObject(objID_leaf, -std::atan2(petiole_tip_axis.y, petiole_tip_axis.x) + compound_rotation, "z");
1995
1996 // Auto-calculate leaf base from petiole geometry (handles compound leaves)
1997 vec3 leaf_base = phytomer_ptr->petiole_vertices[petiole].back(); // Default: petiole tip
1998
1999 int leaves_per_petiole = phytomer_ptr->phytomer_parameters.leaf.leaves_per_petiole.val();
2000 float leaflet_offset_val = clampOffset(leaves_per_petiole, phytomer_ptr->phytomer_parameters.leaf.leaflet_offset.val());
2001
2002 if (leaves_per_petiole > 1 && leaflet_offset_val > 0) {
2003 // Compound leaf: calculate lateral leaflet positions along petiole
2004 int ind_from_tip = leaf - int(floor(float(leaves_per_petiole - 1) / 2.f));
2005 if (ind_from_tip != 0) { // Terminal leaflet stays at tip
2006 float offset = (fabs(ind_from_tip) - 0.5f) * leaflet_offset_val * phytomer_ptr->phytomer_parameters.petiole.length.val();
2007 leaf_base = PlantArchitecture::interpolateTube(phytomer_ptr->petiole_vertices[petiole], 1.f - offset / phytomer_ptr->phytomer_parameters.petiole.length.val());
2008 }
2009 }
2010 context_ptr->translateObject(objID_leaf, leaf_base);
2011
2012 phytomer_ptr->leaf_objIDs[petiole][leaf] = objID_leaf;
2013 phytomer_ptr->leaf_bases[petiole][leaf] = leaf_base;
2014 }
2015 }
2016
2017 // Step 4: Restore saved object data
2018 for (int petiole = 0; petiole < phytomer_ptr->leaf_objIDs.size(); petiole++) {
2019 for (int leaf = 0; leaf < phytomer_ptr->leaf_objIDs[petiole].size(); leaf++) {
2020 uint objID = phytomer_ptr->leaf_objIDs[petiole][leaf];
2021 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objID);
2022
2023 for (const auto &pair: saved_leaf_data_int[petiole][leaf]) {
2024 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2025 }
2026 for (const auto &pair: saved_leaf_data_uint[petiole][leaf]) {
2027 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2028 }
2029 for (const auto &pair: saved_leaf_data_float[petiole][leaf]) {
2030 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2031 }
2032 for (const auto &pair: saved_leaf_data_double[petiole][leaf]) {
2033 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2034 }
2035 for (const auto &pair: saved_leaf_data_vec2[petiole][leaf]) {
2036 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2037 }
2038 for (const auto &pair: saved_leaf_data_vec3[petiole][leaf]) {
2039 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2040 }
2041 for (const auto &pair: saved_leaf_data_vec4[petiole][leaf]) {
2042 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2043 }
2044 for (const auto &pair: saved_leaf_data_int2[petiole][leaf]) {
2045 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2046 }
2047 for (const auto &pair: saved_leaf_data_int3[petiole][leaf]) {
2048 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2049 }
2050 for (const auto &pair: saved_leaf_data_int4[petiole][leaf]) {
2051 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2052 }
2053 for (const auto &pair: saved_leaf_data_string[petiole][leaf]) {
2054 context_ptr->setPrimitiveData(UUIDs, pair.first.c_str(), pair.second);
2055 }
2056 }
2057 }
2058
2059 // Apply floral bud data
2060 if (!floral_bud_data.empty() && floral_bud_data.size() <= phytomer_ptr->petiole_length.size()) {
2061 // Ensure the floral_buds vector is properly sized
2062 phytomer_ptr->floral_buds.resize(floral_bud_data.size());
2063
2064 for (size_t petiole = 0; petiole < floral_bud_data.size(); petiole++) {
2065 phytomer_ptr->floral_buds.at(petiole).resize(floral_bud_data.at(petiole).size());
2066
2067 for (size_t bud = 0; bud < floral_bud_data.at(petiole).size(); bud++) {
2068 const FloralBudData &fbud_data = floral_bud_data.at(petiole).at(bud);
2069
2070 // Create and populate FloralBud struct
2071 FloralBud &fbud = phytomer_ptr->floral_buds.at(petiole).at(bud);
2072 // Note: Do NOT set fbud.state here - setFloralBudState() will set it and create geometry
2073 fbud.parent_index = fbud_data.parent_index;
2074 fbud.bud_index = fbud_data.bud_index;
2075 fbud.isterminal = fbud_data.is_terminal;
2076 fbud.current_fruit_scale_factor = fbud_data.current_fruit_scale_factor;
2077
2078 // Auto-calculate floral bud base_position and base_rotation from parent geometry
2079 // Position: terminal buds attach at internode tip, axillary buds at petiole base
2080 // Rotation: computed from bud_index and number of buds (matches growth algorithm)
2081 if (fbud.isterminal) {
2082 // Terminal buds attach at the tip of this phytomer's internode
2083 fbud.base_position = phytomer_ptr->parent_shoot_ptr->shoot_internode_vertices.back().back();
2084 } else {
2085 // Axillary buds attach at the base of the parent petiole
2086 if (petiole < phytomer_ptr->petiole_vertices.size()) {
2087 fbud.base_position = phytomer_ptr->petiole_vertices.at(petiole).front();
2088 } else {
2089 helios_runtime_error("ERROR (PlantArchitecture::readPlantStructureXML): Floral bud parent_index " + std::to_string(fbud.parent_index) + " exceeds number of petioles.");
2090 }
2091 }
2092
2093 // Auto-calculate base_rotation from bud configuration
2094 // Rotation depends on bud_index and total number of buds per petiole/shoot
2095 uint Nbuds = floral_bud_data.at(petiole).size();
2096 if (fbud.isterminal) {
2097 // Terminal bud rotation formula (from PlantArchitecture.cpp:1243-1249)
2098 float pitch_adjustment = (Nbuds > 1) ? deg2rad(30.f) : 0.f;
2099 float yaw_adjustment = static_cast<float>(fbud.bud_index) * 2.f * M_PI / float(Nbuds);
2100 fbud.base_rotation = make_AxisRotation(pitch_adjustment, yaw_adjustment, 0.f);
2101 } else {
2102 // Axillary bud rotation formula (from PlantArchitecture.cpp:1904-1906)
2103 float pitch_adjustment = static_cast<float>(fbud.bud_index) * 0.1f * M_PI / float(Nbuds);
2104 float yaw_adjustment = -0.25f * M_PI + static_cast<float>(fbud.bud_index) * 0.5f * M_PI / float(Nbuds);
2105 fbud.base_rotation = make_AxisRotation(pitch_adjustment, yaw_adjustment, 0.f);
2106 }
2107
2108 // Set the floral bud state to trigger geometry creation
2109 // This will call updateInflorescence() which uses prototype functions from shoot parameters
2110 // Only rebuild geometry if the shoot has prototype functions defined
2111 BudState desired_state = static_cast<BudState>(fbud_data.bud_state);
2112
2113 if (desired_state != BUD_DORMANT && desired_state != BUD_DEAD && desired_state != BUD_ACTIVE) {
2114 // Apply saved peduncle parameters before creating geometry
2115 if (fbud_data.peduncle_length >= 0) {
2116 phytomer_ptr->phytomer_parameters.peduncle.length = fbud_data.peduncle_length;
2117 }
2118 if (fbud_data.peduncle_radius >= 0) {
2119 phytomer_ptr->phytomer_parameters.peduncle.radius = fbud_data.peduncle_radius;
2120 }
2121 if (fbud_data.peduncle_pitch != 0) {
2122 phytomer_ptr->phytomer_parameters.peduncle.pitch = fbud_data.peduncle_pitch;
2123 }
2124 if (fbud_data.peduncle_roll != 0) {
2125 phytomer_ptr->phytomer_parameters.peduncle.roll = fbud_data.peduncle_roll;
2126 }
2127 if (fbud_data.peduncle_curvature != 0) {
2128 phytomer_ptr->phytomer_parameters.peduncle.curvature = fbud_data.peduncle_curvature;
2129 }
2130
2131 // Apply saved inflorescence parameters before creating geometry
2132 if (fbud_data.flower_offset >= 0) {
2133 phytomer_ptr->phytomer_parameters.inflorescence.flower_offset = fbud_data.flower_offset;
2134 }
2135
2136 if ((desired_state == BUD_FLOWER_CLOSED || desired_state == BUD_FLOWER_OPEN) && phytomer_ptr->phytomer_parameters.inflorescence.flower_prototype_function != nullptr) {
2137 FloralBud &fbud = phytomer_ptr->floral_buds.at(petiole).at(bud);
2138
2139 // Manually set up floral bud state without calling updateInflorescence
2140 context_ptr->deleteObject(fbud.inflorescence_objIDs);
2141 fbud.inflorescence_objIDs.clear();
2142 fbud.inflorescence_bases.clear();
2143 fbud.inflorescence_rotation.clear();
2144
2145 if (phytomer_ptr->build_context_geometry_peduncle) {
2146 context_ptr->deleteObject(fbud.peduncle_objIDs);
2147 fbud.peduncle_objIDs.clear();
2148 }
2149
2150 // Set the state without calling updateInflorescence
2151 fbud.state = desired_state;
2152 fbud.time_counter = 0;
2153
2154 // --- PEDUNCLE GEOMETRY RECONSTRUCTION ---
2155 if (fbud_data.peduncle_length > 0 && fbud_data.peduncle_radius > 0) {
2156
2157 // Compute number of segments
2158 uint Ndiv_peduncle_length = std::max(uint(1), phytomer_ptr->phytomer_parameters.peduncle.length_segments);
2159 uint Ndiv_peduncle_radius = std::max(uint(3), phytomer_ptr->phytomer_parameters.peduncle.radial_subdivisions);
2160
2161 // Initialize arrays
2162 std::vector<vec3> peduncle_vertices_computed(Ndiv_peduncle_length + 1);
2163 std::vector<float> peduncle_radii_computed(Ndiv_peduncle_length + 1);
2164
2165 // Set base position and radius
2166 peduncle_vertices_computed.at(0) = fbud.base_position;
2167 peduncle_radii_computed.at(0) = fbud_data.peduncle_radius;
2168
2169 float dr_peduncle = fbud_data.peduncle_length / float(Ndiv_peduncle_length);
2170
2171 // Compute peduncle axis from parameters
2172 vec3 peduncle_axis_computed;
2173 vec3 peduncle_rotation_axis_computed;
2174
2175 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,
2176 peduncle_rotation_axis_computed);
2177
2178 // Use computed value for reconstruction
2179 vec3 peduncle_axis_actual = peduncle_axis_computed;
2180
2181 // Generate vertices with peduncle curvature algorithm
2182 for (int i = 1; i <= Ndiv_peduncle_length; i++) {
2183 if (fabs(fbud_data.peduncle_curvature) > 0) {
2184 float curvature_value = fbud_data.peduncle_curvature;
2185
2186 // Horizontal bending axis perpendicular to peduncle direction
2187 vec3 horizontal_bending_axis = cross(peduncle_axis_actual, make_vec3(0, 0, 1));
2188 float axis_magnitude = horizontal_bending_axis.magnitude();
2189
2190 if (axis_magnitude > 0.001f) {
2191 horizontal_bending_axis = horizontal_bending_axis / axis_magnitude;
2192
2193 float theta_curvature = deg2rad(curvature_value * dr_peduncle);
2194 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)));
2195
2196 if (fabs(theta_curvature) >= theta_from_target) {
2197 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2198 } else {
2199 peduncle_axis_actual = rotatePointAboutLine(peduncle_axis_actual, nullorigin, horizontal_bending_axis, theta_curvature);
2200 peduncle_axis_actual.normalize();
2201 }
2202 } else {
2203 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2204 }
2205 }
2206
2207 peduncle_vertices_computed.at(i) = peduncle_vertices_computed.at(i - 1) + dr_peduncle * peduncle_axis_actual;
2208 peduncle_radii_computed.at(i) = fbud_data.peduncle_radius;
2209 }
2210
2211 // Store computed geometry
2212 if (petiole < phytomer_ptr->peduncle_vertices.size()) {
2213 if (phytomer_ptr->peduncle_vertices.at(petiole).size() <= bud) {
2214 phytomer_ptr->peduncle_vertices.at(petiole).resize(bud + 1);
2215 phytomer_ptr->peduncle_radii.at(petiole).resize(bud + 1);
2216 }
2217 phytomer_ptr->peduncle_vertices.at(petiole).at(bud) = peduncle_vertices_computed;
2218 phytomer_ptr->peduncle_radii.at(petiole).at(bud) = peduncle_radii_computed;
2219 }
2220
2221 // Rebuild Context geometry with COMPUTED vertices/radii
2222 if (phytomer_ptr->build_context_geometry_peduncle) {
2223 std::vector<RGBcolor> colors(peduncle_vertices_computed.size(), phytomer_ptr->phytomer_parameters.peduncle.color);
2224 fbud.peduncle_objIDs.push_back(context_ptr->addTubeObject(Ndiv_peduncle_radius, peduncle_vertices_computed, peduncle_radii_computed, colors));
2225 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(fbud.peduncle_objIDs.back()), "object_label", "peduncle");
2226 std::string peduncle_material_name = plant_instances.at(plantID).plant_name + "_" + shoot_type_label + "_peduncle";
2227 renameAutoMaterial(context_ptr, fbud.peduncle_objIDs.back(), peduncle_material_name);
2228 }
2229 }
2230
2231 // Restore flower geometry using saved rotations (base positions auto-computed)
2232 if (!fbud_data.flower_rotations.empty()) {
2233
2234 // Ensure prototype maps are initialized before creating geometry
2235 ensureInflorescencePrototypesInitialized(phytomer_ptr->phytomer_parameters, plant_instances.at(plantID).plant_name);
2236
2237 for (size_t i = 0; i < fbud_data.flower_rotations.size(); i++) {
2238 // Get saved base if available (for backward compatibility), otherwise will compute
2239 vec3 flower_base_saved = (i < fbud_data.inflorescence_bases_saved.size()) ? fbud_data.inflorescence_bases_saved.at(i) : make_vec3(0, 0, 0);
2240 float saved_pitch = deg2rad(fbud_data.flower_rotations.at(i).pitch);
2241 float saved_yaw = deg2rad(fbud_data.flower_rotations.at(i).yaw);
2242 float saved_roll = deg2rad(fbud_data.flower_rotations.at(i).roll);
2243 float saved_azimuth = deg2rad(fbud_data.flower_rotations.at(i).azimuth);
2244
2245 // Compute flower base position from parameters
2246 int flowers_per_peduncle = fbud_data.flower_rotations.size();
2247 int petioles_per_internode = phytomer_ptr->phytomer_parameters.petiole.petioles_per_internode;
2248
2249 // Clamp offset and compute position
2250 float flower_offset_clamped = clampOffset(flowers_per_peduncle, fbud_data.flower_offset);
2251 float ind_from_tip_computed = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2252
2253 // Default position: peduncle tip
2254 vec3 flower_base_computed = phytomer_ptr->peduncle_vertices.at(petiole).at(bud).back();
2255
2256 // Compute position along peduncle if offset is non-zero
2257 if (flowers_per_peduncle > 1 && flower_offset_clamped > 0) {
2258 if (ind_from_tip_computed != 0) {
2259 float offset_computed = (ind_from_tip_computed - 0.5f) * flower_offset_clamped * fbud_data.peduncle_length;
2260 float frac_computed = 1.0f;
2261 if (fbud_data.peduncle_length > 0) {
2262 frac_computed = 1.f - offset_computed / fbud_data.peduncle_length;
2263 }
2264 flower_base_computed = interpolateTube(phytomer_ptr->peduncle_vertices.at(petiole).at(bud), frac_computed);
2265 }
2266 }
2267
2268 // Recalculate peduncle_axis from saved peduncle geometry and flower position
2269 float flower_offset_val = fbud_data.flower_offset;
2270 if (flowers_per_peduncle > 2) {
2271 float denom = 0.5f * float(flowers_per_peduncle) - 1.f;
2272 if (flower_offset_val * denom > 1.f) {
2273 flower_offset_val = 1.f / denom;
2274 }
2275 }
2276
2277 float ind_from_tip = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2278 float frac = 1.0f;
2279 if (flowers_per_peduncle > 1 && flower_offset_val > 0 && ind_from_tip != 0) {
2280 float offset = (ind_from_tip - 0.5f) * flower_offset_val * fbud_data.peduncle_length;
2281 if (fbud_data.peduncle_length > 0) {
2282 frac = 1.f - offset / fbud_data.peduncle_length;
2283 }
2284 }
2285 vec3 recalculated_peduncle_axis = phytomer_ptr->getAxisVector(frac, phytomer_ptr->peduncle_vertices.at(petiole).at(bud));
2286
2287 // Use individual base scale if available, otherwise use default parameter
2288 float scale_factor;
2289 if (i < fbud_data.flower_base_scales.size() && fbud_data.flower_base_scales.at(i) >= 0) {
2290 scale_factor = fbud_data.flower_base_scales.at(i);
2291 } else {
2292 scale_factor = phytomer_ptr->phytomer_parameters.inflorescence.flower_prototype_scale.val();
2293 }
2294
2295 // Determine if flower is open
2296 bool is_open_flower = (desired_state == BUD_FLOWER_OPEN);
2297
2298 // Create flower geometry with computed base, saved rotations, and recalculated peduncle axis
2299 phytomer_ptr->createInflorescenceGeometry(fbud, flower_base_computed, recalculated_peduncle_axis, saved_pitch, saved_roll, saved_azimuth, saved_yaw, scale_factor, is_open_flower);
2300 }
2301 }
2302
2303 } else if (desired_state == BUD_FRUITING && phytomer_ptr->phytomer_parameters.inflorescence.fruit_prototype_function != nullptr) {
2304 FloralBud &fbud = phytomer_ptr->floral_buds.at(petiole).at(bud);
2305
2306 // Manually set up floral bud state without calling updateInflorescence
2307 context_ptr->deleteObject(fbud.inflorescence_objIDs);
2308 fbud.inflorescence_objIDs.clear();
2309 fbud.inflorescence_bases.clear();
2310 fbud.inflorescence_rotation.clear();
2311
2312 if (phytomer_ptr->build_context_geometry_peduncle) {
2313 context_ptr->deleteObject(fbud.peduncle_objIDs);
2314 fbud.peduncle_objIDs.clear();
2315 }
2316
2317 fbud.state = desired_state;
2318 fbud.time_counter = 0;
2319
2320 // --- PEDUNCLE GEOMETRY RECONSTRUCTION ---
2321 if (fbud_data.peduncle_length > 0 && fbud_data.peduncle_radius > 0) {
2322
2323 // Compute number of segments
2324 uint Ndiv_peduncle_length = std::max(uint(1), phytomer_ptr->phytomer_parameters.peduncle.length_segments);
2325 uint Ndiv_peduncle_radius = std::max(uint(3), phytomer_ptr->phytomer_parameters.peduncle.radial_subdivisions);
2326
2327 // Initialize arrays
2328 std::vector<vec3> peduncle_vertices_computed(Ndiv_peduncle_length + 1);
2329 std::vector<float> peduncle_radii_computed(Ndiv_peduncle_length + 1);
2330
2331 // Set base position and radius
2332 peduncle_vertices_computed.at(0) = fbud.base_position;
2333 peduncle_radii_computed.at(0) = fbud_data.peduncle_radius;
2334
2335 float dr_peduncle = fbud_data.peduncle_length / float(Ndiv_peduncle_length);
2336
2337 // Compute peduncle axis from parameters
2338 vec3 peduncle_axis_computed;
2339 vec3 peduncle_rotation_axis_computed;
2340
2341 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,
2342 peduncle_rotation_axis_computed);
2343
2344 // Use computed value for reconstruction
2345 vec3 peduncle_axis_actual = peduncle_axis_computed;
2346
2347 // Generate vertices with peduncle curvature algorithm
2348 for (int i = 1; i <= Ndiv_peduncle_length; i++) {
2349 if (fabs(fbud_data.peduncle_curvature) > 0) {
2350 float curvature_value = fbud_data.peduncle_curvature;
2351
2352 // Horizontal bending axis perpendicular to peduncle direction
2353 vec3 horizontal_bending_axis = cross(peduncle_axis_actual, make_vec3(0, 0, 1));
2354 float axis_magnitude = horizontal_bending_axis.magnitude();
2355
2356 if (axis_magnitude > 0.001f) {
2357 horizontal_bending_axis = horizontal_bending_axis / axis_magnitude;
2358
2359 float theta_curvature = deg2rad(curvature_value * dr_peduncle);
2360 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)));
2361
2362 if (fabs(theta_curvature) >= theta_from_target) {
2363 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2364 } else {
2365 peduncle_axis_actual = rotatePointAboutLine(peduncle_axis_actual, nullorigin, horizontal_bending_axis, theta_curvature);
2366 peduncle_axis_actual.normalize();
2367 }
2368 } else {
2369 peduncle_axis_actual = (curvature_value > 0) ? make_vec3(0, 0, 1) : make_vec3(0, 0, -1);
2370 }
2371 }
2372
2373 peduncle_vertices_computed.at(i) = peduncle_vertices_computed.at(i - 1) + dr_peduncle * peduncle_axis_actual;
2374 peduncle_radii_computed.at(i) = fbud_data.peduncle_radius;
2375 }
2376
2377 // Store computed geometry
2378 if (petiole < phytomer_ptr->peduncle_vertices.size()) {
2379 if (phytomer_ptr->peduncle_vertices.at(petiole).size() <= bud) {
2380 phytomer_ptr->peduncle_vertices.at(petiole).resize(bud + 1);
2381 phytomer_ptr->peduncle_radii.at(petiole).resize(bud + 1);
2382 }
2383 phytomer_ptr->peduncle_vertices.at(petiole).at(bud) = peduncle_vertices_computed;
2384 phytomer_ptr->peduncle_radii.at(petiole).at(bud) = peduncle_radii_computed;
2385 }
2386
2387 // Rebuild Context geometry with COMPUTED vertices/radii
2388 if (phytomer_ptr->build_context_geometry_peduncle) {
2389 std::vector<RGBcolor> colors(peduncle_vertices_computed.size(), phytomer_ptr->phytomer_parameters.peduncle.color);
2390 fbud.peduncle_objIDs.push_back(context_ptr->addTubeObject(Ndiv_peduncle_radius, peduncle_vertices_computed, peduncle_radii_computed, colors));
2391 context_ptr->setPrimitiveData(context_ptr->getObjectPrimitiveUUIDs(fbud.peduncle_objIDs.back()), "object_label", "peduncle");
2392 std::string peduncle_material_name = plant_instances.at(plantID).plant_name + "_" + shoot_type_label + "_peduncle";
2393 renameAutoMaterial(context_ptr, fbud.peduncle_objIDs.back(), peduncle_material_name);
2394 }
2395 }
2396
2397 // Restore fruit geometry using saved rotations (base positions auto-computed)
2398 if (!fbud_data.flower_rotations.empty()) {
2399
2400 // Ensure prototype maps are initialized before creating geometry
2401 ensureInflorescencePrototypesInitialized(phytomer_ptr->phytomer_parameters, plant_instances.at(plantID).plant_name);
2402
2403 for (size_t i = 0; i < fbud_data.flower_rotations.size(); i++) {
2404 // Get saved base if available (for backward compatibility), otherwise will compute
2405 vec3 fruit_base_saved = (i < fbud_data.inflorescence_bases_saved.size()) ? fbud_data.inflorescence_bases_saved.at(i) : make_vec3(0, 0, 0);
2406 float saved_pitch = deg2rad(fbud_data.flower_rotations.at(i).pitch);
2407 float saved_yaw = deg2rad(fbud_data.flower_rotations.at(i).yaw);
2408 float saved_roll = deg2rad(fbud_data.flower_rotations.at(i).roll);
2409 float saved_azimuth = deg2rad(fbud_data.flower_rotations.at(i).azimuth);
2410
2411 // Compute fruit base position from parameters
2412 int flowers_per_peduncle = fbud_data.flower_rotations.size();
2413 int petioles_per_internode = phytomer_ptr->phytomer_parameters.petiole.petioles_per_internode;
2414
2415 // Clamp offset and compute position
2416 float flower_offset_clamped = clampOffset(flowers_per_peduncle, fbud_data.flower_offset);
2417 float ind_from_tip_computed = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2418
2419 // Default position: peduncle tip
2420 vec3 fruit_base_computed = phytomer_ptr->peduncle_vertices.at(petiole).at(bud).back();
2421
2422 // Compute position along peduncle if offset is non-zero
2423 if (flowers_per_peduncle > 1 && flower_offset_clamped > 0) {
2424 if (ind_from_tip_computed != 0) {
2425 float offset_computed = (ind_from_tip_computed - 0.5f) * flower_offset_clamped * fbud_data.peduncle_length;
2426 float frac_computed = 1.0f;
2427 if (fbud_data.peduncle_length > 0) {
2428 frac_computed = 1.f - offset_computed / fbud_data.peduncle_length;
2429 }
2430 fruit_base_computed = interpolateTube(phytomer_ptr->peduncle_vertices.at(petiole).at(bud), frac_computed);
2431 }
2432 }
2433
2434 // Verification complete - computation matches saved values within tolerance
2435 // (Verification code removed after Phase 1 testing confirmed correctness)
2436
2437 // Recalculate peduncle_axis from saved peduncle geometry and fruit position
2438 float flower_offset_val = fbud_data.flower_offset;
2439 if (flowers_per_peduncle > 2) {
2440 float denom = 0.5f * float(flowers_per_peduncle) - 1.f;
2441 if (flower_offset_val * denom > 1.f) {
2442 flower_offset_val = 1.f / denom;
2443 }
2444 }
2445
2446 float ind_from_tip = fabs(float(i) - float(flowers_per_peduncle - 1) / float(petioles_per_internode));
2447 float frac = 1.0f;
2448 if (flowers_per_peduncle > 1 && flower_offset_val > 0 && ind_from_tip != 0) {
2449 float offset = (ind_from_tip - 0.5f) * flower_offset_val * fbud_data.peduncle_length;
2450 if (fbud_data.peduncle_length > 0) {
2451 frac = 1.f - offset / fbud_data.peduncle_length;
2452 }
2453 }
2454 vec3 recalculated_peduncle_axis = phytomer_ptr->getAxisVector(frac, phytomer_ptr->peduncle_vertices.at(petiole).at(bud));
2455
2456 // Use individual base scale if available, then apply growth scaling
2457 float base_fruit_scale;
2458 if (i < fbud_data.flower_base_scales.size() && fbud_data.flower_base_scales.at(i) >= 0) {
2459 base_fruit_scale = fbud_data.flower_base_scales.at(i);
2460 } else {
2461 base_fruit_scale = phytomer_ptr->phytomer_parameters.inflorescence.fruit_prototype_scale.val();
2462 }
2463 float scale_factor = base_fruit_scale * fbud_data.current_fruit_scale_factor;
2464
2465 // Create fruit geometry with computed base, saved rotations, and recalculated peduncle axis
2466 phytomer_ptr->createInflorescenceGeometry(fbud, fruit_base_computed, recalculated_peduncle_axis, saved_pitch, saved_roll, saved_azimuth, saved_yaw, scale_factor, false);
2467 }
2468 }
2469 }
2470 } else {
2471 // For states that don't create geometry (DORMANT, DEAD, ACTIVE), just set the state
2472 fbud.state = desired_state;
2473 }
2474 }
2475 }
2476 }
2477
2478 phytomer_count++;
2479 } // phytomers
2480
2481 } // shoots
2482
2483 } // plant instances
2484
2485 if (!quiet) {
2486 std::cout << "done." << std::endl;
2487 }
2488 return plantIDs;
2489}
2490
2491// ---- USD Articulated Rigid Body Export for IsaacSim ---- //
2492
2493namespace {
2494
2495enum USDLinkType { USD_LINK_CAPSULE, USD_LINK_MESH };
2496
2497struct USDMaterial {
2498 std::string name; // Unique material name for USD prim
2499 std::string texture_file; // Absolute path to texture image (empty if color-only)
2500 RGBcolor color; // Diffuse color (used when no texture, or as fallback)
2501};
2502
2503struct USDLink {
2504 std::string name;
2505 vec3 position; // World-space centroid (capsule midpoint or mesh centroid)
2506 float qw, qx, qy, qz; // Orientation quaternion (rotation from Z-axis to segment axis)
2507 float radius;
2508 float half_length; // Half-length of cylinder shaft
2509 float mass;
2510 vec3 inertia_diagonal; // (Ixx, Iyy, Izz)
2511 int parent_link_index; // -1 for root
2512 USDLinkType link_type = USD_LINK_CAPSULE;
2513 int material_index = -1; // Index into ArticulationData::materials (-1 = default physics material)
2514
2515 // Mesh data (only used when link_type == USD_LINK_MESH)
2516 std::vector<vec3> mesh_vertices; // In local space (centroid at origin)
2517 std::vector<vec2> mesh_uvs; // Per-vertex UV coordinates
2518 std::vector<int> mesh_face_vertex_counts;
2519 std::vector<int> mesh_face_vertex_indices;
2520};
2521
2522struct USDJoint {
2523 std::string name;
2524 int parent_link_index;
2525 int child_link_index;
2526 vec3 local_pos_parent; // Joint anchor in parent local frame
2527 vec3 local_pos_child; // Joint anchor in child local frame
2528 float stiffness;
2529 float damping;
2530 bool is_fixed; // true for world anchor joint
2531 // Orientation of joint frame in child body's local space.
2532 // Encodes relative rotation so that zero joint angle reproduces the rest pose.
2533 // localRot0 is always identity; localRot1 = inverse(child_quat) * parent_quat.
2534 float local_rot1_w = 1, local_rot1_x = 0, local_rot1_y = 0, local_rot1_z = 0;
2535};
2536
2537float computeCapsuleVolume(float radius, float half_length) {
2538 float cylinder_vol = M_PI * radius * radius * 2.f * half_length;
2539 float sphere_vol = (4.f / 3.f) * M_PI * radius * radius * radius;
2540 return cylinder_vol + sphere_vol;
2541}
2542
2543vec3 computeCapsuleInertia(float mass, float radius, float half_length) {
2544 float L = 2.f * half_length;
2545 float Ixx = mass * (3.f * radius * radius + L * L) / 12.f;
2546 float Iyy = Ixx;
2547 float Izz = mass * radius * radius / 2.f;
2548 return {Ixx, Iyy, Izz};
2549}
2550
2551vec3 computeSphereInertia(float mass, float radius) {
2552 float I = 0.4f * mass * radius * radius;
2553 return {I, I, I};
2554}
2555
2556float computeJointStiffness(float E, float radius, float segment_length) {
2557 float I = M_PI * radius * radius * radius * radius / 4.f;
2558 return E * I / segment_length;
2559}
2560
2561float computeJointDamping(float stiffness, float rotational_inertia, float damping_ratio) {
2562 return damping_ratio * 2.f * std::sqrt(stiffness * rotational_inertia);
2563}
2564
2565void axisToQuaternion(const vec3 &axis_dir, float &qw, float &qx, float &qy, float &qz) {
2566 vec3 z_axis(0, 0, 1);
2567 vec3 a = axis_dir;
2568 float mag = a.magnitude();
2569 if (mag < 1e-8f) {
2570 qw = 1; qx = 0; qy = 0; qz = 0;
2571 return;
2572 }
2573 a = a / mag;
2574
2575 float dot = z_axis.x * a.x + z_axis.y * a.y + z_axis.z * a.z;
2576
2577 if (dot > 0.9999f) {
2578 qw = 1; qx = 0; qy = 0; qz = 0;
2579 return;
2580 }
2581 if (dot < -0.9999f) {
2582 qw = 0; qx = 1; qy = 0; qz = 0;
2583 return;
2584 }
2585
2586 vec3 cross_prod;
2587 cross_prod.x = z_axis.y * a.z - z_axis.z * a.y;
2588 cross_prod.y = z_axis.z * a.x - z_axis.x * a.z;
2589 cross_prod.z = z_axis.x * a.y - z_axis.y * a.x;
2590
2591 qw = 1.f + dot;
2592 qx = cross_prod.x;
2593 qy = cross_prod.y;
2594 qz = cross_prod.z;
2595
2596 float norm = std::sqrt(qw * qw + qx * qx + qy * qy + qz * qz);
2597 qw /= norm;
2598 qx /= norm;
2599 qy /= norm;
2600 qz /= norm;
2601}
2602
2603// Quaternion multiplication: result = a * b (Hamilton product)
2604void quatMultiply(float aw, float ax, float ay, float az,
2605 float bw, float bx, float by, float bz,
2606 float &rw, float &rx, float &ry, float &rz) {
2607 rw = aw * bw - ax * bx - ay * by - az * bz;
2608 rx = aw * bx + ax * bw + ay * bz - az * by;
2609 ry = aw * by - ax * bz + ay * bw + az * bx;
2610 rz = aw * bz + ax * by - ay * bx + az * bw;
2611}
2612
2613// Compute localRot1 for a joint connecting parent_link to child_link.
2614// localRot1 = inverse(child_quat) * parent_quat
2615// This ensures zero joint angle reproduces the rest pose where both bodies
2616// are at their authored world orientations.
2617void computeJointLocalRot1(const USDLink &parent, const USDLink &child, USDJoint &joint) {
2618 // conjugate of child quaternion = inverse for unit quaternions
2619 float inv_cw = child.qw, inv_cx = -child.qx, inv_cy = -child.qy, inv_cz = -child.qz;
2620 quatMultiply(inv_cw, inv_cx, inv_cy, inv_cz,
2621 parent.qw, parent.qx, parent.qy, parent.qz,
2622 joint.local_rot1_w, joint.local_rot1_x, joint.local_rot1_y, joint.local_rot1_z);
2623}
2624
2625struct ArticulationData {
2626 std::vector<USDLink> links;
2627 std::vector<USDJoint> joints;
2628 std::vector<USDMaterial> materials;
2629 std::map<std::string, int> material_cache; // key -> material index
2630};
2631
2632} // anonymous namespace (temporarily close to define GrowthFrameSnapshot)
2633
2634// GrowthFrameSnapshot: wraps ArticulationData for growth animation frame storage.
2635// Defined outside the anonymous namespace so shared_ptr<GrowthFrameSnapshot> in the header works.
2637 float time; // plant age in days at this frame
2638 ArticulationData artic_data;
2639};
2640
2641namespace {
2642
2643// Look up or create a material entry for the given texture/color combination.
2644int getOrCreateMaterial(ArticulationData &data, const std::string &texture_file, const RGBcolor &color) {
2645 std::string key = texture_file + "|" + std::to_string(color.r) + "," + std::to_string(color.g) + "," + std::to_string(color.b);
2646 auto it = data.material_cache.find(key);
2647 if (it != data.material_cache.end()) {
2648 return it->second;
2649 }
2650 int idx = static_cast<int>(data.materials.size());
2651 USDMaterial mat;
2652 mat.name = "Material_" + std::to_string(idx);
2653 mat.texture_file = texture_file;
2654 mat.color = color;
2655 data.materials.push_back(mat);
2656 data.material_cache[key] = idx;
2657 return idx;
2658}
2659
2660// Get or create a material from a context object by looking up the texture and color
2661// of its first primitive. Returns the material index.
2662int getMaterialFromContextObject(Context *context_ptr, uint objID, ArticulationData &data) {
2663 if (!context_ptr->doesObjectExist(objID)) {
2664 return -1;
2665 }
2666 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objID);
2667 if (UUIDs.empty()) {
2668 return -1;
2669 }
2670 std::string texture_file = context_ptr->getPrimitiveTextureFile(UUIDs[0]);
2671 RGBcolor color = context_ptr->getPrimitiveColor(UUIDs[0]);
2672 return getOrCreateMaterial(data, texture_file, color);
2673}
2674
2675// Apply inverse quaternion rotation to transform a world-space point into the Xform's local frame.
2676// The Xform has translate + orient, so local = inverseRotate(world - translate).
2677// Uses the vector rotation formula: v' = v + 2*w*(u x v) + 2*(u x (u x v))
2678// where q = (w, u) and we use the conjugate q* = (w, -u) for inverse rotation.
2679vec3 worldToLocal(const vec3 &world_point, const vec3 &xform_position, float qw, float qx, float qy, float qz) {
2680 vec3 v = world_point - xform_position;
2681 // Conjugate quaternion for inverse rotation: (qw, -qx, -qy, -qz)
2682 vec3 u(-qx, -qy, -qz);
2683 vec3 uv;
2684 uv.x = u.y * v.z - u.z * v.y;
2685 uv.y = u.z * v.x - u.x * v.z;
2686 uv.z = u.x * v.y - u.y * v.x;
2687 vec3 uuv;
2688 uuv.x = u.y * uv.z - u.z * uv.y;
2689 uuv.y = u.z * uv.x - u.x * uv.z;
2690 uuv.z = u.x * uv.y - u.y * uv.x;
2691 vec3 result;
2692 result.x = v.x + 2.f * (qw * uv.x + uuv.x);
2693 result.y = v.y + 2.f * (qw * uv.y + uuv.y);
2694 result.z = v.z + 2.f * (qw * uv.z + uuv.z);
2695 return result;
2696}
2697
2698// Build a visual mesh for a tube segment by extracting the primitives belonging to
2699// that segment from the tube compound object. For a tube with N nodes and R radial
2700// subdivisions, segment i (between node i and node i+1) has 2*R triangles.
2701// The UUIDs are ordered: outer loop j=0..R-1, inner loop i=0..N-2, 2 triangles per (j,i).
2702void buildTubeSegmentVisualMesh(Context *context_ptr, uint tube_objID, int segment_index,
2703 int total_segments, ArticulationData &data, USDLink &link) {
2704
2705 if (!context_ptr->doesObjectExist(tube_objID)) {
2706 return;
2707 }
2708
2709 uint radial_subdivs = context_ptr->getTubeObjectSubdivisionCount(tube_objID);
2710 std::vector<uint> all_UUIDs = context_ptr->getObjectPrimitiveUUIDs(tube_objID);
2711
2712 // Collect UUIDs for this segment: for each radial strip j, grab 2 triangles at position (j, segment_index)
2713 std::vector<uint> seg_UUIDs;
2714 seg_UUIDs.reserve(2 * radial_subdivs);
2715 for (uint j = 0; j < radial_subdivs; j++) {
2716 int base = static_cast<int>(j) * 2 * total_segments + 2 * segment_index;
2717 if (base + 1 < static_cast<int>(all_UUIDs.size())) {
2718 seg_UUIDs.push_back(all_UUIDs[base]);
2719 seg_UUIDs.push_back(all_UUIDs[base + 1]);
2720 }
2721 }
2722
2723 if (seg_UUIDs.empty()) {
2724 return;
2725 }
2726
2727 // Extract mesh data from these primitives
2728 std::vector<vec3> world_vertices;
2729 std::vector<vec2> all_uvs;
2730 std::string texture_file;
2731 RGBcolor color = make_RGBcolor(0.5f, 0.5f, 0.5f);
2732
2733 for (uint UUID : seg_UUIDs) {
2734 std::vector<vec3> prim_verts = context_ptr->getPrimitiveVertices(UUID);
2735 std::vector<vec2> prim_uvs = context_ptr->getPrimitiveTextureUV(UUID);
2736 int base_idx = static_cast<int>(world_vertices.size());
2737 link.mesh_face_vertex_counts.push_back(static_cast<int>(prim_verts.size()));
2738 for (int i = 0; i < static_cast<int>(prim_verts.size()); i++) {
2739 link.mesh_face_vertex_indices.push_back(base_idx + i);
2740 world_vertices.push_back(prim_verts[i]);
2741 if (i < static_cast<int>(prim_uvs.size())) {
2742 all_uvs.push_back(prim_uvs[i]);
2743 } else {
2744 all_uvs.push_back(make_vec2(0, 0));
2745 }
2746 }
2747
2748 if (texture_file.empty()) {
2749 texture_file = context_ptr->getPrimitiveTextureFile(UUID);
2750 color = context_ptr->getPrimitiveColor(UUID);
2751 }
2752 }
2753
2754 // Convert to local space: translate to origin then apply inverse rotation
2755 link.mesh_vertices.resize(world_vertices.size());
2756 for (size_t i = 0; i < world_vertices.size(); i++) {
2757 link.mesh_vertices[i] = worldToLocal(world_vertices[i], link.position, link.qw, link.qx, link.qy, link.qz);
2758 }
2759 link.mesh_uvs = all_uvs;
2760
2761 // Assign material
2762 link.material_index = getOrCreateMaterial(data, texture_file, color);
2763}
2764
2765// Build mesh data from a Helios context object. Vertices are stored in local space
2766// (translated so the centroid is at origin). Returns the world-space centroid.
2767// Also extracts UV coordinates and determines the material.
2768vec3 buildMeshFromContextObject(Context *context_ptr, uint objID, ArticulationData &data,
2769 USDLink &link) {
2770
2771 std::vector<uint> UUIDs = context_ptr->getObjectPrimitiveUUIDs(objID);
2772
2773 // Collect all vertices, UVs, and faces
2774 std::vector<vec3> world_vertices;
2775 std::vector<vec2> all_uvs;
2776 std::string texture_file;
2777 RGBcolor color = make_RGBcolor(0.5f, 0.5f, 0.5f);
2778
2779 for (uint UUID : UUIDs) {
2780 std::vector<vec3> prim_verts = context_ptr->getPrimitiveVertices(UUID);
2781 std::vector<vec2> prim_uvs = context_ptr->getPrimitiveTextureUV(UUID);
2782 int base_idx = static_cast<int>(world_vertices.size());
2783 link.mesh_face_vertex_counts.push_back(static_cast<int>(prim_verts.size()));
2784 for (int i = 0; i < static_cast<int>(prim_verts.size()); i++) {
2785 link.mesh_face_vertex_indices.push_back(base_idx + i);
2786 world_vertices.push_back(prim_verts[i]);
2787 if (i < static_cast<int>(prim_uvs.size())) {
2788 all_uvs.push_back(prim_uvs[i]);
2789 } else {
2790 all_uvs.push_back(make_vec2(0, 0));
2791 }
2792 }
2793
2794 // Get texture and color from first primitive
2795 if (texture_file.empty()) {
2796 texture_file = context_ptr->getPrimitiveTextureFile(UUID);
2797 color = context_ptr->getPrimitiveColor(UUID);
2798 }
2799 }
2800
2801 // Compute centroid
2802 vec3 centroid(0, 0, 0);
2803 if (!world_vertices.empty()) {
2804 for (const auto &v : world_vertices) {
2805 centroid = centroid + v;
2806 }
2807 centroid = centroid / static_cast<float>(world_vertices.size());
2808 }
2809
2810 // Convert to local space (centroid at origin)
2811 link.mesh_vertices.resize(world_vertices.size());
2812 for (size_t i = 0; i < world_vertices.size(); i++) {
2813 link.mesh_vertices[i] = world_vertices[i] - centroid;
2814 }
2815 link.mesh_uvs = all_uvs;
2816
2817 // Assign material
2818 link.material_index = getOrCreateMaterial(data, texture_file, color);
2819
2820 return centroid;
2821}
2822
2823// Compute approximate inertia for a mesh by treating it as a collection of point masses
2824vec3 computeMeshInertia(float mass, const std::vector<vec3> &local_vertices) {
2825 if (local_vertices.empty()) {
2826 return {0, 0, 0};
2827 }
2828 float mass_per_vertex = mass / static_cast<float>(local_vertices.size());
2829 float Ixx = 0, Iyy = 0, Izz = 0;
2830 for (const auto &v : local_vertices) {
2831 Ixx += mass_per_vertex * (v.y * v.y + v.z * v.z);
2832 Iyy += mass_per_vertex * (v.x * v.x + v.z * v.z);
2833 Izz += mass_per_vertex * (v.x * v.x + v.y * v.y);
2834 }
2835 return {Ixx, Iyy, Izz};
2836}
2837
2838std::string sanitizePrimName(const std::string &name) {
2839 std::string result;
2840 result.reserve(name.size());
2841 for (char c : name) {
2842 if (std::isalnum(c) || c == '_') {
2843 result += c;
2844 } else {
2845 result += '_';
2846 }
2847 }
2848 return result;
2849}
2850
2851ArticulationData buildArticulationData(const PlantInstance &plant, const USDExportParameters &params, Context *context_ptr) {
2852
2853 ArticulationData data;
2854
2855 std::map<std::pair<int, int>, int> last_internode_link_index;
2856 std::map<int, int> shoot_last_link_index;
2857 std::map<std::pair<int, int>, int> shoot_node_link_index;
2858
2859 for (const auto &shoot : plant.shoot_tree) {
2860
2861 int prev_internode_link = -1;
2862 vec3 last_internode_vertex;
2863 bool has_last_internode_vertex = false;
2864
2865 // Compute total tube nodes/segments for this shoot's internode tube (for visual mesh extraction)
2866 int total_tube_nodes = 0;
2867 for (uint pi = 0; pi < shoot->shoot_internode_vertices.size(); pi++) {
2868 total_tube_nodes += static_cast<int>(shoot->shoot_internode_vertices[pi].size());
2869 }
2870 int total_tube_segments = std::max(0, total_tube_nodes - 1);
2871 int flat_node_offset = 0; // running offset into the flattened node array
2872
2873 int shoot_parent_link = -1;
2874 if (shoot->parent_shoot_ID >= 0) {
2875 // Try exact node match first
2876 auto key = std::make_pair(shoot->parent_shoot_ID, static_cast<int>(shoot->parent_node_index));
2877 auto it = shoot_node_link_index.find(key);
2878 if (it != shoot_node_link_index.end()) {
2879 shoot_parent_link = it->second;
2880 } else {
2881 // Exact node was filtered out — fall back to nearest upstream link on the parent shoot
2882 auto shoot_it = shoot_last_link_index.find(shoot->parent_shoot_ID);
2883 if (shoot_it != shoot_last_link_index.end()) {
2884 shoot_parent_link = shoot_it->second;
2885 }
2886 // If parent shoot produced no links at all, shoot_parent_link stays -1
2887 // and this child shoot will become a new root (or be skipped if no segments survive)
2888 }
2889 }
2890
2891 for (uint phytomer_idx = 0; phytomer_idx < shoot->phytomers.size(); phytomer_idx++) {
2892
2893 const auto &vertices = shoot->shoot_internode_vertices[phytomer_idx];
2894 const auto &radii = shoot->shoot_internode_radii[phytomer_idx];
2895 const auto &phytomer = shoot->phytomers[phytomer_idx];
2896
2897 // Build list of segment vertex pairs, handling single-vertex phytomers
2898 // by using the previous phytomer's last vertex as the start point.
2899 // Also track the flat tube segment index for visual mesh extraction.
2900 std::vector<std::pair<vec3, vec3>> segment_pairs;
2901 std::vector<float> segment_radii;
2902 std::vector<int> segment_tube_indices; // flat tube segment index for each segment
2903
2904 if (vertices.size() == 1 && has_last_internode_vertex) {
2905 // Vertex-sharing: segment between previous phytomer's last node and this node
2906 // In flat tube, that's segment (flat_node_offset - 1)
2907 segment_pairs.push_back({last_internode_vertex, vertices[0]});
2908 segment_radii.push_back(radii[0]);
2909 segment_tube_indices.push_back(flat_node_offset - 1);
2910 } else {
2911 for (int seg = 0; seg < static_cast<int>(vertices.size()) - 1; seg++) {
2912 segment_pairs.push_back({vertices[seg], vertices[seg + 1]});
2913 segment_radii.push_back(radii[seg]);
2914 segment_tube_indices.push_back(flat_node_offset + seg);
2915 }
2916 }
2917
2918 flat_node_offset += static_cast<int>(vertices.size());
2919
2920 // Update last vertex for next phytomer's vertex sharing
2921 if (!vertices.empty()) {
2922 last_internode_vertex = vertices.back();
2923 has_last_internode_vertex = true;
2924 }
2925
2926 for (int seg = 0; seg < static_cast<int>(segment_pairs.size()); seg++) {
2927 vec3 start = segment_pairs[seg].first;
2928 vec3 end = segment_pairs[seg].second;
2929 vec3 axis = end - start;
2930 float length = axis.magnitude();
2931
2932 if (length < params.min_segment_length) {
2933 continue;
2934 }
2935
2936 float seg_radius = segment_radii[seg];
2937 if (!(seg_radius > 0) || seg_radius > length) {
2938 continue;
2939 }
2940
2941 vec3 midpoint = (start + end) * 0.5f;
2942 vec3 axis_normalized = axis / length;
2943 float half_len = length / 2.f;
2944
2945 USDLink link;
2946 link.name = "S" + std::to_string(shoot->ID) + "_P" + std::to_string(phytomer_idx) + "_Seg" + std::to_string(seg);
2947 link.position = midpoint;
2948 axisToQuaternion(axis_normalized, link.qw, link.qx, link.qy, link.qz);
2949 link.radius = seg_radius;
2950 link.half_length = half_len;
2951 link.mass = params.wood_density * computeCapsuleVolume(seg_radius, half_len);
2952 link.inertia_diagonal = computeCapsuleInertia(link.mass, seg_radius, half_len);
2953
2954 // Extract visual mesh from tube geometry for this segment
2955 int tube_seg_idx = segment_tube_indices[seg];
2956 if (tube_seg_idx >= 0 && tube_seg_idx < total_tube_segments) {
2957 buildTubeSegmentVisualMesh(context_ptr, shoot->internode_tube_objID,
2958 tube_seg_idx, total_tube_segments, data, link);
2959 }
2960
2961 int parent_idx;
2962 if (prev_internode_link >= 0) {
2963 parent_idx = prev_internode_link;
2964 } else if (shoot_parent_link >= 0) {
2965 parent_idx = shoot_parent_link;
2966 } else {
2967 parent_idx = -1;
2968 }
2969 link.parent_link_index = parent_idx;
2970
2971 int this_link_idx = static_cast<int>(data.links.size());
2972 data.links.push_back(link);
2973
2974 USDJoint joint;
2975 joint.child_link_index = this_link_idx;
2976
2977 if (parent_idx < 0) {
2978 joint.name = "WorldAnchor";
2979 joint.parent_link_index = -1;
2980 joint.local_pos_parent = start;
2981 joint.local_pos_child = vec3(0, 0, -half_len);
2982 joint.stiffness = 0;
2983 joint.damping = 0;
2984 joint.is_fixed = true;
2985 } else {
2986 joint.name = "J_" + data.links[parent_idx].name + "_to_" + link.name;
2987 joint.parent_link_index = parent_idx;
2988 joint.local_pos_parent = vec3(0, 0, data.links[parent_idx].half_length);
2989 joint.local_pos_child = vec3(0, 0, -half_len);
2990
2991 float avg_radius = (data.links[parent_idx].radius + seg_radius) / 2.f;
2992 joint.stiffness = computeJointStiffness(params.elastic_modulus, avg_radius, length);
2993 float child_Izz = link.inertia_diagonal.x;
2994 joint.damping = computeJointDamping(joint.stiffness, child_Izz, params.damping_ratio);
2995 joint.is_fixed = false;
2996 computeJointLocalRot1(data.links[parent_idx], data.links[this_link_idx], joint);
2997 }
2998 data.joints.push_back(joint);
2999
3000 prev_internode_link = this_link_idx;
3001 }
3002
3003 // Always record the best available link for this phytomer node, even if
3004 // all its segments were filtered. This ensures child shoots attaching at this
3005 // node can find the nearest upstream link.
3006 if (prev_internode_link >= 0) {
3007 last_internode_link_index[{shoot->ID, static_cast<int>(phytomer_idx)}] = prev_internode_link;
3008 shoot_node_link_index[{shoot->ID, static_cast<int>(phytomer_idx)}] = prev_internode_link;
3009 shoot_last_link_index[shoot->ID] = prev_internode_link;
3010 }
3011
3012 // ---- Petioles (only if leaves are present) ---- //
3013
3014 int internode_attach_link = prev_internode_link;
3015
3016 for (uint petiole_idx = 0; petiole_idx < phytomer->petiole_vertices.size(); petiole_idx++) {
3017
3018 // Skip petioles that have no attached leaves
3019 bool has_leaves = false;
3020 if (petiole_idx < phytomer->leaf_objIDs.size()) {
3021 for (uint li = 0; li < phytomer->leaf_objIDs[petiole_idx].size(); li++) {
3022 if (phytomer->leaf_objIDs[petiole_idx][li] != 0) {
3023 has_leaves = true;
3024 break;
3025 }
3026 }
3027 }
3028 if (!has_leaves) {
3029 continue;
3030 }
3031
3032 const auto &pet_verts = phytomer->petiole_vertices[petiole_idx];
3033 const auto &pet_radii = phytomer->petiole_radii[petiole_idx];
3034 int prev_petiole_link = -1;
3035
3036 for (int seg = 0; seg < static_cast<int>(pet_verts.size()) - 1; seg++) {
3037 vec3 start = pet_verts[seg];
3038 vec3 end = pet_verts[seg + 1];
3039 vec3 axis = end - start;
3040 float length = axis.magnitude();
3041
3042 if (length < params.min_segment_length) {
3043 continue;
3044 }
3045
3046 float seg_radius = pet_radii[seg];
3047 if (!(seg_radius > 0) || seg_radius > length) {
3048 continue;
3049 }
3050
3051 vec3 midpoint = (start + end) * 0.5f;
3052 vec3 axis_normalized = axis / length;
3053 float half_len = length / 2.f;
3054
3055 USDLink link;
3056 link.name = "S" + std::to_string(shoot->ID) + "_P" + std::to_string(phytomer_idx) + "_Pet" + std::to_string(petiole_idx) + "_Seg" + std::to_string(seg);
3057 link.position = midpoint;
3058 axisToQuaternion(axis_normalized, link.qw, link.qx, link.qy, link.qz);
3059 link.radius = seg_radius;
3060 link.half_length = half_len;
3061 link.mass = params.wood_density * computeCapsuleVolume(seg_radius, half_len);
3062 link.inertia_diagonal = computeCapsuleInertia(link.mass, seg_radius, half_len);
3063
3064 // Extract visual mesh from petiole object, transforming to link's local frame
3065 if (petiole_idx < phytomer->petiole_objIDs.size() && seg < static_cast<int>(phytomer->petiole_objIDs[petiole_idx].size())) {
3066 uint pet_objID = phytomer->petiole_objIDs[petiole_idx][seg];
3067 if (context_ptr->doesObjectExist(pet_objID)) {
3068 USDLink temp_link;
3069 temp_link.position = link.position;
3070 buildMeshFromContextObject(context_ptr, pet_objID, data, temp_link);
3071 // Re-transform vertices into this link's rotated local frame
3072 link.mesh_vertices.resize(temp_link.mesh_vertices.size());
3073 for (size_t vi = 0; vi < temp_link.mesh_vertices.size(); vi++) {
3074 vec3 world_pos = temp_link.mesh_vertices[vi] + temp_link.position; // back to world
3075 link.mesh_vertices[vi] = worldToLocal(world_pos, link.position, link.qw, link.qx, link.qy, link.qz);
3076 }
3077 link.mesh_uvs = std::move(temp_link.mesh_uvs);
3078 link.mesh_face_vertex_counts = std::move(temp_link.mesh_face_vertex_counts);
3079 link.mesh_face_vertex_indices = std::move(temp_link.mesh_face_vertex_indices);
3080 link.material_index = temp_link.material_index;
3081 }
3082 }
3083
3084 int parent_idx = (prev_petiole_link >= 0) ? prev_petiole_link : internode_attach_link;
3085 // Skip if no valid parent link exists (e.g., all internode segments were filtered)
3086 if (parent_idx < 0) {
3087 continue;
3088 }
3089 link.parent_link_index = parent_idx;
3090
3091 int this_link_idx = static_cast<int>(data.links.size());
3092 data.links.push_back(link);
3093
3094 USDJoint joint;
3095 joint.name = "J_" + data.links[parent_idx].name + "_to_" + link.name;
3096 joint.parent_link_index = parent_idx;
3097 joint.child_link_index = this_link_idx;
3098 joint.local_pos_parent = vec3(0, 0, data.links[parent_idx].half_length);
3099 joint.local_pos_child = vec3(0, 0, -half_len);
3100 float avg_radius = (data.links[parent_idx].radius + seg_radius) / 2.f;
3101 joint.stiffness = computeJointStiffness(params.elastic_modulus, avg_radius, length);
3102 float child_Izz = link.inertia_diagonal.x;
3103 joint.damping = computeJointDamping(joint.stiffness, child_Izz, params.damping_ratio);
3104 joint.is_fixed = false;
3105 computeJointLocalRot1(data.links[parent_idx], data.links[this_link_idx], joint);
3106 data.joints.push_back(joint);
3107
3108 prev_petiole_link = this_link_idx;
3109 }
3110
3111 // ---- Leaves on this petiole ---- //
3112
3113 if (petiole_idx < phytomer->leaf_bases.size()) {
3114 for (uint leaf_idx = 0; leaf_idx < phytomer->leaf_bases[petiole_idx].size(); leaf_idx++) {
3115
3116 if (petiole_idx >= phytomer->leaf_objIDs.size() || leaf_idx >= phytomer->leaf_objIDs[petiole_idx].size()) {
3117 continue;
3118 }
3119 uint leaf_objID = phytomer->leaf_objIDs[petiole_idx][leaf_idx];
3120 if (leaf_objID == 0) {
3121 continue;
3122 }
3123
3124 float leaf_area = phytomer->getLeafArea();
3125 uint total_leaves = 0;
3126 for (uint pi = 0; pi < phytomer->leaf_bases.size(); pi++) {
3127 total_leaves += phytomer->leaf_bases[pi].size();
3128 }
3129 if (total_leaves > 0) {
3130 leaf_area /= static_cast<float>(total_leaves);
3131 }
3132
3133 float leaf_mass = params.leaf_mass_per_area * leaf_area;
3134 if (leaf_mass < 1e-6f) {
3135 continue;
3136 }
3137
3138 USDLink link;
3139 link.name = "S" + std::to_string(shoot->ID) + "_P" + std::to_string(phytomer_idx) + "_Pet" + std::to_string(petiole_idx) + "_Leaf" + std::to_string(leaf_idx);
3140 link.link_type = USD_LINK_MESH;
3141 link.qw = 1; link.qx = 0; link.qy = 0; link.qz = 0;
3142 link.radius = 0;
3143 link.half_length = 0;
3144 link.mass = leaf_mass;
3145
3146 link.position = buildMeshFromContextObject(context_ptr, leaf_objID, data, link);
3147 link.inertia_diagonal = computeMeshInertia(leaf_mass, link.mesh_vertices);
3148
3149 int parent_idx = (prev_petiole_link >= 0) ? prev_petiole_link : internode_attach_link;
3150 // Skip if no valid parent link exists (e.g., all internode segments were filtered)
3151 if (parent_idx < 0) {
3152 continue;
3153 }
3154 link.parent_link_index = parent_idx;
3155
3156 int this_link_idx = static_cast<int>(data.links.size());
3157 data.links.push_back(link);
3158
3159 USDJoint joint;
3160 joint.name = "J_" + data.links[parent_idx].name + "_to_" + link.name;
3161 joint.parent_link_index = parent_idx;
3162 joint.child_link_index = this_link_idx;
3163 joint.local_pos_parent = vec3(0, 0, data.links[parent_idx].half_length);
3164 joint.local_pos_child = vec3(0, 0, 0);
3165 joint.stiffness = params.organ_spring_stiffness;
3166 joint.damping = params.organ_spring_damping;
3167 joint.is_fixed = false;
3168 computeJointLocalRot1(data.links[parent_idx], data.links[this_link_idx], joint);
3169 data.joints.push_back(joint);
3170 }
3171 }
3172 }
3173
3174 // ---- Peduncles and inflorescences (only if flowers/fruit are present) ---- //
3175
3176 for (uint petiole_idx = 0; petiole_idx < phytomer->floral_buds.size(); petiole_idx++) {
3177 for (uint bud_idx = 0; bud_idx < phytomer->floral_buds[petiole_idx].size(); bud_idx++) {
3178
3179 const auto &fbud = phytomer->floral_buds[petiole_idx][bud_idx];
3180
3181 // Skip buds that have no flowers or fruit
3182 if (fbud.state != BUD_FRUITING && fbud.state != BUD_FLOWER_OPEN && fbud.state != BUD_FLOWER_CLOSED) {
3183 continue;
3184 }
3185
3186 int prev_peduncle_link = -1;
3187 if (petiole_idx < phytomer->peduncle_vertices.size() && bud_idx < phytomer->peduncle_vertices[petiole_idx].size()) {
3188
3189 const auto &ped_verts = phytomer->peduncle_vertices[petiole_idx][bud_idx];
3190 const auto &ped_radii = phytomer->peduncle_radii[petiole_idx][bud_idx];
3191
3192 for (int seg = 0; seg < static_cast<int>(ped_verts.size()) - 1; seg++) {
3193 vec3 start = ped_verts[seg];
3194 vec3 end = ped_verts[seg + 1];
3195 vec3 axis = end - start;
3196 float length = axis.magnitude();
3197
3198 if (length < params.min_segment_length) {
3199 continue;
3200 }
3201
3202 float seg_radius = ped_radii[seg];
3203 if (!(seg_radius > 0) || seg_radius > length) {
3204 continue;
3205 }
3206
3207 vec3 midpoint = (start + end) * 0.5f;
3208 vec3 axis_normalized = axis / length;
3209 float half_len = length / 2.f;
3210
3211 USDLink link;
3212 link.name = "S" + std::to_string(shoot->ID) + "_P" + std::to_string(phytomer_idx) + "_Ped" + std::to_string(petiole_idx) + "_B" + std::to_string(bud_idx) + "_Seg" + std::to_string(seg);
3213 link.position = midpoint;
3214 axisToQuaternion(axis_normalized, link.qw, link.qx, link.qy, link.qz);
3215 link.radius = seg_radius;
3216 link.half_length = half_len;
3217 link.mass = params.wood_density * computeCapsuleVolume(seg_radius, half_len);
3218 link.inertia_diagonal = computeCapsuleInertia(link.mass, seg_radius, half_len);
3219
3220 // Extract visual mesh from peduncle object, transforming to link's local frame
3221 if (seg < static_cast<int>(fbud.peduncle_objIDs.size())) {
3222 uint ped_objID = fbud.peduncle_objIDs[seg];
3223 if (context_ptr->doesObjectExist(ped_objID)) {
3224 USDLink temp_link;
3225 temp_link.position = link.position;
3226 buildMeshFromContextObject(context_ptr, ped_objID, data, temp_link);
3227 link.mesh_vertices.resize(temp_link.mesh_vertices.size());
3228 for (size_t vi = 0; vi < temp_link.mesh_vertices.size(); vi++) {
3229 vec3 world_pos = temp_link.mesh_vertices[vi] + temp_link.position;
3230 link.mesh_vertices[vi] = worldToLocal(world_pos, link.position, link.qw, link.qx, link.qy, link.qz);
3231 }
3232 link.mesh_uvs = std::move(temp_link.mesh_uvs);
3233 link.mesh_face_vertex_counts = std::move(temp_link.mesh_face_vertex_counts);
3234 link.mesh_face_vertex_indices = std::move(temp_link.mesh_face_vertex_indices);
3235 link.material_index = temp_link.material_index;
3236 }
3237 }
3238
3239 int parent_idx = (prev_peduncle_link >= 0) ? prev_peduncle_link : internode_attach_link;
3240 // Skip if no valid parent link exists (e.g., all internode segments were filtered)
3241 if (parent_idx < 0) {
3242 continue;
3243 }
3244 link.parent_link_index = parent_idx;
3245
3246 int this_link_idx = static_cast<int>(data.links.size());
3247 data.links.push_back(link);
3248
3249 USDJoint joint;
3250 joint.name = "J_" + data.links[parent_idx].name + "_to_" + link.name;
3251 joint.parent_link_index = parent_idx;
3252 joint.child_link_index = this_link_idx;
3253 joint.local_pos_parent = vec3(0, 0, data.links[parent_idx].half_length);
3254 joint.local_pos_child = vec3(0, 0, -half_len);
3255 float avg_radius = (data.links[parent_idx].radius + seg_radius) / 2.f;
3256 joint.stiffness = computeJointStiffness(params.elastic_modulus, avg_radius, length);
3257 float child_Izz = link.inertia_diagonal.x;
3258 joint.damping = computeJointDamping(joint.stiffness, child_Izz, params.damping_ratio);
3259 joint.is_fixed = false;
3260 computeJointLocalRot1(data.links[parent_idx], data.links[this_link_idx], joint);
3261 data.joints.push_back(joint);
3262
3263 prev_peduncle_link = this_link_idx;
3264 }
3265 }
3266
3267 int organ_attach = (prev_peduncle_link >= 0) ? prev_peduncle_link : internode_attach_link;
3268
3269 for (uint inf_idx = 0; inf_idx < fbud.inflorescence_bases.size(); inf_idx++) {
3270
3271 if (inf_idx >= fbud.inflorescence_objIDs.size() || fbud.inflorescence_objIDs[inf_idx] == 0) {
3272 continue;
3273 }
3274
3275 float organ_mass;
3276 std::string organ_label;
3277 if (fbud.state == BUD_FRUITING) {
3278 organ_mass = params.fruit_mass;
3279 organ_label = "Fruit";
3280 } else if (fbud.state == BUD_FLOWER_OPEN || fbud.state == BUD_FLOWER_CLOSED) {
3281 organ_mass = params.flower_mass;
3282 organ_label = "Flower";
3283 } else {
3284 continue;
3285 }
3286
3287 // Skip if no valid parent link exists (e.g., all internode segments were filtered)
3288 if (organ_attach < 0) {
3289 continue;
3290 }
3291
3292 uint organ_objID = fbud.inflorescence_objIDs[inf_idx];
3293
3294 USDLink link;
3295 link.name = "S" + std::to_string(shoot->ID) + "_P" + std::to_string(phytomer_idx) + "_" + organ_label + std::to_string(inf_idx);
3296 link.link_type = USD_LINK_MESH;
3297 link.qw = 1; link.qx = 0; link.qy = 0; link.qz = 0;
3298 link.radius = 0;
3299 link.half_length = 0;
3300 link.mass = organ_mass;
3301
3302 link.position = buildMeshFromContextObject(context_ptr, organ_objID, data, link);
3303 link.inertia_diagonal = computeMeshInertia(organ_mass, link.mesh_vertices);
3304 link.parent_link_index = organ_attach;
3305
3306 int this_link_idx = static_cast<int>(data.links.size());
3307 data.links.push_back(link);
3308
3309 USDJoint joint;
3310 joint.name = "J_" + data.links[organ_attach].name + "_to_" + link.name;
3311 joint.parent_link_index = organ_attach;
3312 joint.child_link_index = this_link_idx;
3313 joint.local_pos_parent = vec3(0, 0, data.links[organ_attach].half_length);
3314 joint.local_pos_child = vec3(0, 0, 0);
3315 joint.stiffness = params.organ_spring_stiffness;
3316 joint.damping = params.organ_spring_damping;
3317 joint.is_fixed = false;
3318 computeJointLocalRot1(data.links[organ_attach], data.links[this_link_idx], joint);
3319 data.joints.push_back(joint);
3320 }
3321 }
3322 }
3323 }
3324 }
3325
3326 return data;
3327}
3328
3329void writeUSDAHeader(std::ofstream &out, const std::string &default_prim) {
3330 out << "#usda 1.0\n";
3331 out << "(\n";
3332 out << " defaultPrim = \"" << default_prim << "\"\n";
3333 out << " metersPerUnit = 1.0\n";
3334 out << " kilogramsPerUnit = 1.0\n";
3335 out << " upAxis = \"Z\"\n";
3336 out << ")\n\n";
3337}
3338
3339void writePhysicsScene(std::ofstream &out) {
3340 out << "def PhysicsScene \"PhysicsScene\"\n";
3341 out << "{\n";
3342 out << " vector3f physics:gravityDirection = (0, 0, -1)\n";
3343 out << " float physics:gravityMagnitude = 9.81\n";
3344 out << "}\n\n";
3345}
3346
3347void writePhysicsMaterial(std::ofstream &out, const USDExportParameters &params,
3348 const std::string &indent) {
3349 out << indent << "def Scope \"PhysicsMaterials\"\n";
3350 out << indent << "{\n";
3351 out << indent << " def Material \"WoodMaterial\" (\n";
3352 out << indent << " prepend apiSchemas = [\"PhysicsMaterialAPI\"]\n";
3353 out << indent << " )\n";
3354 out << indent << " {\n";
3355 out << indent << " float physics:staticFriction = " << params.static_friction << "\n";
3356 out << indent << " float physics:dynamicFriction = " << params.dynamic_friction << "\n";
3357 out << indent << " float physics:restitution = " << params.restitution << "\n";
3358 out << indent << " }\n";
3359 out << indent << "}\n\n";
3360}
3361
3362// Compute smooth per-vertex normals in faceVarying layout (one entry per face-vertex).
3363// For each vertex, the normal is the area-weighted average of the normals of all faces
3364// that share that vertex — this produces smooth shading across curved surfaces like tubes.
3365// The result buffer has exactly sum(faceVertexCounts) entries, matching the faceVarying
3366// interpolation contract required by USD and Isaac Sim.
3367std::vector<vec3> computeFaceVaryingNormals(const std::vector<vec3> &verts,
3368 const std::vector<int> &face_vertex_counts,
3369 const std::vector<int> &face_vertex_indices) {
3370
3371 // Accumulate area-weighted face normals per vertex
3372 std::vector<vec3> vertex_normals(verts.size(), make_vec3(0, 0, 0));
3373
3374 size_t idx_offset = 0;
3375 for (int count : face_vertex_counts) {
3376 if (count >= 3 && idx_offset + 2 < face_vertex_indices.size()) {
3377 vec3 v0 = verts[face_vertex_indices[idx_offset + 0]];
3378 vec3 v1 = verts[face_vertex_indices[idx_offset + 1]];
3379 vec3 v2 = verts[face_vertex_indices[idx_offset + 2]];
3380 // Cross product is proportional to face area — area-weighting comes for free
3381 vec3 weighted_normal = cross(v1 - v0, v2 - v0);
3382 for (int k = 0; k < count; k++) {
3383 int vi = face_vertex_indices[idx_offset + k];
3384 if (vi >= 0 && vi < static_cast<int>(vertex_normals.size())) {
3385 vertex_normals[vi] = vertex_normals[vi] + weighted_normal;
3386 }
3387 }
3388 }
3389 idx_offset += static_cast<size_t>(count);
3390 }
3391
3392 // Normalize accumulated vertex normals
3393 for (auto &n : vertex_normals) {
3394 float len = n.magnitude();
3395 if (len > 1e-10f) {
3396 n = n / len;
3397 } else {
3398 n = make_vec3(0, 0, 1); // degenerate vertex — emit up-vector
3399 }
3400 }
3401
3402 // Build faceVarying buffer: one entry per face-vertex
3403 std::vector<vec3> normals;
3404 normals.reserve(face_vertex_indices.size());
3405 for (int vi : face_vertex_indices) {
3406 if (vi >= 0 && vi < static_cast<int>(vertex_normals.size())) {
3407 normals.push_back(vertex_normals[vi]);
3408 } else {
3409 normals.push_back(make_vec3(0, 0, 1));
3410 }
3411 }
3412
3413 return normals;
3414}
3415
3416// Build indexed primvar: deduplicate values and return (unique_values, indices).
3417// Reduces file size and satisfies the IndexedPrimvarChecker requirement.
3418template <typename T>
3419std::pair<std::vector<T>, std::vector<int>> buildIndexedPrimvar(const std::vector<T> &values) {
3420 std::vector<T> unique_vals;
3421 std::vector<int> indices;
3422 indices.reserve(values.size());
3423
3424 // Simple linear search — meshes are small enough that this is fine
3425 for (const auto &v : values) {
3426 int found = -1;
3427 for (int i = 0; i < static_cast<int>(unique_vals.size()); i++) {
3428 if (unique_vals[i].x == v.x && unique_vals[i].y == v.y && unique_vals[i].z == v.z) {
3429 found = i; break;
3430 }
3431 }
3432 if (found < 0) {
3433 found = static_cast<int>(unique_vals.size());
3434 unique_vals.push_back(v);
3435 }
3436 indices.push_back(found);
3437 }
3438 return {unique_vals, indices};
3439}
3440
3441// Specialisation for vec2 (UVs) — no z component
3442std::pair<std::vector<vec2>, std::vector<int>> buildIndexedPrimvarVec2(const std::vector<vec2> &values) {
3443 std::vector<vec2> unique_vals;
3444 std::vector<int> indices;
3445 indices.reserve(values.size());
3446 for (const auto &v : values) {
3447 int found = -1;
3448 for (int i = 0; i < static_cast<int>(unique_vals.size()); i++) {
3449 if (unique_vals[i].x == v.x && unique_vals[i].y == v.y) { found = i; break; }
3450 }
3451 if (found < 0) { found = static_cast<int>(unique_vals.size()); unique_vals.push_back(v); }
3452 indices.push_back(found);
3453 }
3454 return {unique_vals, indices};
3455}
3456
3457void writeMeshGeometry(std::ofstream &out, const USDLink &link,
3458 const std::vector<USDMaterial> &materials, const std::string &inner2,
3459 const std::string &plant_prim) {
3460 // Points
3461 out << inner2 << "point3f[] points = [\n";
3462 for (size_t i = 0; i < link.mesh_vertices.size(); i++) {
3463 out << inner2 << " (" << link.mesh_vertices[i].x << ", " << link.mesh_vertices[i].y << ", " << link.mesh_vertices[i].z << ")";
3464 if (i + 1 < link.mesh_vertices.size()) out << ",";
3465 out << "\n";
3466 }
3467 out << inner2 << "]\n";
3468
3469 // Extent (bounding box) — required by ExtentsChecker
3470 if (!link.mesh_vertices.empty()) {
3471 float xmin = link.mesh_vertices[0].x, xmax = xmin;
3472 float ymin = link.mesh_vertices[0].y, ymax = ymin;
3473 float zmin = link.mesh_vertices[0].z, zmax = zmin;
3474 for (const auto &v : link.mesh_vertices) {
3475 xmin = std::min(xmin, v.x); xmax = std::max(xmax, v.x);
3476 ymin = std::min(ymin, v.y); ymax = std::max(ymax, v.y);
3477 zmin = std::min(zmin, v.z); zmax = std::max(zmax, v.z);
3478 }
3479 out << inner2 << "float3[] extent = [(" << xmin << ", " << ymin << ", " << zmin
3480 << "), (" << xmax << ", " << ymax << ", " << zmax << ")]\n";
3481 }
3482
3483 out << inner2 << "int[] faceVertexCounts = [";
3484 for (size_t i = 0; i < link.mesh_face_vertex_counts.size(); i++) {
3485 if (i > 0) out << ", ";
3486 out << link.mesh_face_vertex_counts[i];
3487 }
3488 out << "]\n";
3489
3490 out << inner2 << "int[] faceVertexIndices = [";
3491 for (size_t i = 0; i < link.mesh_face_vertex_indices.size(); i++) {
3492 if (i > 0) out << ", ";
3493 out << link.mesh_face_vertex_indices[i];
3494 }
3495 out << "]\n";
3496
3497 // Normals — indexed faceVarying
3498 std::vector<vec3> normals = computeFaceVaryingNormals(link.mesh_vertices, link.mesh_face_vertex_counts, link.mesh_face_vertex_indices);
3499 if (!normals.empty()) {
3500 auto [unique_normals, normal_indices] = buildIndexedPrimvar(normals);
3501 out << inner2 << "normal3f[] primvars:normals = [";
3502 for (size_t i = 0; i < unique_normals.size(); i++) {
3503 if (i > 0) out << ", ";
3504 out << "(" << unique_normals[i].x << ", " << unique_normals[i].y << ", " << unique_normals[i].z << ")";
3505 }
3506 out << "] (\n";
3507 out << inner2 << " interpolation = \"faceVarying\"\n";
3508 out << inner2 << ")\n";
3509 out << inner2 << "int[] primvars:normals:indices = [";
3510 for (size_t i = 0; i < normal_indices.size(); i++) {
3511 if (i > 0) out << ", ";
3512 out << normal_indices[i];
3513 }
3514 out << "]\n";
3515 }
3516
3517 // UVs — indexed vertex
3518 if (!link.mesh_uvs.empty()) {
3519 auto [unique_uvs, uv_indices] = buildIndexedPrimvarVec2(link.mesh_uvs);
3520 out << inner2 << "texCoord2f[] primvars:st = [";
3521 for (size_t i = 0; i < unique_uvs.size(); i++) {
3522 if (i > 0) out << ", ";
3523 out << "(" << unique_uvs[i].x << ", " << unique_uvs[i].y << ")";
3524 }
3525 out << "] (\n";
3526 out << inner2 << " interpolation = \"vertex\"\n";
3527 out << inner2 << ")\n";
3528 out << inner2 << "int[] primvars:st:indices = [";
3529 for (size_t i = 0; i < uv_indices.size(); i++) {
3530 if (i > 0) out << ", ";
3531 out << uv_indices[i];
3532 }
3533 out << "]\n";
3534 }
3535
3536 if (link.material_index >= 0 && link.material_index < static_cast<int>(materials.size())) {
3537 out << inner2 << "rel material:binding = </" << plant_prim << "/Materials/" << materials[link.material_index].name << ">\n";
3538 }
3539}
3540
3541void writeVisualMaterials(std::ofstream &out, const std::vector<USDMaterial> &materials,
3542 const std::string &output_dir, const std::string &plant_prim,
3543 const std::string &indent) {
3544 if (materials.empty()) {
3545 return;
3546 }
3547
3548 std::string inner = indent + " ";
3549 std::string inner2 = inner + " ";
3550 std::string inner3 = inner2 + " ";
3551
3552 out << indent << "def Scope \"Materials\"\n";
3553 out << indent << "{\n";
3554
3555 // Copy texture files into a "textures" subdirectory beside the output file
3556 // so the USDA is self-contained and portable to other machines.
3557 std::filesystem::path tex_dir = std::filesystem::path(output_dir) / "textures";
3558 bool textures_copied = false;
3559
3560 for (const auto &mat : materials) {
3561 std::string tex_path;
3562 if (!mat.texture_file.empty()) {
3563 std::filesystem::path src(mat.texture_file);
3564 if (std::filesystem::exists(src)) {
3565 if (!textures_copied) {
3566 std::filesystem::create_directories(tex_dir);
3567 textures_copied = true;
3568 }
3569 std::filesystem::path dst = tex_dir / src.filename();
3570 // Only copy if source and destination differ
3571 if (!std::filesystem::exists(dst) || !std::filesystem::equivalent(src, dst)) {
3572 std::filesystem::copy_file(src, dst, std::filesystem::copy_options::overwrite_existing);
3573 }
3574 // USD relative path anchored to the file location
3575 tex_path = "./textures/" + src.filename().string();
3576 } else {
3577 // Source file doesn't exist — write the path as-is and hope the user fixes it
3578 tex_path = mat.texture_file;
3579 }
3580 }
3581
3582 std::string mat_base = "/" + plant_prim + "/Materials/" + mat.name;
3583
3584 out << inner << "def Material \"" << mat.name << "\"\n";
3585 out << inner << "{\n";
3586
3587 out << inner2 << "token outputs:surface.connect = <" << mat_base << "/PreviewSurface.outputs:surface>\n";
3588 out << "\n";
3589
3590 out << inner2 << "def Shader \"PreviewSurface\"\n";
3591 out << inner2 << "{\n";
3592 out << inner3 << "uniform token info:id = \"UsdPreviewSurface\"\n";
3593 out << inner3 << "color3f inputs:diffuseColor = (" << mat.color.r << ", " << mat.color.g << ", " << mat.color.b << ")\n";
3594
3595 if (!tex_path.empty()) {
3596 out << inner3 << "color3f inputs:diffuseColor.connect = <" << mat_base << "/DiffuseTexture.outputs:rgb>\n";
3597 out << inner3 << "float inputs:opacity.connect = <" << mat_base << "/DiffuseTexture.outputs:a>\n";
3598 out << inner3 << "float inputs:opacityThreshold = 0.5\n";
3599 }
3600
3601 out << inner3 << "float inputs:roughness = 0.8\n";
3602 out << inner3 << "float inputs:metallic = 0.0\n";
3603 out << inner3 << "token outputs:surface\n";
3604 out << inner2 << "}\n";
3605
3606 if (!tex_path.empty()) {
3607 out << "\n";
3608 out << inner2 << "def Shader \"DiffuseTexture\"\n";
3609 out << inner2 << "{\n";
3610 out << inner3 << "uniform token info:id = \"UsdUVTexture\"\n";
3611 out << inner3 << "asset inputs:file = @" << tex_path << "@\n";
3612 out << inner3 << "float2 inputs:st.connect = <" << mat_base << "/TexCoordReader.outputs:result>\n";
3613 out << inner3 << "token inputs:wrapS = \"repeat\"\n";
3614 out << inner3 << "token inputs:wrapT = \"repeat\"\n";
3615 out << inner3 << "float3 outputs:rgb\n";
3616 out << inner3 << "float outputs:a\n";
3617 out << inner2 << "}\n";
3618 out << "\n";
3619 out << inner2 << "def Shader \"TexCoordReader\"\n";
3620 out << inner2 << "{\n";
3621 out << inner3 << "uniform token info:id = \"UsdPrimvarReader_float2\"\n";
3622 out << inner3 << "string inputs:varname = \"st\"\n";
3623 out << inner3 << "float2 outputs:result\n";
3624 out << inner2 << "}\n";
3625 }
3626
3627 out << inner << "}\n";
3628 }
3629
3630 out << indent << "}\n\n";
3631}
3632
3633void writeLinkPrim(std::ofstream &out, const USDLink &link, const std::vector<USDMaterial> &materials,
3634 const std::string &plant_prim, const std::string &indent) {
3635 out << indent << "def Xform \"" << link.name << "\" (\n";
3636 out << indent << " prepend apiSchemas = [\"PhysicsRigidBodyAPI\", \"PhysicsMassAPI\"]\n";
3637 out << indent << ")\n";
3638 out << indent << "{\n";
3639
3640 std::string inner = indent + " ";
3641 std::string inner2 = inner + " ";
3642
3643 out << std::fixed << std::setprecision(6);
3644 out << inner << "point3f xformOp:translate = (" << link.position.x << ", " << link.position.y << ", " << link.position.z << ")\n";
3645 out << inner << "quatf xformOp:orient = (" << link.qw << ", " << link.qx << ", " << link.qy << ", " << link.qz << ")\n";
3646 out << inner << "uniform token[] xformOpOrder = [\"xformOp:translate\", \"xformOp:orient\"]\n";
3647
3648 // Enforce a minimum mass so PhysX doesn't get a zero-mass rigid body
3649 float written_mass = std::max(link.mass, 1e-6f);
3650 out << inner << "float physics:mass = " << written_mass << "\n";
3651
3652 // Only author diagonalInertia + principalAxes when all components are large enough
3653 // to survive 6-decimal formatting. When omitted, PhysX auto-computes from the collision shape.
3654 float inertia_floor = 1e-6f;
3655 bool has_positive_inertia = link.inertia_diagonal.x > inertia_floor
3656 && link.inertia_diagonal.y > inertia_floor
3657 && link.inertia_diagonal.z > inertia_floor;
3658 if (has_positive_inertia) {
3659 out << inner << "float3 physics:diagonalInertia = (" << link.inertia_diagonal.x << ", " << link.inertia_diagonal.y << ", " << link.inertia_diagonal.z << ")\n";
3660 out << inner << "quatf physics:principalAxes = (1, 0, 0, 0)\n";
3661 }
3662
3663 out << "\n";
3664
3665 std::string phys_mat_path = "</" + plant_prim + "/PhysicsMaterials/WoodMaterial>";
3666
3667 if (link.link_type == USD_LINK_MESH && !link.mesh_vertices.empty()) {
3668 // Visual mesh for rendering
3669 out << inner << "def Mesh \"Visual\" (\n";
3670 out << inner << " prepend apiSchemas = [\"MaterialBindingAPI\"]\n";
3671 out << inner << ")\n";
3672 out << inner << "{\n";
3673 out << inner2 << "uniform token subdivisionScheme = \"none\"\n";
3674 out << inner2 << "bool doubleSided = 1\n";
3675 writeMeshGeometry(out, link, materials, inner2, plant_prim);
3676 out << inner << "}\n\n";
3677
3678 // Collision mesh for physics (convex hull approximation, hidden from rendering)
3679 out << inner << "def Mesh \"Collision\" (\n";
3680 out << inner << " prepend apiSchemas = [\"MaterialBindingAPI\", \"PhysicsCollisionAPI\", \"PhysxCollisionAPI\"]\n";
3681 out << inner << ")\n";
3682 out << inner << "{\n";
3683 out << inner2 << "uniform token subdivisionScheme = \"none\"\n";
3684 out << inner2 << "uniform token purpose = \"guide\"\n";
3685
3686 out << inner2 << "point3f[] points = [\n";
3687 for (size_t i = 0; i < link.mesh_vertices.size(); i++) {
3688 out << inner2 << " (" << link.mesh_vertices[i].x << ", " << link.mesh_vertices[i].y << ", " << link.mesh_vertices[i].z << ")";
3689 if (i + 1 < link.mesh_vertices.size()) out << ",";
3690 out << "\n";
3691 }
3692 out << inner2 << "]\n";
3693
3694 // Extent for collision mesh
3695 {
3696 float xmin = link.mesh_vertices[0].x, xmax = xmin;
3697 float ymin = link.mesh_vertices[0].y, ymax = ymin;
3698 float zmin = link.mesh_vertices[0].z, zmax = zmin;
3699 for (const auto &v : link.mesh_vertices) {
3700 xmin = std::min(xmin, v.x); xmax = std::max(xmax, v.x);
3701 ymin = std::min(ymin, v.y); ymax = std::max(ymax, v.y);
3702 zmin = std::min(zmin, v.z); zmax = std::max(zmax, v.z);
3703 }
3704 out << inner2 << "float3[] extent = [(" << xmin << ", " << ymin << ", " << zmin
3705 << "), (" << xmax << ", " << ymax << ", " << zmax << ")]\n";
3706 }
3707
3708 out << inner2 << "int[] faceVertexCounts = [";
3709 for (size_t i = 0; i < link.mesh_face_vertex_counts.size(); i++) {
3710 if (i > 0) out << ", ";
3711 out << link.mesh_face_vertex_counts[i];
3712 }
3713 out << "]\n";
3714
3715 out << inner2 << "int[] faceVertexIndices = [";
3716 for (size_t i = 0; i < link.mesh_face_vertex_indices.size(); i++) {
3717 if (i > 0) out << ", ";
3718 out << link.mesh_face_vertex_indices[i];
3719 }
3720 out << "]\n";
3721
3722 out << inner2 << "uniform token physics:approximation = \"convexHull\"\n";
3723 out << inner2 << "rel material:binding:physics = " << phys_mat_path << "\n";
3724 out << inner << "}\n";
3725 } else if (link.link_type == USD_LINK_CAPSULE) {
3726 // Visual mesh (if available from tube geometry)
3727 if (!link.mesh_vertices.empty()) {
3728 out << inner << "def Mesh \"Visual\" (\n";
3729 out << inner << " prepend apiSchemas = [\"MaterialBindingAPI\"]\n";
3730 out << inner << ")\n";
3731 out << inner << "{\n";
3732 out << inner2 << "uniform token subdivisionScheme = \"none\"\n";
3733 out << inner2 << "bool doubleSided = 1\n";
3734 writeMeshGeometry(out, link, materials, inner2, plant_prim);
3735 out << inner << "}\n\n";
3736 }
3737
3738 // Collision capsule (hidden from rendering)
3739 out << inner << "def Capsule \"Collision\" (\n";
3740 out << inner << " prepend apiSchemas = [\"MaterialBindingAPI\", \"PhysicsCollisionAPI\"]\n";
3741 out << inner << ")\n";
3742 out << inner << "{\n";
3743 out << inner << " uniform token purpose = \"guide\"\n";
3744 out << inner << " double radius = " << link.radius << "\n";
3745 out << inner << " double height = " << (2.f * link.half_length) << "\n";
3746 out << inner << " uniform token axis = \"Z\"\n";
3747 out << inner << " rel material:binding:physics = " << phys_mat_path << "\n";
3748 out << inner << "}\n";
3749 }
3750
3751 out << indent << "}\n";
3752}
3753
3754void writeFixedJoint(std::ofstream &out, const USDJoint &joint, const std::vector<USDLink> &links, const std::string &plant_prim, const std::string &indent) {
3755 out << indent << "def PhysicsFixedJoint \"" << joint.name << "\"\n";
3756 out << indent << "{\n";
3757
3758 std::string inner = indent + " ";
3759 out << std::fixed << std::setprecision(6);
3760 out << inner << "rel physics:body1 = </" << plant_prim << "/Links/" << links[joint.child_link_index].name << ">\n";
3761 out << inner << "point3f physics:localPos0 = (" << joint.local_pos_parent.x << ", " << joint.local_pos_parent.y << ", " << joint.local_pos_parent.z << ")\n";
3762 out << inner << "point3f physics:localPos1 = (" << joint.local_pos_child.x << ", " << joint.local_pos_child.y << ", " << joint.local_pos_child.z << ")\n";
3763 out << inner << "quatf physics:localRot0 = (1, 0, 0, 0)\n";
3764 out << inner << "quatf physics:localRot1 = (" << joint.local_rot1_w << ", " << joint.local_rot1_x << ", " << joint.local_rot1_y << ", " << joint.local_rot1_z << ")\n";
3765 out << inner << "bool physics:collisionEnabled = false\n";
3766 out << indent << "}\n";
3767}
3768
3769void writeSphericalJoint(std::ofstream &out, const USDJoint &joint, const std::vector<USDLink> &links, const std::string &plant_prim, const std::string &indent) {
3770 out << indent << "def PhysicsSphericalJoint \"" << joint.name << "\" (\n";
3771 out << indent << " prepend apiSchemas = [\"PhysicsLimitAPI:cone\", \"PhysicsDriveAPI:angular\"]\n";
3772 out << indent << ")\n";
3773 out << indent << "{\n";
3774
3775 std::string inner = indent + " ";
3776 out << std::fixed << std::setprecision(6);
3777 out << inner << "rel physics:body0 = </" << plant_prim << "/Links/" << links[joint.parent_link_index].name << ">\n";
3778 out << inner << "rel physics:body1 = </" << plant_prim << "/Links/" << links[joint.child_link_index].name << ">\n";
3779 out << inner << "point3f physics:localPos0 = (" << joint.local_pos_parent.x << ", " << joint.local_pos_parent.y << ", " << joint.local_pos_parent.z << ")\n";
3780 out << inner << "point3f physics:localPos1 = (" << joint.local_pos_child.x << ", " << joint.local_pos_child.y << ", " << joint.local_pos_child.z << ")\n";
3781 out << inner << "quatf physics:localRot0 = (1, 0, 0, 0)\n";
3782 out << inner << "quatf physics:localRot1 = (" << joint.local_rot1_w << ", " << joint.local_rot1_x << ", " << joint.local_rot1_y << ", " << joint.local_rot1_z << ")\n";
3783 out << inner << "bool physics:collisionEnabled = false\n";
3784
3785 out << "\n";
3786
3787 // Cone limit: constrain swing range
3788 out << inner << "float limit:cone:physics:yAngle = 60\n";
3789 out << inner << "float limit:cone:physics:zAngle = 60\n";
3790
3791 out << "\n";
3792
3793 // Angular drive: spring-damper to restore rest pose
3794 out << inner << "uniform token drive:angular:physics:type = \"force\"\n";
3795 out << inner << "float drive:angular:physics:stiffness = " << joint.stiffness << "\n";
3796 out << inner << "float drive:angular:physics:damping = " << joint.damping << "\n";
3797 out << inner << "float drive:angular:physics:targetPosition = 0\n";
3798
3799 out << indent << "}\n";
3800}
3801
3802} // anonymous namespace
3803
3804void PlantArchitecture::writePlantStructureUSD(uint plantID, const std::string &filename, const USDExportParameters &params) const {
3805
3806 if (plant_instances.find(plantID) == plant_instances.end()) {
3807 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureUSD): Plant ID " + std::to_string(plantID) + " does not exist.");
3808 }
3809
3810 const auto &plant = plant_instances.at(plantID);
3811
3812 if (plant.shoot_tree.empty()) {
3813 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureUSD): Plant has no shoots to export.");
3814 }
3815
3816 std::string output_file = filename;
3817 if (!validateOutputPath(output_file, {".usda", ".USDA"})) {
3818 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureUSD): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
3819 } else if (getFileName(output_file).empty()) {
3820 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureUSD): The output file given was a directory. This argument should be the path to a file.");
3821 }
3822
3823 std::ofstream out(output_file);
3824 if (!out.is_open()) {
3825 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureUSD): Could not open file " + output_file + " for writing.");
3826 }
3827
3828 ArticulationData artic = buildArticulationData(plant, params, context_ptr);
3829
3830 if (artic.links.empty()) {
3831 helios_runtime_error("ERROR (PlantArchitecture::writePlantStructureUSD): No valid segments found in plant. All segments may be shorter than min_segment_length.");
3832 }
3833
3834 std::string plant_prim = sanitizePrimName("Plant_" + std::to_string(plantID));
3835
3836 writeUSDAHeader(out, plant_prim);
3837 writePhysicsScene(out);
3838
3839 out << "def Xform \"" << plant_prim << "\" (\n";
3840 out << " prepend apiSchemas = [\"PhysicsArticulationRootAPI\", \"PhysxArticulationAPI\"]\n";
3841 out << ")\n";
3842 out << "{\n";
3843 out << " bool physxArticulation:enabledSelfCollisions = false\n";
3844 out << " int physxArticulation:solverPositionIterationCount = " << params.solver_position_iterations << "\n";
3845 out << "\n";
3846
3847 // Nest PhysicsMaterials and Materials inside the default prim so referencing it is self-contained
3848 writePhysicsMaterial(out, params, " ");
3849 writeVisualMaterials(out, artic.materials, getFilePath(output_file), plant_prim, " ");
3850
3851 out << " def Xform \"Links\"\n";
3852 out << " {\n";
3853 for (const auto &link : artic.links) {
3854 writeLinkPrim(out, link, artic.materials, plant_prim, " ");
3855 out << "\n";
3856 }
3857 out << " }\n";
3858 out << "\n";
3859
3860 out << " def Xform \"Joints\"\n";
3861 out << " {\n";
3862 for (const auto &joint : artic.joints) {
3863 if (joint.is_fixed) {
3864 writeFixedJoint(out, joint, artic.links, plant_prim, " ");
3865 } else {
3866 writeSphericalJoint(out, joint, artic.links, plant_prim, " ");
3867 }
3868 out << "\n";
3869 }
3870 out << " }\n";
3871
3872 out << "}\n";
3873
3874 out.close();
3875
3876 if (printmessages) {
3877 std::cout << "Wrote USD articulated plant to " << output_file << " (" << artic.links.size() << " links, " << artic.joints.size() << " joints)" << std::endl;
3878 }
3879}
3880
3881// ============================================================================
3882// Growth Animation USD Export
3883// ============================================================================
3884
3885void PlantArchitecture::registerGrowthFrame(uint plantID, float min_segment_length) {
3886
3887 if (plant_instances.find(plantID) == plant_instances.end()) {
3888 helios_runtime_error("ERROR (PlantArchitecture::registerGrowthFrame): Plant ID " + std::to_string(plantID) + " does not exist.");
3889 }
3890
3891 const auto &plant = plant_instances.at(plantID);
3892
3893 if (plant.shoot_tree.empty()) {
3894 helios_runtime_error("ERROR (PlantArchitecture::registerGrowthFrame): Plant has no shoots to capture.");
3895 }
3896
3897 USDExportParameters params;
3898 params.min_segment_length = min_segment_length;
3899
3900 auto frame = std::make_shared<GrowthFrameSnapshot>();
3901 frame->time = plant.current_age;
3902 frame->artic_data = buildArticulationData(plant, params, context_ptr);
3903
3904 growth_animation_storage.plant_frames[plantID].push_back(std::move(frame));
3905
3906 if (printmessages) {
3907 std::cout << "Registered growth frame " << growth_animation_storage.plant_frames[plantID].size()
3908 << " for plant " << plantID << " (age=" << plant.current_age << " days, "
3909 << growth_animation_storage.plant_frames[plantID].back()->artic_data.links.size() << " links)" << std::endl;
3910 }
3911}
3912
3914 growth_animation_storage.plant_frames.erase(plantID);
3915}
3916
3918 auto it = growth_animation_storage.plant_frames.find(plantID);
3919 if (it == growth_animation_storage.plant_frames.end()) {
3920 return 0;
3921 }
3922 return it->second.size();
3923}
3924
3925void PlantArchitecture::writePlantGrowthUSD(uint plantID, const std::string &filename, float seconds_per_frame) const {
3926
3927 if (plant_instances.find(plantID) == plant_instances.end()) {
3928 helios_runtime_error("ERROR (PlantArchitecture::writePlantGrowthUSD): Plant ID " + std::to_string(plantID) + " does not exist.");
3929 }
3930
3931 auto frames_it = growth_animation_storage.plant_frames.find(plantID);
3932 if (frames_it == growth_animation_storage.plant_frames.end() || frames_it->second.empty()) {
3933 helios_runtime_error("ERROR (PlantArchitecture::writePlantGrowthUSD): No growth frames registered for plant " + std::to_string(plantID) + ". Call registerGrowthFrame() first.");
3934 }
3935
3936 const auto &frames = frames_it->second;
3937 uint num_frames = frames.size();
3938
3939 std::string output_file = filename;
3940 if (!validateOutputPath(output_file, {".usda", ".USDA"})) {
3941 helios_runtime_error("ERROR (PlantArchitecture::writePlantGrowthUSD): Could not open file " + filename + " for writing. Make sure the directory exists and is writable.");
3942 } else if (getFileName(output_file).empty()) {
3943 helios_runtime_error("ERROR (PlantArchitecture::writePlantGrowthUSD): The output file given was a directory. This argument should be the path to a file.");
3944 }
3945
3946 // --- Build superset topology ---
3947 // Collect all unique link names across all frames, and for each link keep the data from the last frame it appears in (canonical data).
3948 std::vector<std::string> link_order; // ordered list of unique link names
3949 std::map<std::string, USDLink> canonical_links; // link name -> canonical mesh/material data
3950 std::map<std::string, int> canonical_material_index; // link name -> material index in merged material list
3951
3952 // Merged material list across all frames
3953 std::vector<USDMaterial> all_materials;
3954 std::map<std::string, int> material_merge_cache; // key -> index in all_materials
3955
3956 // For each frame, track which links are present and their transforms
3957 // link_presence[link_name][frame_idx] = true if link exists at that frame
3958 std::map<std::string, std::vector<bool>> link_presence;
3959 // link_transforms[link_name][frame_idx] = (position, quaternion)
3960 struct LinkTransform {
3961 vec3 position;
3962 float qw, qx, qy, qz;
3963 };
3964 std::map<std::string, std::vector<LinkTransform>> link_transforms;
3965
3966 for (uint f = 0; f < num_frames; f++) {
3967 const auto &artic = frames[f]->artic_data;
3968
3969 // Merge materials from this frame
3970 std::map<int, int> frame_to_merged_mat; // frame-local material index -> merged index
3971 for (int m = 0; m < (int)artic.materials.size(); m++) {
3972 const auto &mat = artic.materials[m];
3973 std::string key = mat.texture_file + "|" + std::to_string(mat.color.r) + "," + std::to_string(mat.color.g) + "," + std::to_string(mat.color.b);
3974 auto cache_it = material_merge_cache.find(key);
3975 if (cache_it != material_merge_cache.end()) {
3976 frame_to_merged_mat[m] = cache_it->second;
3977 } else {
3978 int idx = (int)all_materials.size();
3979 all_materials.push_back(mat);
3980 // Ensure unique material name
3981 all_materials.back().name = sanitizePrimName("Mat_" + std::to_string(idx));
3982 material_merge_cache[key] = idx;
3983 frame_to_merged_mat[m] = idx;
3984 }
3985 }
3986
3987 for (const auto &link : artic.links) {
3988 // Initialize presence/transform vectors if first time seeing this link
3989 if (link_presence.find(link.name) == link_presence.end()) {
3990 link_order.push_back(link.name);
3991 link_presence[link.name].resize(num_frames, false);
3992 link_transforms[link.name].resize(num_frames);
3993 }
3994
3995 link_presence[link.name][f] = true;
3996 link_transforms[link.name][f] = {link.position, link.qw, link.qx, link.qy, link.qz};
3997
3998 // Update canonical data (last frame where link appears has most developed geometry)
3999 canonical_links[link.name] = link;
4000 if (link.material_index >= 0) {
4001 canonical_material_index[link.name] = frame_to_merged_mat[link.material_index];
4002 } else {
4003 canonical_material_index[link.name] = -1;
4004 }
4005 }
4006 }
4007
4008 // --- Write USDA file ---
4009 std::ofstream out(output_file);
4010 if (!out.is_open()) {
4011 helios_runtime_error("ERROR (PlantArchitecture::writePlantGrowthUSD): Could not open file " + output_file + " for writing.");
4012 }
4013
4014 out << std::fixed;
4015
4016 std::string plant_prim = sanitizePrimName("Plant_" + std::to_string(plantID));
4017
4018 // Blender maps USD time codes 1:1 to Blender frames and uses framesPerSecond for playback.
4019 // To make each growth frame last `seconds_per_frame` seconds, we space time codes by
4020 // fps * seconds_per_frame. E.g., 6 frames at 1 sec/frame with 24fps ->
4021 // time codes 0, 24, 48, 72, 96, 120 -> 5 seconds of animation at 24fps playback.
4022 float fps = 24.0f;
4023 float time_code_spacing = fps * seconds_per_frame;
4024 std::vector<float> time_codes(num_frames);
4025 for (uint f = 0; f < num_frames; f++) {
4026 time_codes[f] = f * time_code_spacing;
4027 }
4028
4029 // Header
4030 out << "#usda 1.0\n";
4031 out << "(\n";
4032 out << " defaultPrim = \"" << plant_prim << "\"\n";
4033 out << " metersPerUnit = 1.0\n";
4034 out << " upAxis = \"Z\"\n";
4035 out << " startTimeCode = 0\n";
4036 out << " endTimeCode = " << time_codes.back() << "\n";
4037 out << " framesPerSecond = " << fps << "\n";
4038 out << " timeCodesPerSecond = " << fps << "\n";
4039 out << ")\n\n";
4040
4041 // Plant root Xform (Materials nested inside so referencing the default prim is self-contained)
4042 out << "def Xform \"" << plant_prim << "\"\n";
4043 out << "{\n";
4044
4045 if (!all_materials.empty()) {
4046 writeVisualMaterials(out, all_materials, getFilePath(output_file), plant_prim, " ");
4047 }
4048
4049 // Write each link as a time-sampled Xform
4050 for (const auto &link_name : link_order) {
4051 const auto &presence = link_presence[link_name];
4052 const auto &transforms = link_transforms[link_name];
4053 const auto &canonical = canonical_links[link_name];
4054 int mat_idx = canonical_material_index[link_name];
4055
4056 // Find the first frame where this link appears — used as the default position
4057 // for frames before the link exists, so geometry doesn't cluster at the origin.
4058 LinkTransform first_known = {canonical.position, canonical.qw, canonical.qx, canonical.qy, canonical.qz};
4059 for (uint f = 0; f < num_frames; f++) {
4060 if (presence[f]) {
4061 first_known = transforms[f];
4062 break;
4063 }
4064 }
4065
4066 std::string prim_name = sanitizePrimName(link_name);
4067
4068 out << " def Xform \"" << prim_name << "\"\n";
4069 out << " {\n";
4070
4071 // For each transform property, we use "step" keyframing: before each growth frame's
4072 // time code, we insert a "hold" keyframe 1 Blender frame earlier with the previous
4073 // frame's value. This prevents Blender from smoothly interpolating between growth
4074 // frames — all transitions are instantaneous.
4075
4076 // Time-sampled translate
4077 out << " point3f xformOp:translate.timeSamples = {\n";
4078 {
4079 bool first_entry = true;
4080 for (uint f = 0; f < num_frames; f++) {
4081 const auto &t = presence[f] ? transforms[f] : first_known;
4082
4083 // Hold keyframe: 1 Blender frame before this time code, hold the previous value
4084 if (f > 0 && time_codes[f] > 0) {
4085 const auto &prev_t = presence[f - 1] ? transforms[f - 1] : first_known;
4086 if (!first_entry) out << ",\n";
4087 out << " " << (time_codes[f] - 1.0f) << ": (" << prev_t.position.x << ", " << prev_t.position.y << ", " << prev_t.position.z << ")";
4088 first_entry = false;
4089 }
4090
4091 if (!first_entry) out << ",\n";
4092 out << " " << time_codes[f] << ": (" << t.position.x << ", " << t.position.y << ", " << t.position.z << ")";
4093 first_entry = false;
4094 }
4095 out << "\n }\n";
4096 }
4097
4098 // Time-sampled orient
4099 out << " quatf xformOp:orient.timeSamples = {\n";
4100 {
4101 bool first_entry = true;
4102 for (uint f = 0; f < num_frames; f++) {
4103 const auto &t = presence[f] ? transforms[f] : first_known;
4104
4105 if (f > 0 && time_codes[f] > 0) {
4106 const auto &prev_t = presence[f - 1] ? transforms[f - 1] : first_known;
4107 if (!first_entry) out << ",\n";
4108 out << " " << (time_codes[f] - 1.0f) << ": (" << prev_t.qw << ", " << prev_t.qx << ", " << prev_t.qy << ", " << prev_t.qz << ")";
4109 first_entry = false;
4110 }
4111
4112 if (!first_entry) out << ",\n";
4113 out << " " << time_codes[f] << ": (" << t.qw << ", " << t.qx << ", " << t.qy << ", " << t.qz << ")";
4114 first_entry = false;
4115 }
4116 out << "\n }\n";
4117 }
4118
4119 // Time-sampled scale: (0,0,0) hides organs before they appear, (1,1,1) shows them.
4120 out << " float3 xformOp:scale.timeSamples = {\n";
4121 {
4122 bool first_entry = true;
4123 for (uint f = 0; f < num_frames; f++) {
4124 bool is_visible = presence[f];
4125 bool was_visible = (f > 0) && presence[f - 1];
4126
4127 // Hold previous scale 1 frame before transition
4128 if (f > 0 && time_codes[f] > 0) {
4129 if (!first_entry) out << ",\n";
4130 out << " " << (time_codes[f] - 1.0f) << ": " << (was_visible ? "(1, 1, 1)" : "(0, 0, 0)");
4131 first_entry = false;
4132 }
4133
4134 if (!first_entry) out << ",\n";
4135 out << " " << time_codes[f] << ": " << (is_visible ? "(1, 1, 1)" : "(0, 0, 0)");
4136 first_entry = false;
4137 }
4138 out << "\n }\n";
4139 }
4140
4141 out << " uniform token[] xformOpOrder = [\"xformOp:translate\", \"xformOp:orient\", \"xformOp:scale\"]\n";
4142
4143 // Time-sampled visibility (kept for USD-compliant readers; scale-to-zero handles Blender)
4144 out << " token visibility.timeSamples = {\n";
4145 for (uint f = 0; f < num_frames; f++) {
4146 out << " " << time_codes[f] << ": \"" << (presence[f] ? "inherited" : "invisible") << "\"";
4147 out << (f < num_frames - 1 ? ",\n" : "\n");
4148 }
4149 out << " }\n";
4150
4151 // Static mesh from canonical frame
4152 bool has_mesh = false;
4153 if (canonical.link_type == USD_LINK_MESH && !canonical.mesh_vertices.empty()) {
4154 has_mesh = true;
4155 } else if (canonical.link_type == USD_LINK_CAPSULE && !canonical.mesh_vertices.empty()) {
4156 has_mesh = true;
4157 }
4158
4159 // Helper: write visibility time samples on the child visual prim too, since Blender's
4160 // USD importer may not respect visibility inheritance from parent Xform.
4161 auto writeChildVisibility = [&]() {
4162 out << " token visibility.timeSamples = {\n";
4163 for (uint f2 = 0; f2 < num_frames; f2++) {
4164 out << " " << time_codes[f2] << ": \"" << (presence[f2] ? "inherited" : "invisible") << "\"";
4165 out << (f2 < num_frames - 1 ? ",\n" : "\n");
4166 }
4167 out << " }\n";
4168 };
4169
4170 if (has_mesh) {
4171 out << "\n";
4172 out << " def Mesh \"Visual\" (\n";
4173 out << " prepend apiSchemas = [\"MaterialBindingAPI\"]\n";
4174 out << " )\n";
4175 out << " {\n";
4176
4177 writeChildVisibility();
4178 out << " uniform token subdivisionScheme = \"none\"\n";
4179 out << " bool doubleSided = 1\n";
4180
4181 // Vertices
4182 out << " point3f[] points = [";
4183 for (size_t v = 0; v < canonical.mesh_vertices.size(); v++) {
4184 if (v > 0) out << ", ";
4185 out << "(" << canonical.mesh_vertices[v].x << ", " << canonical.mesh_vertices[v].y << ", " << canonical.mesh_vertices[v].z << ")";
4186 }
4187 out << "]\n";
4188
4189 // Extent
4190 if (!canonical.mesh_vertices.empty()) {
4191 float xmn = canonical.mesh_vertices[0].x, xmx = xmn;
4192 float ymn = canonical.mesh_vertices[0].y, ymx = ymn;
4193 float zmn = canonical.mesh_vertices[0].z, zmx = zmn;
4194 for (const auto &v : canonical.mesh_vertices) {
4195 xmn = std::min(xmn, v.x); xmx = std::max(xmx, v.x);
4196 ymn = std::min(ymn, v.y); ymx = std::max(ymx, v.y);
4197 zmn = std::min(zmn, v.z); zmx = std::max(zmx, v.z);
4198 }
4199 out << " float3[] extent = [(" << xmn << ", " << ymn << ", " << zmn
4200 << "), (" << xmx << ", " << ymx << ", " << zmx << ")]\n";
4201 }
4202
4203 // Face vertex counts
4204 out << " int[] faceVertexCounts = [";
4205 for (size_t i = 0; i < canonical.mesh_face_vertex_counts.size(); i++) {
4206 if (i > 0) out << ", ";
4207 out << canonical.mesh_face_vertex_counts[i];
4208 }
4209 out << "]\n";
4210
4211 // Face vertex indices
4212 out << " int[] faceVertexIndices = [";
4213 for (size_t i = 0; i < canonical.mesh_face_vertex_indices.size(); i++) {
4214 if (i > 0) out << ", ";
4215 out << canonical.mesh_face_vertex_indices[i];
4216 }
4217 out << "]\n";
4218
4219 // Normals — indexed faceVarying
4220 {
4221 std::vector<vec3> normals = computeFaceVaryingNormals(canonical.mesh_vertices, canonical.mesh_face_vertex_counts, canonical.mesh_face_vertex_indices);
4222 if (!normals.empty()) {
4223 auto [unique_n, n_idx] = buildIndexedPrimvar(normals);
4224 out << " normal3f[] primvars:normals = [";
4225 for (size_t i = 0; i < unique_n.size(); i++) {
4226 if (i > 0) out << ", ";
4227 out << "(" << unique_n[i].x << ", " << unique_n[i].y << ", " << unique_n[i].z << ")";
4228 }
4229 out << "] (\n interpolation = \"faceVarying\"\n )\n";
4230 out << " int[] primvars:normals:indices = [";
4231 for (size_t i = 0; i < n_idx.size(); i++) { if (i > 0) out << ", "; out << n_idx[i]; }
4232 out << "]\n";
4233 }
4234 }
4235
4236 // UVs — indexed vertex
4237 if (!canonical.mesh_uvs.empty()) {
4238 auto [unique_uv, uv_idx] = buildIndexedPrimvarVec2(canonical.mesh_uvs);
4239 out << " texCoord2f[] primvars:st = [";
4240 for (size_t i = 0; i < unique_uv.size(); i++) {
4241 if (i > 0) out << ", ";
4242 out << "(" << unique_uv[i].x << ", " << unique_uv[i].y << ")";
4243 }
4244 out << "] (\n interpolation = \"vertex\"\n )\n";
4245 out << " int[] primvars:st:indices = [";
4246 for (size_t i = 0; i < uv_idx.size(); i++) { if (i > 0) out << ", "; out << uv_idx[i]; }
4247 out << "]\n";
4248 }
4249
4250 // Material binding — path is now nested under the default prim
4251 if (mat_idx >= 0 && mat_idx < (int)all_materials.size()) {
4252 out << " rel material:binding = </" << plant_prim << "/Materials/" << all_materials[mat_idx].name << ">\n";
4253 }
4254
4255 out << " }\n";
4256 } else if (canonical.link_type == USD_LINK_CAPSULE) {
4257 // Write a capsule shape for visual representation
4258 out << "\n";
4259 out << " def Capsule \"Visual\"\n";
4260 out << " {\n";
4261 writeChildVisibility();
4262 out << " double radius = " << canonical.radius << "\n";
4263 out << " double height = " << (2.0 * canonical.half_length) << "\n";
4264 out << " uniform token axis = \"Z\"\n";
4265 if (mat_idx >= 0 && mat_idx < (int)all_materials.size()) {
4266 out << " rel material:binding = </Materials/" << all_materials[mat_idx].name << ">\n";
4267 }
4268 out << " }\n";
4269 }
4270
4271 out << " }\n\n";
4272 }
4273
4274 out << "}\n";
4275 out.close();
4276
4277 if (printmessages) {
4278 std::cout << "Wrote USD growth animation to " << output_file << " (" << link_order.size() << " prims, " << num_frames << " frames)" << std::endl;
4279 }
4280}