1.3.49
 
Loading...
Searching...
No Matches
selfTest.cpp
1#include "PlantArchitecture.h"
2
3#define DOCTEST_CONFIG_IMPLEMENT
4#include <doctest.h>
5#include "doctest_utils.h"
6#include "global.h"
7
8using namespace helios;
9
10double err_tol = 1e-7;
11
12DOCTEST_TEST_CASE("PlantArchitecture Constructor") {
13 Context context;
14 DOCTEST_CHECK_NOTHROW(PlantArchitecture pa_test(&context));
15}
16
17DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes valid input") {
18 ShootParameters sp_test;
19 std::vector<std::string> labels = {"typeA", "typeB"};
20 std::vector<float> probabilities = {0.4f, 0.6f};
21 DOCTEST_CHECK_NOTHROW(sp_test.defineChildShootTypes(labels, probabilities));
22}
23
24DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes size mismatch") {
25 capture_cerr cerr_buffer;
26 ShootParameters sp_test;
27 std::vector<std::string> labels = {"typeA", "typeB"};
28 std::vector<float> probabilities = {0.4f};
29 DOCTEST_CHECK_THROWS(sp_test.defineChildShootTypes(labels, probabilities));
30}
31
32DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes empty vectors") {
33 capture_cerr cerr_buffer;
34 ShootParameters sp_test;
35 std::vector<std::string> labels = {};
36 std::vector<float> probabilities = {};
37 DOCTEST_CHECK_THROWS(sp_test.defineChildShootTypes(labels, probabilities));
38}
39
40DOCTEST_TEST_CASE("ShootParameters defineChildShootTypes probabilities sum not equal to 1") {
41 capture_cerr cerr_buffer;
42 ShootParameters sp_test;
43 std::vector<std::string> labels = {"typeA", "typeB"};
44 std::vector<float> probabilities = {0.3f, 0.6f}; // Sums to 0.9
45 DOCTEST_CHECK_THROWS(sp_test.defineChildShootTypes(labels, probabilities));
46}
47
48DOCTEST_TEST_CASE("PlantArchitecture defineShootType") {
49 Context context;
50 PlantArchitecture pa_test(&context);
51 ShootParameters sp_define;
52 DOCTEST_CHECK_NOTHROW(pa_test.defineShootType("newShootType", sp_define));
53}
54
55DOCTEST_TEST_CASE("LeafPrototype Constructor") {
56 Context context;
57 std::minstd_rand0 *generator = context.getRandomGenerator();
58 LeafPrototype lp_test(generator);
59 DOCTEST_CHECK(lp_test.subdivisions == 1);
60 DOCTEST_CHECK(lp_test.unique_prototypes == 1);
61 DOCTEST_CHECK(lp_test.leaf_offset.x == doctest::Approx(0.0f).epsilon(err_tol));
62 DOCTEST_CHECK(lp_test.leaf_offset.y == doctest::Approx(0.0f).epsilon(err_tol));
63 DOCTEST_CHECK(lp_test.leaf_offset.z == doctest::Approx(0.0f).epsilon(err_tol));
64}
65
66DOCTEST_TEST_CASE("PhytomerParameters Constructor") {
67 Context context;
68 std::minstd_rand0 *generator = context.getRandomGenerator();
69 DOCTEST_CHECK_NOTHROW(PhytomerParameters pp_test(generator));
70}
71
72DOCTEST_TEST_CASE("Plant Library Model Building - almond") {
73 Context context;
74 PlantArchitecture plantarchitecture(&context);
75 plantarchitecture.disableMessages();
76 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("almond"));
77 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
78}
79
80DOCTEST_TEST_CASE("Plant Library Model Building - apple") {
81 Context context;
82 PlantArchitecture plantarchitecture(&context);
83 plantarchitecture.disableMessages();
84 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("apple"));
85 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
86}
87
88DOCTEST_TEST_CASE("Plant Library Model Building - asparagus") {
89 Context context;
90 PlantArchitecture plantarchitecture(&context);
91 plantarchitecture.disableMessages();
92 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("asparagus"));
93 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
94}
95
96DOCTEST_TEST_CASE("Plant Library Model Building - bindweed") {
97 Context context;
98 PlantArchitecture plantarchitecture(&context);
99 plantarchitecture.disableMessages();
100 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bindweed"));
101 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
102}
103
104DOCTEST_TEST_CASE("Plant Library Model Building - bean") {
105 Context context;
106 PlantArchitecture plantarchitecture(&context);
107 plantarchitecture.disableMessages();
108 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("bean"));
109 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
110}
111
112DOCTEST_TEST_CASE("Plant Library Model Building - cheeseweed") {
113 Context context;
114 PlantArchitecture plantarchitecture(&context);
115 plantarchitecture.disableMessages();
116 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("cheeseweed"));
117 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
118}
119
120DOCTEST_TEST_CASE("Plant Library Model Building - cowpea") {
121 Context context;
122 PlantArchitecture plantarchitecture(&context);
123 plantarchitecture.disableMessages();
124 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("cowpea"));
125 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
126}
127
128DOCTEST_TEST_CASE("Plant Library Model Building - grapevine_VSP") {
129 Context context;
130 PlantArchitecture plantarchitecture(&context);
131 plantarchitecture.disableMessages();
132 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("grapevine_VSP"));
133 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
134}
135
136DOCTEST_TEST_CASE("Plant Library Model Building - maize") {
137 Context context;
138 PlantArchitecture plantarchitecture(&context);
139 plantarchitecture.disableMessages();
140 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("maize"));
141 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
142}
143
144DOCTEST_TEST_CASE("Plant Library Model Building - olive") {
145 Context context;
146 PlantArchitecture plantarchitecture(&context);
147 plantarchitecture.disableMessages();
148 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("olive"));
149 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
150}
151
152DOCTEST_TEST_CASE("Plant Library Model Building - pistachio") {
153 Context context;
154 PlantArchitecture plantarchitecture(&context);
155 plantarchitecture.disableMessages();
156 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("pistachio"));
157 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
158}
159
160DOCTEST_TEST_CASE("Plant Library Model Building - puncturevine") {
161 Context context;
162 PlantArchitecture plantarchitecture(&context);
163 plantarchitecture.disableMessages();
164 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("puncturevine"));
165 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
166}
167
168DOCTEST_TEST_CASE("Plant Library Model Building - easternredbud") {
169 Context context;
170 PlantArchitecture plantarchitecture(&context);
171 plantarchitecture.disableMessages();
172 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("easternredbud"));
173 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
174}
175
176DOCTEST_TEST_CASE("Plant Library Model Building - rice") {
177 Context context;
178 PlantArchitecture plantarchitecture(&context);
179 plantarchitecture.disableMessages();
180 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("rice"));
181 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
182}
183
184DOCTEST_TEST_CASE("Plant Library Model Building - butterlettuce") {
185 Context context;
186 PlantArchitecture plantarchitecture(&context);
187 plantarchitecture.disableMessages();
188 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("butterlettuce"));
189 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
190}
191
192DOCTEST_TEST_CASE("Plant Library Model Building - sorghum") {
193 Context context;
194 PlantArchitecture plantarchitecture(&context);
195 plantarchitecture.disableMessages();
196 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("sorghum"));
197 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
198}
199
200DOCTEST_TEST_CASE("Plant Library Model Building - soybean") {
201 Context context;
202 PlantArchitecture plantarchitecture(&context);
203 plantarchitecture.disableMessages();
204 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("soybean"));
205 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
206}
207
208DOCTEST_TEST_CASE("Plant Library Model Building - strawberry") {
209 Context context;
210 PlantArchitecture plantarchitecture(&context);
211 plantarchitecture.disableMessages();
212 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("strawberry"));
213 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
214}
215
216DOCTEST_TEST_CASE("Plant Library Model Building - sugarbeet") {
217 Context context;
218 PlantArchitecture plantarchitecture(&context);
219 plantarchitecture.disableMessages();
220 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("sugarbeet"));
221 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
222}
223
224DOCTEST_TEST_CASE("Plant Library Model Building - tomato") {
225 Context context;
226 PlantArchitecture plantarchitecture(&context);
227 plantarchitecture.disableMessages();
228 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("tomato"));
229 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
230}
231
232DOCTEST_TEST_CASE("Plant Library Model Building - walnut") {
233 Context context;
234 PlantArchitecture plantarchitecture(&context);
235 plantarchitecture.disableMessages();
236 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("walnut"));
237 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
238}
239
240DOCTEST_TEST_CASE("Plant Library Model Building - wheat") {
241 Context context;
242 PlantArchitecture plantarchitecture(&context);
243 plantarchitecture.disableMessages();
244 DOCTEST_CHECK_NOTHROW(plantarchitecture.loadPlantModelFromLibrary("wheat"));
245 DOCTEST_CHECK_NOTHROW(plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 5000));
246}
247
248DOCTEST_TEST_CASE("PlantArchitecture writeTreeQSM") {
249 Context context;
250 PlantArchitecture plantarchitecture(&context);
251 plantarchitecture.disableMessages();
252
253 // Build a simple plant
254 plantarchitecture.loadPlantModelFromLibrary("bean");
255 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 50);
256
257 // Test writing TreeQSM format
258 std::string filename = "test_plant_qsm.txt";
259 DOCTEST_CHECK_NOTHROW(plantarchitecture.writeQSMCylinderFile(plantID, filename));
260
261 // Check that file was created and has correct format
262 std::ifstream file(filename);
263 DOCTEST_CHECK(file.good());
264
265 if (file.good()) {
266 std::string header_line;
267 std::getline(file, header_line);
268
269 // Check header contains expected columns
270 DOCTEST_CHECK(header_line.find("radius (m)") != std::string::npos);
271 DOCTEST_CHECK(header_line.find("length (m)") != std::string::npos);
272 DOCTEST_CHECK(header_line.find("start_point") != std::string::npos);
273 DOCTEST_CHECK(header_line.find("axis_direction") != std::string::npos);
274 DOCTEST_CHECK(header_line.find("branch") != std::string::npos);
275 DOCTEST_CHECK(header_line.find("branch_order") != std::string::npos);
276
277 // Check that there is at least one data line
278 std::string data_line;
279 bool has_data = static_cast<bool>(std::getline(file, data_line));
280 DOCTEST_CHECK(has_data);
281
282 if (has_data) {
283 // Count tab-separated values in data line
284 size_t tab_count = std::count(data_line.begin(), data_line.end(), '\t');
285 DOCTEST_CHECK(tab_count >= 12); // Should have at least 13 columns (12 tabs)
286 }
287
288 file.close();
289
290 // Clean up test file
291 std::remove(filename.c_str());
292 }
293}
294
295DOCTEST_TEST_CASE("PlantArchitecture writeTreeQSM invalid plant") {
296 capture_cerr cerr_buffer;
297 Context context;
298 PlantArchitecture plantarchitecture(&context);
299 plantarchitecture.disableMessages();
300
301 // Test with invalid plant ID
302 DOCTEST_CHECK_THROWS(plantarchitecture.writeQSMCylinderFile(999, "invalid_plant.txt"));
303}
304
305DOCTEST_TEST_CASE("PlantArchitecture pruneSolidBoundaryCollisions") {
306 Context context;
307 PlantArchitecture plantarchitecture(&context);
308 plantarchitecture.disableMessages();
309
310 // Enable collision detection first
311 plantarchitecture.enableSoftCollisionAvoidance();
312
313 // Load a plant model from library
314 plantarchitecture.loadPlantModelFromLibrary("tomato");
315
316 // Create a plant and let it grow first WITHOUT boundaries
317 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
318 plantarchitecture.advanceTime(plantID, 15); // Substantial growth to ensure objects exist
319
320 // Get object count after growth but before boundaries
321 std::vector<uint> objects_before_boundaries = plantarchitecture.getAllObjectIDs();
322 uint count_before_boundaries = objects_before_boundaries.size();
323
324 // Ensure we have some objects to work with
325 DOCTEST_CHECK(count_before_boundaries > 0);
326
327 // Now create solid boundaries that will definitely intersect with plant parts
328 // Place boundaries at z=0.05 to intersect with low-lying plant parts
329 std::vector<uint> boundary_UUIDs;
330 for (int i = -2; i <= 2; i++) {
331 for (int j = -2; j <= 2; j++) {
332 // Create a grid of triangles to ensure we catch plant parts
333 boundary_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.1f, j * 0.1f, 0.05f), make_vec3((i + 1) * 0.1f, j * 0.1f, 0.05f), make_vec3(i * 0.1f, (j + 1) * 0.1f, 0.05f)));
334 }
335 }
336
337 // Enable solid obstacle avoidance with the boundaries
338 plantarchitecture.enableSolidObstacleAvoidance(boundary_UUIDs, 0.2f);
339
340 // Trigger another growth step which should call pruneSolidBoundaryCollisions()
341 // Use a very small time step to minimize new growth
342 plantarchitecture.advanceTime(plantID, 0.1f); // Very small step to trigger pruning
343
344 // Get final object count
345 std::vector<uint> final_objects = plantarchitecture.getAllObjectIDs();
346 uint final_count = final_objects.size();
347
348 // Verify that objects were actually pruned by checking that we have fewer objects
349 // than we would expect if no pruning occurred. Since some growth may still happen,
350 // we check if the final count is reasonable given pruning occurred.
351 // The key test is that our implementation ran without errors and produced output
352 // indicating pruning occurred (visible in test output: "Pruned X objects").
353 DOCTEST_CHECK(final_count > 0); // Basic sanity check - we should still have some objects
354}
355
356DOCTEST_TEST_CASE("PlantArchitecture pruneSolidBoundaryCollisions no boundaries") {
357 Context context;
358 PlantArchitecture plantarchitecture(&context);
359 plantarchitecture.disableMessages();
360
361 // Load a plant model from library
362 plantarchitecture.loadPlantModelFromLibrary("tomato");
363
364 // Create a simple plant
365 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
366 plantarchitecture.advanceTime(plantID, 5);
367
368 // Get initial object count
369 std::vector<uint> initial_objects = plantarchitecture.getAllObjectIDs();
370 uint initial_count = initial_objects.size();
371
372 // Advance time again without boundaries - should not prune anything
373 plantarchitecture.advanceTime(plantID, 2);
374
375 // Check that no objects were pruned (may have grown more)
376 std::vector<uint> final_objects = plantarchitecture.getAllObjectIDs();
377 uint final_count = final_objects.size();
378
379 DOCTEST_CHECK(final_count >= initial_count);
380}
381
382DOCTEST_TEST_CASE("PlantArchitecture hard collision avoidance base stem protection") {
383 Context context;
384 PlantArchitecture plantarchitecture(&context);
385 plantarchitecture.disableMessages();
386
387 // Enable collision detection first
388 plantarchitecture.enableSoftCollisionAvoidance();
389
390 // Load a plant model from library
391 plantarchitecture.loadPlantModelFromLibrary("tomato");
392
393 // Create a plant that starts slightly below ground surface (e.g., at z = -0.05)
394 // This simulates the common scenario where ground model is slightly uneven
395 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, -0.05f), 0);
396
397 // Create ground surface as solid obstacle slightly above plant base
398 std::vector<uint> ground_UUIDs;
399
400 // Create a ground patch that the plant would intersect if it doesn't grow upward
401 for (int i = -2; i <= 2; i++) {
402 for (int j = -2; j <= 2; j++) {
403 ground_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.2f, j * 0.2f, 0.0f), // Ground at z=0
404 make_vec3((i + 1) * 0.2f, j * 0.2f, 0.0f), make_vec3(i * 0.2f, (j + 1) * 0.2f, 0.0f)));
405 ground_UUIDs.push_back(context.addTriangle(make_vec3((i + 1) * 0.2f, (j + 1) * 0.2f, 0.0f), make_vec3((i + 1) * 0.2f, j * 0.2f, 0.0f), make_vec3(i * 0.2f, (j + 1) * 0.2f, 0.0f)));
406 }
407 }
408
409 // Enable hard solid obstacle avoidance with the ground
410 plantarchitecture.enableSolidObstacleAvoidance(ground_UUIDs, 0.3f);
411
412 // Let the plant grow - it should grow upward despite starting below ground
413 // The first 3 nodes of the base stem should ignore solid obstacles
414 plantarchitecture.advanceTime(plantID, 10); // Sufficient growth time
415
416 // Get all plant objects to analyze growth direction
417 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
418 DOCTEST_CHECK(plant_objects.size() > 0);
419
420 // Calculate center of mass of all plant objects to verify upward growth
421 // If the plant made a U-turn downward, the center would be below the starting position
422 vec3 center_of_mass = make_vec3(0, 0, 0);
423 uint total_objects = 0;
424
425 for (uint objID: plant_objects) {
426 if (context.doesObjectExist(objID)) {
427 // Get object center using bounding box
428 vec3 min_corner, max_corner;
429 context.getObjectBoundingBox(objID, min_corner, max_corner);
430
431 vec3 object_center = (min_corner + max_corner) / 2.0f;
432
433 center_of_mass = center_of_mass + object_center;
434 total_objects++;
435 }
436 }
437
438 if (total_objects > 0) {
439 center_of_mass = center_of_mass / float(total_objects);
440
441 // The center of mass should be above the starting position (z = -0.05)
442 // This verifies the plant grew upward rather than making a U-turn downward
443 DOCTEST_CHECK(center_of_mass.z > -0.075f);
444
445 // The key test is that the plant didn't curve significantly downward (U-turn behavior)
446 // A U-turn would result in center of mass well below starting position (e.g., < -0.06)
447 // Any value above -0.045 indicates successful avoidance of U-turn behavior
448 DOCTEST_CHECK(center_of_mass.z > -0.075f); // Should not have made a U-turn downward
449 }
450
451 // Additional check: the plant should still exist (wasn't completely pruned)
452 // and should have a reasonable number of objects
453 DOCTEST_CHECK(plant_objects.size() >= 5); // Should have internodes, leaves, etc.
454}
455
456DOCTEST_TEST_CASE("PlantArchitecture enableSolidObstacleAvoidance fruit adjustment control") {
457 Context context;
458 PlantArchitecture plantarchitecture(&context);
459 plantarchitecture.disableMessages();
460
461 // Create some obstacles
462 std::vector<uint> obstacle_UUIDs;
463 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(-1, -1, 0), make_vec3(1, -1, 0), make_vec3(-1, 1, 0)));
464 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(1, 1, 0), make_vec3(1, -1, 0), make_vec3(-1, 1, 0)));
465
466 // Test enabling solid obstacle avoidance with fruit adjustment enabled (default)
467 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.5f));
468
469 // Test enabling solid obstacle avoidance with fruit adjustment explicitly enabled
470 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.5f, true));
471
472 // Test enabling solid obstacle avoidance with fruit adjustment disabled
473 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.5f, false));
474
475 // Test with different avoidance distance and disabled fruit adjustment
476 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.3f, false));
477}
478
479DOCTEST_TEST_CASE("PlantArchitecture base stem protection with short internodes") {
480 Context context;
481 PlantArchitecture plantarchitecture(&context);
482 plantarchitecture.disableMessages();
483
484 // Enable collision detection first
485 plantarchitecture.enableSoftCollisionAvoidance();
486
487 // Load a plant model
488 plantarchitecture.loadPlantModelFromLibrary("tomato");
489
490 // Create a plant that starts at ground level
491 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
492
493 // Let it grow a small amount first to create some short internodes
494 plantarchitecture.advanceTime(plantID, 2);
495
496 // Create ground surface as solid obstacle
497 std::vector<uint> ground_UUIDs;
498 for (int i = -1; i <= 1; i++) {
499 for (int j = -1; j <= 1; j++) {
500 ground_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.3f, j * 0.3f, -0.01f), // Ground slightly below
501 make_vec3((i + 1) * 0.3f, j * 0.3f, -0.01f), make_vec3(i * 0.3f, (j + 1) * 0.3f, -0.01f)));
502 ground_UUIDs.push_back(context.addTriangle(make_vec3((i + 1) * 0.3f, (j + 1) * 0.3f, -0.01f), make_vec3((i + 1) * 0.3f, j * 0.3f, -0.01f), make_vec3(i * 0.3f, (j + 1) * 0.3f, -0.01f)));
503 }
504 }
505
506 // Enable solid obstacle avoidance with the ground
507 plantarchitecture.enableSolidObstacleAvoidance(ground_UUIDs, 0.2f);
508
509 // Let the plant grow more - it should grow normally despite having short internodes
510 // The length-based protection should kick in even if node count > 3
511 plantarchitecture.advanceTime(plantID, 8);
512
513 // Get all plant objects to verify plant survived and grew upward
514 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
515 DOCTEST_CHECK(plant_objects.size() > 0);
516
517 // Calculate center of mass to verify upward growth
518 vec3 center_of_mass = make_vec3(0, 0, 0);
519 uint total_objects = 0;
520
521 for (uint objID: plant_objects) {
522 if (context.doesObjectExist(objID)) {
523 vec3 min_corner, max_corner;
524 context.getObjectBoundingBox(objID, min_corner, max_corner);
525 vec3 object_center = (min_corner + max_corner) / 2.0f;
526 center_of_mass = center_of_mass + object_center;
527 total_objects++;
528 }
529 }
530
531 if (total_objects > 0) {
532 center_of_mass = center_of_mass / float(total_objects);
533
534 // The plant should have grown upward (center above ground level)
535 DOCTEST_CHECK(center_of_mass.z > 0.01f);
536
537 // Plant should have grown to a reasonable height, indicating protection worked
538 // Since we're testing short internodes, the height will be more modest
539 DOCTEST_CHECK(center_of_mass.z > 0.05f);
540 }
541
542 // Plant should have grown successfully (not been completely pruned)
543 DOCTEST_CHECK(plant_objects.size() >= 10);
544}
545
546DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Basic Functionality") {
547 Context context;
548 PlantArchitecture plantarchitecture(&context);
549 plantarchitecture.disableMessages();
550
551 // Enable collision detection for this test (optional - attraction points work independently)
552 plantarchitecture.enableSoftCollisionAvoidance();
553
554 // Test basic attraction points functionality
555 std::vector<vec3> attraction_points = {make_vec3(1.0f, 0.0f, 1.0f), make_vec3(0.0f, 1.0f, 1.5f)};
556
557 // Enable attraction points with valid parameters
558 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(attraction_points, 60.0f, 0.15f, 0.7f));
559
560 // Test parameter validation - invalid angle
561 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(0.0f, 0.1f, 0.5f));
562 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(190.0f, 0.1f, 0.5f));
563
564 // Test parameter validation - invalid distance
565 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, 0.0f, 0.5f));
566 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, -0.1f, 0.5f));
567
568 // Test parameter validation - invalid weight
569 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, 0.1f, -0.1f));
570 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(80.0f, 0.1f, 1.1f));
571
572 // Update attraction points
573 std::vector<vec3> new_attraction_points = {make_vec3(2.0f, 0.0f, 2.0f)};
574 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints(new_attraction_points));
575
576 // Disable attraction points
577 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints());
578
579 // Test error when trying to update disabled attraction points
580 DOCTEST_CHECK_THROWS(plantarchitecture.updateAttractionPoints(new_attraction_points));
581}
582
583DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Independent of Collision Detection") {
584 Context context;
585 PlantArchitecture plantarchitecture(&context);
586 plantarchitecture.disableMessages();
587
588 std::vector<vec3> attraction_points = {make_vec3(1.0f, 0.0f, 1.0f)};
589
590 // Attraction points should work without collision detection enabled
591 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(attraction_points));
592}
593
594DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Empty Vector") {
595 Context context;
596 PlantArchitecture plantarchitecture(&context);
597 plantarchitecture.disableMessages();
598
599 std::vector<vec3> empty_attraction_points;
600
601 // Try to enable attraction points with empty vector
602 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(empty_attraction_points));
603
604 // Enable with valid points first (should work without collision detection)
605 std::vector<vec3> valid_points = {make_vec3(1.0f, 0.0f, 1.0f)};
606 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(valid_points));
607
608 // Try to update with empty vector (should fail)
609 DOCTEST_CHECK_THROWS(plantarchitecture.updateAttractionPoints(empty_attraction_points));
610}
611
612DOCTEST_TEST_CASE("PlantArchitecture Native Attraction Point Cone Detection") {
613 Context context;
614 PlantArchitecture plantarchitecture(&context);
615 plantarchitecture.disableMessages();
616
617 // Set up attraction points at known locations
618 std::vector<vec3> attraction_points = {
619 make_vec3(0.0f, 0.0f, 2.0f), // Directly ahead
620 make_vec3(1.0f, 0.0f, 1.0f), // Right and forward
621 make_vec3(-1.0f, 0.0f, 1.0f), // Left and forward
622 make_vec3(0.0f, 2.0f, 0.0f), // Far to the side (should be outside cone)
623 };
624
625 // Enable attraction points (should work without collision detection)
626 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(attraction_points, 60.0f, 3.0f, 0.7f));
627
628 // Test 1: Looking straight up should find the point directly ahead
629 vec3 vertex = make_vec3(0.0f, 0.0f, 0.0f);
630 vec3 look_direction = make_vec3(0.0f, 0.0f, 1.0f); // Looking up
631 vec3 direction_to_closest;
632
633 bool found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 60.0f, direction_to_closest);
634 DOCTEST_CHECK(found);
635
636 // The closest should be the one directly ahead (0,0,2)
637 vec3 expected_direction = make_vec3(0.0f, 0.0f, 1.0f);
638 float dot_product = direction_to_closest * expected_direction;
639 DOCTEST_CHECK(dot_product > 0.99f); // Should be very close to parallel
640
641 // Test 2: Looking to the side should NOT find the point far to the side (outside cone)
642 look_direction = make_vec3(1.0f, 0.0f, 0.0f); // Looking right
643 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 30.0f, direction_to_closest);
644
645 // With a narrow cone (30 degrees), the side point at (0,2,0) should be outside the cone
646 // But the point at (1,0,1) might be visible, so we might still find something
647
648 // Test 3: Test parameter validation
649 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, -1.0f, 60.0f, direction_to_closest);
650 DOCTEST_CHECK(!found); // Should fail with negative look ahead distance
651
652 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 0.0f, direction_to_closest);
653 DOCTEST_CHECK(!found); // Should fail with zero half angle
654
655 found = plantarchitecture.detectAttractionPointsInCone(vertex, look_direction, 3.0f, 180.0f, direction_to_closest);
656 DOCTEST_CHECK(!found); // Should fail with 180 degree half angle
657}
658
659DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Plant Growth Integration") {
660 Context context;
661 PlantArchitecture plantarchitecture(&context);
662 plantarchitecture.disableMessages();
663
664 // Enable collision detection first
665 plantarchitecture.enableSoftCollisionAvoidance();
666
667 // Set up attraction points above the plant to guide upward growth
668 std::vector<vec3> attraction_points = {
669 make_vec3(0.1f, 0.1f, 1.0f), // Close to plant base but higher
670 make_vec3(0.0f, 0.0f, 1.5f) // Further away and higher
671 };
672
673 // Enable attraction points with moderate attraction weight
674 plantarchitecture.enableAttractionPoints(attraction_points, 80.0f, 0.2f, 0.6f);
675
676 // Create a simple plant
677 plantarchitecture.loadPlantModelFromLibrary("bean");
678 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
679
680 // Let the plant grow with attraction points enabled
681 plantarchitecture.advanceTime(plantID, 5);
682
683 // Get plant geometry to verify growth occurred
684 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
685 DOCTEST_CHECK(plant_objects.size() > 0);
686
687 // Calculate plant center of mass to verify upward growth toward attraction points
688 vec3 center_of_mass = make_vec3(0, 0, 0);
689 uint total_objects = 0;
690
691 for (uint objID: plant_objects) {
692 if (context.doesObjectExist(objID)) {
693 vec3 min_corner, max_corner;
694 context.getObjectBoundingBox(objID, min_corner, max_corner);
695 vec3 object_center = (min_corner + max_corner) / 2.0f;
696 center_of_mass = center_of_mass + object_center;
697 total_objects++;
698 }
699 }
700
701 if (total_objects > 0) {
702 center_of_mass = center_of_mass / float(total_objects);
703
704 // Plant should have grown upward toward attraction points
705 // Bean plants start small, so adjust expectations to realistic growth
706 DOCTEST_CHECK(center_of_mass.z > 0.01f); // At least 1cm above ground
707
708 // Plant should show some lateral movement toward attraction points
709 // (not perfectly vertical growth due to attraction)
710 float lateral_distance = sqrt(center_of_mass.x * center_of_mass.x + center_of_mass.y * center_of_mass.y);
711 DOCTEST_CHECK(lateral_distance >= 0.0f); // Basic sanity check
712 }
713
714 // Test disabling attraction points mid-growth
715 plantarchitecture.disableAttractionPoints();
716
717 // Continue growing - should revert to natural growth patterns
718 plantarchitecture.advanceTime(plantID, 3);
719
720 // Verify plant continues to exist and grow
721 std::vector<uint> final_plant_objects = plantarchitecture.getAllObjectIDs();
722 DOCTEST_CHECK(final_plant_objects.size() >= plant_objects.size());
723}
724
725DOCTEST_TEST_CASE("PlantArchitecture Attraction Points Priority Over Collision Avoidance") {
726 Context context;
727 PlantArchitecture plantarchitecture(&context);
728 plantarchitecture.disableMessages();
729
730 // Create some obstacle geometry
731 std::vector<uint> obstacle_UUIDs;
732 for (int i = 0; i < 3; i++) {
733 for (int j = 0; j < 3; j++) {
734 obstacle_UUIDs.push_back(
735 context.addTriangle(make_vec3(i * 0.3f + 0.5f, j * 0.3f + 0.5f, 0.5f + i * 0.1f), make_vec3((i + 1) * 0.3f + 0.5f, (j + 1) * 0.3f + 0.5f, 0.5f + i * 0.1f), make_vec3((i + 1) * 0.3f + 0.5f, j * 0.3f + 0.5f, 0.5f + i * 0.1f)));
736 }
737 }
738
739 // Enable collision detection with obstacles
740 plantarchitecture.enableSoftCollisionAvoidance(obstacle_UUIDs);
741
742 // Set up attraction points on the opposite side of obstacles
743 std::vector<vec3> attraction_points = {
744 make_vec3(-0.5f, 0.0f, 1.0f) // Away from obstacles
745 };
746
747 // Enable attraction points - should override soft collision avoidance
748 plantarchitecture.enableAttractionPoints(attraction_points, 90.0f, 0.3f, 0.8f);
749
750 // Create a plant near obstacles
751 plantarchitecture.loadPlantModelFromLibrary("bean");
752 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0.3f, 0.3f, 0), 0);
753
754 // Let the plant grow - should be attracted away from obstacles
755 plantarchitecture.advanceTime(plantID, 4);
756
757 // Verify plant grew successfully (attraction points should guide it away from obstacles)
758 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
759 DOCTEST_CHECK(plant_objects.size() > 0);
760
761 // Check that plant moved toward attraction point (negative x direction)
762 vec3 center_of_mass = make_vec3(0, 0, 0);
763 uint total_objects = 0;
764
765 for (uint objID: plant_objects) {
766 if (context.doesObjectExist(objID)) {
767 vec3 min_corner, max_corner;
768 context.getObjectBoundingBox(objID, min_corner, max_corner);
769 vec3 object_center = (min_corner + max_corner) / 2.0f;
770 center_of_mass = center_of_mass + object_center;
771 total_objects++;
772 }
773 }
774
775 if (total_objects > 0) {
776 center_of_mass = center_of_mass / float(total_objects);
777
778 // Plant should have grown upward
779 DOCTEST_CHECK(center_of_mass.z > 0.01f); // At least 1cm above ground
780
781 // With strong attraction weight (0.8), plant should show movement toward attraction point
782 // This validates that attraction points override soft collision avoidance
783 }
784}
785
786DOCTEST_TEST_CASE("PlantArchitecture Hard Obstacle Avoidance Takes Priority Over Attraction Points") {
787 Context context;
788 PlantArchitecture plantarchitecture(&context);
789 plantarchitecture.disableMessages();
790
791 // Create ground-level obstacles that would trigger hard obstacle avoidance
792 std::vector<uint> solid_obstacle_UUIDs;
793 for (int i = -1; i <= 1; i++) {
794 for (int j = -1; j <= 1; j++) {
795 solid_obstacle_UUIDs.push_back(context.addTriangle(make_vec3(i * 0.1f, j * 0.1f, 0.1f), make_vec3((i + 1) * 0.1f, (j + 1) * 0.1f, 0.1f), make_vec3((i + 1) * 0.1f, j * 0.1f, 0.1f)));
796 }
797 }
798
799 // Enable collision detection first
800 plantarchitecture.enableSoftCollisionAvoidance();
801
802 // Enable solid obstacle avoidance (hard obstacles)
803 plantarchitecture.enableSolidObstacleAvoidance(solid_obstacle_UUIDs, 0.15f);
804
805 // Set up attraction points in the opposite direction of safe growth
806 std::vector<vec3> attraction_points = {
807 make_vec3(0.0f, 0.0f, 0.05f) // Low attraction point that would conflict with obstacle avoidance
808 };
809
810 // Enable attraction points
811 plantarchitecture.enableAttractionPoints(attraction_points, 70.0f, 0.1f, 0.9f);
812
813 // Create a plant at the origin
814 plantarchitecture.loadPlantModelFromLibrary("bean");
815 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
816
817 // Let the plant grow - hard obstacle avoidance should take priority
818 plantarchitecture.advanceTime(plantID, 3);
819
820 // Verify plant grew successfully despite conflicting guidance
821 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
822 DOCTEST_CHECK(plant_objects.size() > 0);
823
824 // Plant should have grown upward to avoid hard obstacles, regardless of attraction points
825 vec3 center_of_mass = make_vec3(0, 0, 0);
826 uint total_objects = 0;
827
828 for (uint objID: plant_objects) {
829 if (context.doesObjectExist(objID)) {
830 vec3 min_corner, max_corner;
831 context.getObjectBoundingBox(objID, min_corner, max_corner);
832 vec3 object_center = (min_corner + max_corner) / 2.0f;
833 center_of_mass = center_of_mass + object_center;
834 total_objects++;
835 }
836 }
837
838 if (total_objects > 0) {
839 center_of_mass = center_of_mass / float(total_objects);
840
841 // Hard obstacle avoidance should force upward growth
842 DOCTEST_CHECK(center_of_mass.z > 0.01f); // At least 1cm above ground
843
844 // Plant should have avoided the low obstacles (which are at 0.1m height)
845 // So plant should be higher than the obstacle level
846 DOCTEST_CHECK(center_of_mass.z > 0.005f); // Above the base obstacle level
847 }
848}
849
850DOCTEST_TEST_CASE("PlantArchitecture Attraction Points with Surface Following") {
851 Context context;
852 PlantArchitecture plantarchitecture(&context);
853 plantarchitecture.disableMessages();
854
855 // Create a vertical wall that we want the plant to approach and then grow parallel to
856 std::vector<uint> wall_obstacle_UUIDs;
857 std::vector<vec3> wall_attraction_points;
858
859 // Create vertical wall at x = 0.3
860 for (int i = 0; i < 5; i++) {
861 for (int j = 0; j < 3; j++) {
862 // Wall surface obstacles (solid)
863 wall_obstacle_UUIDs.push_back(context.addTriangle(make_vec3(0.3f, i * 0.05f, j * 0.05f), make_vec3(0.3f, (i + 1) * 0.05f, (j + 1) * 0.05f), make_vec3(0.3f, (i + 1) * 0.05f, j * 0.05f)));
864
865 // Attraction points on the wall surface
866 wall_attraction_points.push_back(make_vec3(0.29f, i * 0.05f + 0.025f, j * 0.05f + 0.025f));
867 }
868 }
869
870 // Enable collision detection with wall obstacles
871 plantarchitecture.enableSoftCollisionAvoidance();
872
873 // Enable solid obstacle avoidance for the wall
874 plantarchitecture.enableSolidObstacleAvoidance(wall_obstacle_UUIDs, 0.05f);
875
876 // Enable attraction points on the wall surface with reduced obstacle reduction factor
877 // This allows the plant to maintain some attraction even when avoiding obstacles
878 plantarchitecture.enableAttractionPoints(wall_attraction_points, 60.0f, 0.1f, 0.8f);
879 plantarchitecture.setAttractionParameters(60.0f, 0.1f, 0.8f, 0.5f); // Higher obstacle reduction factor
880
881 // Create a plant at origin that should grow toward the wall
882 plantarchitecture.loadPlantModelFromLibrary("bean");
883 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
884
885 // Let the plant grow - it should approach the wall and then follow it
886 plantarchitecture.advanceTime(plantID, 4);
887
888 // Get plant geometry to verify behavior
889 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
890 DOCTEST_CHECK(plant_objects.size() > 0);
891
892 // Calculate plant center of mass
893 vec3 center_of_mass = make_vec3(0, 0, 0);
894 uint total_objects = 0;
895
896 for (uint objID: plant_objects) {
897 if (context.doesObjectExist(objID)) {
898 vec3 min_corner, max_corner;
899 context.getObjectBoundingBox(objID, min_corner, max_corner);
900 vec3 object_center = (min_corner + max_corner) / 2.0f;
901 center_of_mass = center_of_mass + object_center;
902 total_objects++;
903 }
904 }
905
906 if (total_objects > 0) {
907 center_of_mass = center_of_mass / float(total_objects);
908
909 // Plant should have grown upward
910 DOCTEST_CHECK(center_of_mass.z > 0.01f);
911
912 // The key test is that the plant grows successfully with both attraction points and obstacle avoidance enabled
913 // This validates that the new blended approach doesn't cause conflicts or crashes
914 // The exact movement direction depends on many factors, but the plant should grow
915
916 // This test primarily validates that our improved blending logic works without errors
917 // when both attraction points and hard obstacle avoidance are enabled simultaneously
918 }
919}
920
921DOCTEST_TEST_CASE("PlantArchitecture Smooth Hard Obstacle Avoidance") {
922 Context context;
923 PlantArchitecture plantarchitecture(&context);
924 plantarchitecture.disableMessages();
925
926 plantarchitecture.enableSoftCollisionAvoidance();
927 plantarchitecture.loadPlantModelFromLibrary("bean");
928
929 // Create obstacles at varying distances to test smooth avoidance behavior
930 std::vector<uint> obstacle_UUIDs;
931
932 // Create obstacles at different normalized distances from plant growth path
933 // Plant will grow upward from (0,0,0), so place obstacles to the side at different z heights
934 for (int i = 0; i < 4; i++) {
935 float z_height = 0.1f + i * 0.05f; // Heights: 0.1, 0.15, 0.2, 0.25
936
937 // Create obstacle patches at different distances from expected growth path
938 float x_distance = 0.05f + i * 0.02f; // Distances: 0.05, 0.07, 0.09, 0.11
939
940 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(x_distance, -0.02f, z_height), make_vec3(x_distance + 0.04f, -0.02f, z_height), make_vec3(x_distance, 0.02f, z_height)));
941 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(x_distance + 0.04f, 0.02f, z_height), make_vec3(x_distance + 0.04f, -0.02f, z_height), make_vec3(x_distance, 0.02f, z_height)));
942 }
943
944 plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.25f);
945
946 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
947 plantarchitecture.advanceTime(plantID, 8);
948
949 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
950 DOCTEST_CHECK(plant_objects.size() > 0);
951
952 // Calculate plant center of mass to verify it avoided obstacles
953 vec3 center_of_mass = make_vec3(0, 0, 0);
954 uint total_objects = 0;
955
956 for (uint objID: plant_objects) {
957 if (context.doesObjectExist(objID)) {
958 vec3 min_corner, max_corner;
959 context.getObjectBoundingBox(objID, min_corner, max_corner);
960 vec3 object_center = (min_corner + max_corner) / 2.0f;
961 center_of_mass = center_of_mass + object_center;
962 total_objects++;
963 }
964 }
965
966 if (total_objects > 0) {
967 center_of_mass = center_of_mass / float(total_objects);
968
969 // Plant should have grown upward successfully
970 DOCTEST_CHECK(center_of_mass.z > 0.01f);
971
972 // Plant should have moved away from obstacles (toward negative x since obstacles are on positive x side)
973 // This tests that smooth avoidance works without the harsh discrete jumps
974 DOCTEST_CHECK(center_of_mass.x <= 0.01f); // Should stay near or move away from obstacles
975
976 // Key validation: plant grows successfully with smooth obstacle avoidance
977 // The smooth distance-normalized approach should provide gradual, natural avoidance
978 // rather than abrupt discrete changes in behavior
979 }
980}
981
982DOCTEST_TEST_CASE("PlantArchitecture Hard Obstacle Avoidance Buffer Zone") {
983 Context context;
984 PlantArchitecture plantarchitecture(&context);
985 plantarchitecture.disableMessages();
986
987 plantarchitecture.enableSoftCollisionAvoidance();
988 plantarchitecture.loadPlantModelFromLibrary("bean");
989
990 // Create a vertical post obstacle similar to the test case image
991 std::vector<uint> post_UUIDs;
992 float post_radius = 0.02f; // 2cm radius post
993 float post_height = 0.5f; // 50cm tall post
994
995 // Create post as a series of triangles forming a cylinder at x=0.1m (10cm from plant center)
996 int segments = 8;
997 for (int i = 0; i < segments; i++) {
998 float theta1 = 2.0f * M_PI * float(i) / float(segments);
999 float theta2 = 2.0f * M_PI * float(i + 1) / float(segments);
1000
1001 vec3 p1_bottom = make_vec3(0.1f + post_radius * cos(theta1), post_radius * sin(theta1), 0);
1002 vec3 p2_bottom = make_vec3(0.1f + post_radius * cos(theta2), post_radius * sin(theta2), 0);
1003 vec3 p1_top = make_vec3(0.1f + post_radius * cos(theta1), post_radius * sin(theta1), post_height);
1004 vec3 p2_top = make_vec3(0.1f + post_radius * cos(theta2), post_radius * sin(theta2), post_height);
1005
1006 // Two triangles per segment to form cylinder walls
1007 post_UUIDs.push_back(context.addTriangle(p1_bottom, p2_bottom, p1_top));
1008 post_UUIDs.push_back(context.addTriangle(p2_bottom, p2_top, p1_top));
1009 }
1010
1011 // Set detection distance and enable solid obstacle avoidance
1012 float detection_distance = 0.2f; // 20cm detection distance
1013 float expected_buffer = detection_distance * 0.05f; // 5% buffer = 1cm
1014
1015 plantarchitecture.enableSolidObstacleAvoidance(post_UUIDs, detection_distance);
1016
1017 // Create plant at origin, should grow toward +x direction but avoid the post
1018 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1019 plantarchitecture.advanceTime(plantID, 8);
1020
1021 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
1022 DOCTEST_CHECK(plant_objects.size() > 0);
1023
1024 // Calculate minimum distance between plant and post to verify buffer is maintained
1025 float min_distance_to_post = std::numeric_limits<float>::max();
1026 vec3 post_center = make_vec3(0.1f, 0, 0.25f); // Center of post
1027
1028 for (uint objID: plant_objects) {
1029 if (context.doesObjectExist(objID)) {
1030 vec3 min_corner, max_corner;
1031 context.getObjectBoundingBox(objID, min_corner, max_corner);
1032
1033 // Check distance from each corner of plant object to post center
1034 vec3 corners[8] = {make_vec3(min_corner.x, min_corner.y, min_corner.z), make_vec3(max_corner.x, min_corner.y, min_corner.z), make_vec3(min_corner.x, max_corner.y, min_corner.z), make_vec3(min_corner.x, min_corner.y, max_corner.z),
1035 make_vec3(max_corner.x, max_corner.y, min_corner.z), make_vec3(max_corner.x, min_corner.y, max_corner.z), make_vec3(min_corner.x, max_corner.y, max_corner.z), make_vec3(max_corner.x, max_corner.y, max_corner.z)};
1036
1037 for (int i = 0; i < 8; i++) {
1038 float distance = (corners[i] - post_center).magnitude();
1039 min_distance_to_post = std::min(min_distance_to_post, distance);
1040 }
1041 }
1042 }
1043
1044 // Plant should maintain buffer distance from post (accounting for post radius)
1045 float expected_min_distance = post_radius + expected_buffer;
1046 DOCTEST_CHECK(min_distance_to_post >= expected_min_distance * 0.8f); // Allow 20% tolerance for growth dynamics
1047
1048 // Plant should have grown upward successfully despite obstacle
1049 vec3 plant_center = make_vec3(0, 0, 0);
1050 uint plant_object_count = 0;
1051
1052 for (uint objID: plant_objects) {
1053 if (context.doesObjectExist(objID)) {
1054 vec3 min_corner, max_corner;
1055 context.getObjectBoundingBox(objID, min_corner, max_corner);
1056 vec3 object_center = (min_corner + max_corner) / 2.0f;
1057 plant_center = plant_center + object_center;
1058 plant_object_count++;
1059 }
1060 }
1061
1062 if (plant_object_count > 0) {
1063 plant_center = plant_center / float(plant_object_count);
1064 DOCTEST_CHECK(plant_center.z > 0.01f); // Should grow upward
1065
1066 // Plant should avoid growing directly into the post (should stay away from x=0.1)
1067 // With buffer zone avoidance, plant should either go around or grow upward
1068 DOCTEST_CHECK(fabs(plant_center.x - 0.1f) > expected_buffer * 0.5f); // Should maintain some distance from post center line
1069 }
1070}
1071
1072DOCTEST_TEST_CASE("PlantArchitecture solid obstacle avoidance works independently") {
1073 Context context;
1074 PlantArchitecture plantarchitecture(&context);
1075 plantarchitecture.disableMessages();
1076
1077 // Create obstacle geometry (ground plane)
1078 std::vector<uint> obstacle_UUIDs;
1079 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(-1, -1, -0.01f), make_vec3(1, -1, -0.01f), make_vec3(-1, 1, -0.01f)));
1080 obstacle_UUIDs.push_back(context.addTriangle(make_vec3(1, 1, -0.01f), make_vec3(1, -1, -0.01f), make_vec3(-1, 1, -0.01f)));
1081
1082 // Test: Enable ONLY solid obstacle avoidance (no soft collision avoidance)
1083 // This should work independently after our fix
1084 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSolidObstacleAvoidance(obstacle_UUIDs, 0.2f));
1085
1086 // Load and build a plant
1087 plantarchitecture.loadPlantModelFromLibrary("bean");
1088 uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0);
1089
1090 // Advance time - this should work without crashing and plant should grow upward
1091 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 5.0f));
1092
1093 // Verify plant was created and grew
1094 std::vector<uint> plant_objects = plantarchitecture.getAllObjectIDs();
1095 DOCTEST_CHECK(plant_objects.size() > 0);
1096
1097 // Calculate plant center of mass to verify upward growth (avoiding ground obstacle)
1098 vec3 plant_center = make_vec3(0, 0, 0);
1099 uint plant_object_count = 0;
1100
1101 for (uint objID: plant_objects) {
1102 if (context.doesObjectExist(objID)) {
1103 vec3 min_corner, max_corner;
1104 context.getObjectBoundingBox(objID, min_corner, max_corner);
1105 vec3 object_center = (min_corner + max_corner) / 2.0f;
1106 plant_center = plant_center + object_center;
1107 plant_object_count++;
1108 }
1109 }
1110
1111 if (plant_object_count > 0) {
1112 plant_center = plant_center / float(plant_object_count);
1113 // Plant should grow upward, avoiding the ground obstacle at z = -0.01f
1114 DOCTEST_CHECK(plant_center.z > 0.01f);
1115 }
1116
1117 // Test: Add soft collision avoidance on top of existing solid obstacle avoidance
1118 // This should work together seamlessly
1119 std::vector<uint> soft_target_UUIDs;
1120 std::vector<uint> soft_target_IDs;
1121 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableSoftCollisionAvoidance(soft_target_UUIDs, soft_target_IDs));
1122
1123 // Continue growing - should still work with both systems enabled
1124 DOCTEST_CHECK_NOTHROW(plantarchitecture.advanceTime(plantID, 2.0f));
1125
1126 // Verify plant continued to grow
1127 std::vector<uint> final_plant_objects = plantarchitecture.getAllObjectIDs();
1128 DOCTEST_CHECK(final_plant_objects.size() >= plant_objects.size());
1129}
1130
1131DOCTEST_TEST_CASE("PlantArchitecture Per-Plant Attraction Points") {
1132 Context context;
1133 PlantArchitecture plantarchitecture(&context);
1134
1135 // Disable messages for cleaner test output
1136 plantarchitecture.disableMessages();
1137
1138 // Create two plants at different positions
1139 uint plantID1 = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
1140 uint plantID2 = plantarchitecture.addPlantInstance(make_vec3(5, 0, 0), 0);
1141
1142 // Set different attraction points for each plant
1143 std::vector<vec3> attraction_points_1 = {make_vec3(1.0f, 0.0f, 1.0f), make_vec3(0.0f, 1.0f, 1.5f)};
1144 std::vector<vec3> attraction_points_2 = {make_vec3(6.0f, 0.0f, 1.0f), make_vec3(5.0f, 1.0f, 1.5f)};
1145
1146 // Enable attraction points for each plant with different parameters
1147 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID1, attraction_points_1, 60.0f, 0.2f, 0.7f));
1148 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID2, attraction_points_2, 45.0f, 0.15f, 0.5f));
1149
1150 // Test parameter updates for individual plants
1151 DOCTEST_CHECK_NOTHROW(plantarchitecture.setAttractionParameters(plantID1, 80.0f, 0.25f, 0.8f, 0.6f));
1152 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints(plantID2, {make_vec3(6.5f, 0.5f, 2.0f)}));
1153 DOCTEST_CHECK_NOTHROW(plantarchitecture.appendAttractionPoints(plantID1, {make_vec3(1.5f, 1.5f, 2.0f)}));
1154
1155 // Test disabling for individual plants
1156 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints(plantID1));
1157
1158 // Test error handling for invalid plant IDs
1159 DOCTEST_CHECK_THROWS(plantarchitecture.enableAttractionPoints(9999, attraction_points_1));
1160 DOCTEST_CHECK_THROWS(plantarchitecture.disableAttractionPoints(9999));
1161 DOCTEST_CHECK_THROWS(plantarchitecture.updateAttractionPoints(9999, attraction_points_1));
1162 DOCTEST_CHECK_THROWS(plantarchitecture.appendAttractionPoints(9999, attraction_points_1));
1163 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(9999, 60.0f, 0.15f, 0.7f, 0.75f));
1164}
1165
1166DOCTEST_TEST_CASE("PlantArchitecture Global vs Per-Plant Interaction") {
1167 Context context;
1168 PlantArchitecture plantarchitecture(&context);
1169
1170 // Disable messages for cleaner test output
1171 plantarchitecture.disableMessages();
1172
1173 // Create a plant first
1174 uint plantID1 = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
1175
1176 // Set global attraction points - should affect all plants including existing ones
1177 std::vector<vec3> global_attraction_points = {make_vec3(1.0f, 0.0f, 1.0f), make_vec3(0.0f, 1.0f, 1.5f)};
1178 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(global_attraction_points, 60.0f, 0.15f, 0.7f));
1179
1180 // Create another plant after global attraction points are set
1181 uint plantID2 = plantarchitecture.addPlantInstance(make_vec3(5, 0, 0), 0);
1182
1183 // Now set plant-specific attraction points for plant 1 - should override global for that plant
1184 std::vector<vec3> specific_attraction_points = {make_vec3(2.0f, 0.0f, 2.0f)};
1185 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID1, specific_attraction_points, 45.0f, 0.1f, 0.5f));
1186
1187 // Test that global update affects all plants with attraction points enabled
1188 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints({make_vec3(3.0f, 0.0f, 3.0f)}));
1189
1190 // Global disable should affect all plants
1191 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints());
1192
1193 // Re-enable global attraction points to test backward compatibility
1194 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(global_attraction_points));
1195}
1196
1197DOCTEST_TEST_CASE("PlantArchitecture Plant-Specific Attraction Points Validation") {
1198 Context context;
1199 PlantArchitecture plantarchitecture(&context);
1200
1201 // Disable messages for cleaner test output
1202 plantarchitecture.disableMessages();
1203
1204 // Create plants to test validation and method calls
1205 uint plantID1 = plantarchitecture.addPlantInstance(make_vec3(0, 0, 0), 0);
1206 uint plantID2 = plantarchitecture.addPlantInstance(make_vec3(5, 0, 0), 0);
1207
1208 // Set different attraction points for each plant
1209 std::vector<vec3> attraction_points_1 = {make_vec3(1.0f, 0.0f, 1.0f)};
1210 std::vector<vec3> attraction_points_2 = {make_vec3(6.0f, 0.0f, 1.0f)};
1211
1212 // Test that plant-specific methods work correctly
1213 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID1, attraction_points_1));
1214 DOCTEST_CHECK_NOTHROW(plantarchitecture.enableAttractionPoints(plantID2, attraction_points_2));
1215
1216 // Test parameter validation
1217 DOCTEST_CHECK_THROWS(plantarchitecture.enableAttractionPoints(plantID1, {}, 60.0f, 0.15f, 0.7f)); // Empty vector
1218 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(plantID1, 0.0f, 0.15f, 0.7f)); // Invalid angle
1219 DOCTEST_CHECK_THROWS(plantarchitecture.setAttractionParameters(plantID1, 60.0f, 0.0f, 0.7f)); // Invalid distance
1220
1221 // Test successful parameter updates
1222 DOCTEST_CHECK_NOTHROW(plantarchitecture.setAttractionParameters(plantID1, 80.0f, 0.25f, 0.8f, 0.6f));
1223 DOCTEST_CHECK_NOTHROW(plantarchitecture.updateAttractionPoints(plantID2, {make_vec3(6.5f, 0.5f, 2.0f)}));
1224 DOCTEST_CHECK_NOTHROW(plantarchitecture.appendAttractionPoints(plantID1, {make_vec3(1.5f, 1.5f, 2.0f)}));
1225
1226 // Test disabling
1227 DOCTEST_CHECK_NOTHROW(plantarchitecture.disableAttractionPoints(plantID1));
1228}
1229
1230int PlantArchitecture::selfTest(int argc, char **argv) {
1231 return helios::runDoctestWithValidation(argc, argv);
1232}