37 auto camera = vtkSmartPointer<vtkCamera>::New();
38 const V boxCenter(1, 2, 3);
39 camera->SetFocalPoint(101, 202, 303);
40 camera->SetPosition(111, 202, 303);
41 camera->SetViewUp(0, 0, 1);
43 rotation.
apply(camera, boxCenter,
V(0, 0, 2), 0.5, 180);
44 check(
V(camera->GetPosition()).isApprox(
V(1, 12, 3)),
"Frame rotation ignored runID or axis normalization");
45 rotation.
apply(camera, boxCenter, V::UnitZ(), 0.5, 360);
46 check(
V(camera->GetPosition()).isApprox(
V(-9, 2, 3)),
"Sparse frame rotation is incorrect");
47 rotation.
apply(camera, boxCenter, V::UnitZ(), 0.5, 180);
48 rotation.
apply(camera, boxCenter, V::UnitZ(), 0.5, 180);
49 check(
V(camera->GetPosition()).isApprox(
V(1, 12, 3)),
"Backward or repeated frame accumulated rotation");
50 rotation.
apply(camera, boxCenter, V::UnitZ(), 0.0, 180);
51 check(
V(camera->GetPosition()).isApprox(
V(11, 2, 3)),
"Disabling rotation did not restore the view");
52 rotation.
apply(camera, boxCenter, V::UnitY(), -0.5, 180);
53 check(
V(camera->GetPosition()).isApprox(
V(1, 2, 13)),
"Axis change or negative angle is incorrect");
54 check(
V(camera->GetViewUp()).isApprox(
V(-1, 0, 0)),
"Camera up vector did not rotate");
56 rotation.
apply(camera, boxCenter, V::UnitY(), -0.5, 360);
57 check(std::abs(camera->GetDistance() - 5.0) < 1.e-10,
"Frame rotation lost interactive zoom");
58 check(
V(camera->GetFocalPoint()).isApprox(boxCenter),
"Rotation did not target the box center");
59 const V offset =
V(camera->GetPosition()) - boxCenter;
61 const V pannedPosition = boxCenter + offset + pan;
62 const V pannedFocal = boxCenter + pan;
63 camera->SetPosition(pannedPosition.data());
64 camera->SetFocalPoint(pannedFocal.data());
65 rotation.
apply(camera, boxCenter, V::UnitY(), -0.5, 360);
66 check(
V(camera->GetFocalPoint()).isApprox(boxCenter) &&
67 V(camera->GetPosition()).isApprox(boxCenter + offset),
68 "Manual panning moved the automatic rotation center");
69 rotation.
apply(camera, boxCenter, V::UnitY(), -0.5, 0);
70 check(
V(camera->GetFocalPoint()).isApprox(boxCenter),
"Frame zero lost the box center");
73 auto camera = vtkSmartPointer<vtkCamera>::New();
74 camera->SetPosition(10,0,0);
75 camera->SetFocalPoint(0,0,0);
76 camera->SetViewUp(0,0,1);
77 camera->ParallelProjectionOn();
79 motion.
apply(camera,V::Zero(),V::UnitZ(),0,10,1);
80 check(
V(camera->GetPosition()).norm()<1.e-10,
"Camera did not reach center");
81 check(camera->GetDistance()>9.99 && !camera->GetParallelProjection(),
"Fly-through collapsed view or retained parallel projection");
82 motion.
apply(camera,V::Zero(),V::UnitZ(),0,15,1);
83 check(
V(camera->GetPosition()).isApprox(
V(-5,0,0)),
"Camera did not pass through center");
84 motion.
apply(camera,V::Zero(),V::UnitZ(),18,5,1);
85 motion.
apply(camera,V::Zero(),V::UnitZ(),18,5,1);
86 check(
V(camera->GetPosition()).isApprox(
V(0,5,0)),
"Combined rotation/travel or repeated frame incorrect");
87 motion.
apply(camera,V::Zero(),V::UnitZ(),0,15,0);
88 check(
V(camera->GetPosition()).isApprox(
V(10,0,0)),
"Disabling camera motion did not restore position");
90 check(
argc == 2 ||
argc == 3,
"Expected repository root and optional --without-dd");
91 const bool withDD=
argc==2;
92 const std::filesystem::path root(
argv[1]);
94 std::filesystem::temp_directory_path() /
95 (
"model-surface-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
96 for(
const auto *name : {
"inputFiles",
"config",
"F"})
98 std::filesystem::create_directories(dir / name);
100 const auto input = dir /
"inputFiles";
101 std::filesystem::copy_file(root /
"Library/Materials/W.txt", input /
"W.txt");
102 std::filesystem::copy_file(root /
"Library/DislocationDynamics/DD.txt", input /
"DD.txt");
103 std::filesystem::copy_file(root /
"Library/Meshes/unitCube24.msh", input /
"mesh.msh");
105 std::ofstream
f(input /
"DefectiveCrystal.txt");
106 f <<
"physics=" << (withDD ?
"ElasticDeformation DislocationDynamics" :
"ElasticDeformation") <<
";\nuseFEM=0;\nNsteps=1;\nmaxResolveSteps=0;\ndtMax=1;\n"
107 <<
"outputFrequency=1;\noutputBinary=0;\nstartAtTimeStep=0;\n";
108 std::ofstream
p(input /
"polycrystal.txt");
109 p <<
"materialFile=W.txt;\nmeshFile=mesh.msh;\nabsoluteTemperature=300;\n"
110 <<
"C2G1=1 0 0 0 1 0 0 0 1;\nF=100 0 0 0 100 0 0 0 100;\nX0=0 0 0;\nperiodicFaceIDs=;\n";
111 std::ofstream
e(input /
"ElasticDeformation.txt");
112 e <<
"useElasticDeformationFEM=0;\ninertiaReliefPenaltyFactor=1e-3;\n"
113 <<
"ExternalStress0=0 0 .01 0 0 0;\nExternalStressRate=0 0 .01 0 0 0;\n"
114 <<
"ExternalStrain0=0 0 0 0 0 0;\nExternalStrainRate=0 0 0 0 0 0;\n"
115 <<
"stiffnessRatio=0 0 0 0 0 0;\n";
121 std::set<size_t> allExternalIDs;
122 std::set<const model::Simplex<3, 2> *> firstFaceSimplices;
123 size_t firstFaceID = 0;
124 for(
const auto ®ion : base.mesh.regions())
126 for(
const auto &face : region.second->faces())
128 if(face.second->isExternal())
130 if(allExternalIDs.empty())
132 firstFaceID = face.second->sID;
133 firstFaceSimplices = face.second->simplices();
135 allExternalIDs.insert(face.second->sID);
139 auto selectedIDs=allExternalIDs;
140 selectedIDs.erase(firstFaceID);
142 check(excluded.triangles.size() == coarse.triangles.size() - firstFaceSimplices.size(),
143 "Exclusion did not remove the Mesh-tab face's actual simplices");
144 for(
const auto *face : excluded.boundaryFaces)
146 check(!firstFaceSimplices.count(face),
"Excluded simplex remains in the surface");
149 "Excluding every face did not produce an empty surface");
150 check(coarse.triangles.size() == 24 && fine.triangles.size() == 96,
"Incorrect boundary refinement");
152 for(
const double offset : {0.0, 7.0})
156 for(
const auto *face : fine.boundaryFaces)
158 xTrace.
faces.insert(face);
159 yTrace.
faces.insert(face);
161 const auto cut = fine.split({xTrace, yTrace, xTrace});
162 check(!cut.walls.empty(),
"Missing slip walls");
166 patch.
globalPatches()[
nullptr] = {
V(offset, -100, -100),
V(offset, 100, -100),
V(offset, 100, 100),
167 V(offset, -100, 100)};
168 bool checkedJump =
false;
169 for(
const auto &wall : cut.walls)
171 const size_t a = wall[1],
b = wall[2];
172 if((cut.referencePoints[
a] - cut.referencePoints[
b]).norm() < 1.e-8 &&
173 std::abs(cut.referencePoints[
a].x() - offset) < 1.e-8 &&
174 (cut.samplePoints[
a].x() - offset) * (cut.samplePoints[
b].x() - offset) < 0.0)
178 (4 * std::acos(-1.0));
179 check(jump > 0.9 && jump < 1.1,
"One-sided samples lost the solid-angle displacement jump");
183 check(checkedJump,
"No paired trace samples tested");
185 for(
const auto &
t : cut.triangles)
187 const V a = cut.referencePoints[
t[0]],
b = cut.referencePoints[
t[1]],
c = cut.referencePoints[
t[2]];
188 const V cross = (
b -
a).cross(
c -
a);
189 cutArea += cross.norm() / 2;
190 check(cross.dot((
a +
b +
c) / 3) > 0,
"Cut reversed or degenerated surface triangle");
191 for(
const auto &trace : {xTrace, yTrace})
193 double lo = 0, hi = 0;
194 for(
const auto id :
t)
196 const double d = trace.normal.dot(cut.referencePoints[
id] - trace.point);
197 lo = std::min(lo, d);
198 hi = std::max(hi, d);
200 check(lo > -1.e-8 || hi < 1.e-8,
"Triangle bridges a slip trace");
203 check(std::abs(cutArea - 60000) < 1.e-7,
"Trace cuts changed exterior area");
205 for(
const auto &sample : cut.samplePoints)
207 jump.push_back(
V(0, 0, (sample.x() > offset ? 1.0 : 0.0) + (sample.y() > -9 ? 2.0 : 0.0)));
209 const auto moved = cut.displaced(jump, 10);
211 double topWallArea = 0;
212 for(
const auto &
t : cut.walls)
214 const V a = cut.referencePoints[
t[0]],
b = cut.referencePoints[
t[1]],
c = cut.referencePoints[
t[2]];
215 check((
b -
a).cross(
c -
a).norm() < 1.e-8,
"Wall has finite reference width");
216 const double area = (moved[
t[1]] - moved[
t[0]]).cross(moved[
t[2]] - moved[
t[0]]).norm() / 2;
218 if(std::abs(
a.z() - 50) < 1.e-8 && std::abs(
b.z() - 50) < 1.e-8 && std::abs(
c.z() - 50) < 1.e-8)
223 check(wallArea > 0,
"Step walls collapsed after displacement");
224 check(std::abs(topWallArea - 3000) < 1.e-6,
"Missing or duplicate wall segments at trace crossings");
226 std::map<std::pair<size_t, size_t>,
size_t>
edges;
228 for(
const auto &
t : fine.triangles)
230 const V a = fine.referencePoints[
t[0]],
b = fine.referencePoints[
t[1]],
c = fine.referencePoints[
t[2]];
231 const V cross = (
b -
a).cross(
c -
a);
232 check(cross.dot((
a +
b +
c) / 3) > 0,
"Inward surface triangle");
233 check(cross.norm() / 2 <= 625 * (1 + 1.e-12),
"Triangle area exceeds requested limit");
234 area += cross.norm() / 2;
235 for(
int k = 0;
k < 3; ++
k)
237 const auto endpoints = std::minmax(
t[
k],
t[(
k + 1) % 3]);
238 ++
edges[{endpoints.first, endpoints.second}];
241 check(std::abs(
area - 60000) < 1.e-8,
"Incorrect exterior area or internal faces included");
242 for(
const auto &edge :
edges)
244 check(edge.second == 2,
"Crack or duplicate triangles in reference surface");
246 for(
const auto &
p : fine.referencePoints)
248 check(std::abs(
p.cwiseAbs().maxCoeff() - 50) < 1.e-10,
"Surface point off mesh boundary");
251 for(
const auto &
p : fine.referencePoints)
253 step.push_back(
V(0, 0,
p.x() > 0 ? 1 : 0));
255 const auto stepped = fine.displaced(step, 10);
256 const auto reference = fine.displaced(step, 0);
257 for(
size_t k = 0;
k < step.size(); ++
k)
259 check((stepped[
k] - fine.referencePoints[
k]).isApprox(10 * step[
k]),
"Step amplitude lost");
260 check(reference[
k].isApprox(fine.referencePoints[
k]),
"Zero amplification did not restore surface");
262 for(
double badArea : {0.0, -1.0, 1.e-20})
264 bool rejected =
false;
269 catch(
const std::invalid_argument &)
273 check(rejected,
"Invalid or excessive refinement accepted");
278 auto window = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
279 window->SetReadyForRendering(
false);
280 auto renderer = vtkSmartPointer<vtkRenderer>::New();
281 window->AddRenderer(renderer);
282 const int originalLights = renderer->GetLights()->GetNumberOfItems();
285 auto *ambient = surface.findChild<QDoubleSpinBox*>(
"surfaceAmbient");
286 auto *diffuse = surface.findChild<QDoubleSpinBox*>(
"surfaceDiffuse");
287 auto *specular = surface.findChild<QDoubleSpinBox*>(
"surfaceSpecular");
288 check(ambient && diffuse && specular,
"Missing shading controls");
289 check(ambient->value() == 0.25 && diffuse->value() == 0.75 && specular->value() == 0.0,
290 "Incorrect shading defaults");
291 const auto meshTime = surface.
polyData->GetMTime();
292 ambient->setValue(0.4);
293 diffuse->setValue(0.5);
294 specular->setValue(0.2);
295 check(surface.
actor->GetProperty()->GetAmbient() == 0.4 &&
296 surface.
actor->GetProperty()->GetDiffuse() == 0.5 &&
297 surface.
actor->GetProperty()->GetSpecular() == 0.2,
298 "Shading controls did not update the material immediately");
299 check(surface.
polyData->GetMTime() == meshTime,
"Shading change rebuilt the surface");
303 check(surface.
polyData->GetNumberOfPoints() == 0,
"Hidden surface computed eagerly");
306 auto sample = vtkSmartPointer<vtkPolyData>::New();
307 auto samplePoints = vtkSmartPointer<vtkPoints>::New();
308 samplePoints->SetDataTypeToDouble();
309 for(
const V& point : {
V(0,0,0),
V(1,0,0),
V(0,1,0),
310 V(1,0,0),
V(0,0,0),
V(0,-1,0.02),
311 V(0,0,1.e-9),
V(1,0,1.e-9),
V(0,1,1.e-9)})
313 samplePoints->InsertNextPoint(point.data());
315 auto sampleCells = vtkSmartPointer<vtkCellArray>::New();
319 sampleCells->InsertNextCell(3, ids);
321 sample->SetPoints(samplePoints);
322 sample->SetPolys(sampleCells);
326 "Shading failed to merge duplicates or closed a small step");
327 auto* shaded = surface.
normals->GetOutput();
329 for(vtkIdType
k = 0;
k < shaded->GetNumberOfPoints(); ++
k)
331 double xyz[3], normal[3];
332 shaded->GetPoint(
k, xyz);
333 if(
V(xyz).norm() == 0.0)
335 shaded->GetPointData()->GetNormals()->GetTuple(
k, normal);
336 smooth = normal[1] > 0.001 && normal[1] < 0.019;
339 check(smooth,
"Shading retained a normal jump across nearly coplanar triangles");
343 "Surface tab did not build after enable");
344 check(surface.
normals->GetOutput()->GetCellData()->GetNormals() !=
nullptr,
"Missing updated normals");
345 check(renderer->GetLights()->IsItemPresent(surface.
reliefLight),
"Missing surface relief light");
346 for(
size_t k = 0;
k < fine.referencePoints.size(); ++
k)
351 fine.referencePoints[
k] +
353 crystal.
displacement(fine.referencePoints[
k],
nullptr,
nullptr, fine.parents[
k]);
354 check(
V(xyz[0], xyz[1], xyz[2]).isApprox(expected, 1.e-12),
355 "Surface did not use displacement field");
358 "Missing physics selectors");
360 check(physics->isChecked() && physics->text().toStdString() == crystal.
microstructures().front()->tag,
361 "Physics selector does not match Fields defaults");
362 physics->setChecked(
false);
363 for(
size_t k = 0;
k < fine.referencePoints.size(); ++
k)
367 check(
V(xyz[0], xyz[1], xyz[2]).isApprox(fine.referencePoints[
k]),
368 "Unselected physics still contributes displacement");
371 check(!physics->isChecked(),
"Frame update lost physics selection");
373 physics->setChecked(
true);
375 for(
size_t k = 0;
k < fine.referencePoints.size(); ++
k)
380 fine.referencePoints[
k] +
382 crystal.
displacement(fine.referencePoints[
k],
nullptr,
nullptr, fine.parents[
k]);
383 check(
V(xyz[0], xyz[1], xyz[2]).isApprox(expected, 1.e-12),
384 "Hidden physics selection did not refresh on enable");
386 double first[3], second[3];
388 base.simulationParameters.totalTime = 1.0;
390 surface.
polyData->GetPoint(0, second);
392 "Frame update did not recompute surface displacement");
396 "Repeated frame accumulated deformation");
400 "Amplification control failed");
401 check(!surface.
clipSurface->isChecked(),
"Clipping should be disabled initially");
403 const auto checkClipped = [&](
const V ¢er,
const V &normal)
406 auto *output = surface.
normals->GetOutput();
407 check(output->GetNumberOfPoints() > 0,
"Clipping unexpectedly removed the entire surface");
408 for(vtkIdType
k = 0;
k < output->GetNumberOfPoints(); ++
k)
411 output->GetPoint(
k, xyz);
412 check(normal.dot(
V(xyz[0], xyz[1], xyz[2]) - center) >= -1.e-6,
413 "Rendered surface contains points on discarded side");
416 checkClipped(V::Zero(), V::UnitX());
420 checkClipped(
V(10, 0, 0), -V::UnitX());
422 checkClipped(
V(10, 0, 0), -V::UnitX());
425 const double offset = (base.mesh.xMax() - base.mesh.xMin()).norm() * 0.1;
426 const V shifted =
V(10, 0, 0) - offset * V::UnitX();
427 check(
V(surface.
clipPlane->GetOrigin()).isApprox(shifted),
"Slider moved off the clip normal");
429 checkClipped(shifted, -V::UnitX());
431 check(
V(surface.
clipPlane->GetOrigin()).isApprox(shifted),
"Frame update lost slider offset");
435 check(
V(surface.
clipPlane->GetOrigin()).isApprox(shifted),
"Clip toggle lost slider offset");
440 const V diagonalNormal =
V(1, 1, 0).normalized();
441 check(
V(surface.
clipPlane->GetOrigin()).isApprox(
V(10, 0, 0) - offset * diagonalNormal),
442 "Slider failed for oblique normal");
448 "Invalid clipping normal was not rejected");
449 checkClipped(
V(10, 0, 0), -V::UnitX());
451 check(surface.
normals->GetOutput()->GetNumberOfCells() == 96,
452 "Disabling clipping did not restore complete surface");
456 check(!surface.
actor->GetVisibility(),
"Hidden surface remains visible");
458 "Hidden surface changed scene lighting");
460 check(surface.
actor->GetVisibility(),
"Surface failed to refresh after re-enable");
463 check(!surface.
actor->GetVisibility(),
"Invalid mesh input left stale surface visible");
467 "Surface did not recover after correcting mesh input");
471 auto plane = base.dislocationDynamics().glidePlaneFactory.getFromKey(
478 check(surface.
actor->GetVisibility(),
"Factory trace extraction failed");
479 check(surface.
polyData->GetNumberOfPoints() >
static_cast<vtkIdType
>(fine.referencePoints.size()),
480 "Live glide plane did not split surface");
483 check(surface.
polyData->GetNumberOfPoints() ==
static_cast<vtkIdType
>(fine.referencePoints.size()),
484 "Expired glide plane cuts survived frame update");
489 const int savedThreads = omp_get_max_threads();
490 const int savedDynamic = omp_get_dynamic();
492 omp_set_num_threads(1);
496 auto plane = base.dislocationDynamics().glidePlaneFactory.getFromKey(
504 "Parallel sampling fixture failed");
505 std::vector<V> serial;
506 for(vtkIdType
k = 0;
k < surface.
polyData->GetNumberOfPoints(); ++
k)
510 serial.emplace_back(xyz[0], xyz[1], xyz[2]);
513 omp_set_num_threads(4);
517 surface.
polyData->GetNumberOfPoints() ==
static_cast<vtkIdType
>(serial.size()),
518 "Parallel update failed");
519 for(
size_t k = 0;
k < serial.size(); ++
k)
523 check(
V(xyz[0], xyz[1], xyz[2]).isApprox(serial[
k], 1.e-12),
524 "Serial and parallel displacement differ");
526 const double savedTime = base.simulationParameters.totalTime;
527 base.simulationParameters.totalTime = std::numeric_limits<double>::quiet_NaN();
529 check(!surface.
actor->GetVisibility() && surface.
status->text().contains(
"Nonfinite displacement"),
530 "Worker failure was not reported safely");
531 base.simulationParameters.totalTime = savedTime;
533 check(surface.
actor->GetVisibility(),
"Surface did not recover after worker failure");
536 auto *faceCheck = surface.findChild<QCheckBox*>(QString(
"surfaceExternalFace_%1").arg(firstFaceID));
537 check(faceCheck !=
nullptr,
"Missing external face selection");
539 "Incorrect default face selections");
540 faceCheck->setChecked(
false);
541 check(surface.
polyData->GetNumberOfCells() ==
static_cast<vtkIdType
>(excluded.triangles.size()),
542 "Face checkbox did not rebuild the surface");
544 check(surface.
polyData->GetNumberOfCells() ==
static_cast<vtkIdType
>(excluded.triangles.size()),
545 "Frame update lost face exclusions");
546 for(
const auto id : allExternalIDs)
548 surface.findChild<QCheckBox*>(QString(
"surfaceExternalFace_%1").arg(
id))->setChecked(
false);
550 check(surface.
polyData->GetNumberOfCells() == 0,
"Selecting all faces left stale geometry");
553 check(surface.
polyData->GetNumberOfCells()==0,
"External group lost the empty selection");
554 faceCheck->setChecked(
true);
556 check(surface.
polyData->GetNumberOfCells()==0,
"Disabled external group still renders faces");
558 check(faceCheck->isChecked() && surface.
polyData->GetNumberOfCells()==
static_cast<vtkIdType
>(firstFaceSimplices.size()),
559 "External group did not restore the individual selection");
561 omp_set_num_threads(savedThreads);
562 omp_set_dynamic(savedDynamic);
565 check(renderer->GetActors()->GetNumberOfItems() == 0,
"Surface actor leaked after destruction");
566 check(renderer->GetLights()->GetNumberOfItems() == originalLights,
567 "Surface relief light leaked after destruction");
569 std::filesystem::remove_all(dir);
571 <<
"PASS: surface mesh topology, refinement, displacement steps, frame updates, controls and lifecycle\n";
574 catch(
const std::exception &error)
576 std::cerr << error.what() <<
'\n';