// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen // SPDX-License-Identifier: BSD-3-Clause #include "vtkContourLoopExtraction.h" #include "vtkArrayDispatch.h" #include "vtkCellArray.h" #include "vtkCellData.h" #include "vtkExecutive.h" #include "vtkGarbageCollector.h" #include "vtkInformation.h" #include "vtkInformationVector.h" #include "vtkObjectFactory.h" #include "vtkPointData.h" #include "vtkPoints.h" #include "vtkPolyData.h" #include #include VTK_ABI_NAMESPACE_BEGIN vtkStandardNewMacro(vtkContourLoopExtraction); //------------------------------------------------------------------------------ namespace { // Note on the definition of parametric coordinates: Given a sequence of // lines segments (vi,vi+1) that form a primitive (e.g., polyline or // polygon), the parametric coordinate t along the primitive is // [i,i+1). Any point (like an intersection point on the segment) is i+t, // where 0 <= t < 1. // Infrastructure for cropping---------------------------------------------- struct LoopPoint { double T; // parametric coordinate along linked lines vtkIdType Id; LoopPoint(double t, vtkIdType id) : T(t) , Id(id) { } }; // Special sort operation on primitive parametric coordinate T------------- bool PointSorter(LoopPoint const& lhs, LoopPoint const& rhs) { return lhs.T < rhs.T; } // Vectors are used to hold points. typedef std::vector LoopPointType; // Update the scalar range------------------------------------------------- void UpdateRange(vtkDataArray* scalars, vtkIdType pid, double range[2]) { if (!scalars) { return; } int numComp = scalars->GetNumberOfComponents(); double s; for (int i = 0; i < numComp; ++i) { s = scalars->GetComponent(pid, i); range[0] = (s < range[0] ? s : range[0]); range[1] = (s > range[1] ? s : range[1]); } } // March along connected lines to the end---------------------------------- // pts[0] is assumed to be the starting point and already inserted. vtkIdType TraverseLoop(double dir, vtkPolyData* polyData, vtkIdType lineId, vtkIdType start, LoopPointType& sortedPoints, std::vector& visited, vtkDataArray* scalars, double range[2]) { vtkIdType last = start, numInserted = 0; double t = 0.0; bool terminated = false; vtkIdType ncells; vtkIdType npts; const vtkIdType* pts; vtkIdType* cells; vtkIdType nei; vtkIdType lastCell = lineId; polyData->GetCellPoints(lineId, npts, pts); // Recall that we are working with 2-pt lines while (!terminated) { last = (pts[0] != last ? pts[0] : pts[1]); numInserted++; t = dir * static_cast(numInserted); sortedPoints.emplace_back(t, last); UpdateRange(scalars, last, range); polyData->GetPointCells(last, ncells, cells); if (ncells == 1 || last == start) // this is the last point { return last; } else if (ncells == 2) // continue along loop { nei = (cells[0] != lastCell ? cells[0] : cells[1]); polyData->GetCellPoints(nei, npts, pts); visited[nei] = 1; lastCell = nei; } else // non-manifold, for now just quit (TODO: break apart loops) { terminated = true; break; } } return last; } // March along connected lines to the end---------------------------------- void OutputPolygon(LoopPointType& sortedPoints, vtkPoints* inPts, vtkCellArray* outLines, vtkCellArray* outPolys, int loopClosure) { // Check to see that last point is the same as the first. Such a loop is // closed and can be directly output. Otherwise, check on the strategy // for closing the loop and close as appropriate. vtkIdType num = static_cast(sortedPoints.size()); if (sortedPoints[0].Id == sortedPoints[num - 1].Id) { --num; sortedPoints.erase(sortedPoints.begin() + num); } else if (loopClosure == VTK_LOOP_CLOSURE_ALL) { // do nothing and it will close between the first and last points } // If here we assume that the loop begins and ends on the given bounding // box (i.e. the boundary of the data). Close the loop by walking the // bounding box in the plane defined by the Normal plus the loop start // point. else if (loopClosure == VTK_LOOP_CLOSURE_BOUNDARY) { // First check the simple case, complete the loop along horizontal or // vertical lines (assumed on the boundary). double p0[3], p1[3], delX, delY; inPts->GetPoint(sortedPoints[0].Id, p0); inPts->GetPoint(sortedPoints[num - 1].Id, p1); delX = fabs(p0[0] - p1[0]); delY = fabs(p0[1] - p1[1]); // if no change in either the x or y direction just return, the loop will complete if (delX < FLT_EPSILON || delY < FLT_EPSILON) { // do nothing loop will complete; points are along same (horizontal or vertical) boundary // edge } // Otherwise check if the points are on the "boundary" and then complete the loop // along the shortest path around the boundary. else { return; } } // Don't close else // loopClosure == VTK_LOOP_CLOSURE_OFF { return; } // Return if not a valid loop if (num < 3) { return; } // If here can output the loop if (outLines) { outLines->InsertNextCell(num + 1); for (vtkIdType i = 0; i < num; ++i) { outLines->InsertCellPoint(sortedPoints[i].Id); } outLines->InsertCellPoint(sortedPoints[0].Id); } if (outPolys) { outPolys->InsertNextCell(num); for (vtkIdType i = 0; i < num; ++i) { outPolys->InsertCellPoint(sortedPoints[i].Id); } } } using PointMap = std::vector; struct MarkUses : public vtkCellArray::DispatchUtilities { template void operator()( OffsetsT* vtkNotUsed(offsets), ConnectivityT* conn, vtkIdType numIds, PointMap& ptMap) { auto connRange = GetRange(conn); for (vtkIdType i = 0; i < numIds; ++i) { ptMap[connRange[i]] = 1; } } }; // Helper function to in place mark point usage void MarkPointUses(vtkCellArray* ca, vtkIdType numConn, PointMap& ptMap) { ca->Dispatch(MarkUses{}, numConn, ptMap); } // Helper functions to clean output data struct UpdateUses : public vtkCellArray::DispatchUtilities { template void operator()( OffsetsT* vtkNotUsed(offsets), ConnectivityT* conn, vtkIdType numIds, PointMap& ptMap) { auto connRange = GetRange(conn); for (vtkIdType i = 0; i < numIds; ++i) { connRange[i] = ptMap[connRange[i]]; } } }; // Helper function to in place update point numbering void UpdatePointUses(vtkCellArray* ca, vtkIdType numConn, PointMap& ptMap) { ca->Dispatch(UpdateUses{}, numConn, ptMap); } // Discard unused points; renumber output polylines and polygons. // Performs this operation in place. void CleanOutputPoints(vtkPolyData* output) { // Mark points used by lines or polygons, and create a point map // of old point ids to new point ids. vtkIdType numPts = output->GetNumberOfPoints(); std::vector ptMap(numPts, 0); vtkPoints* pts = output->GetPoints(); // Get the connectivity of the lines and polygons. Run through them // and mark points that are used. vtkCellArray* lines = output->GetLines(); vtkIdType numLinesIds = lines->GetNumberOfConnectivityIds(); if (numLinesIds > 0) { MarkPointUses(lines, numLinesIds, ptMap); } vtkCellArray* polys = output->GetPolys(); vtkIdType numPolysIds = polys->GetNumberOfConnectivityIds(); if (numPolysIds > 0) { MarkPointUses(polys, numPolysIds, ptMap); } // Renumber points / build the point map vtkNew newPts; vtkIdType newId; for (auto pId = 0; pId < numPts; ++pId) { if (ptMap[pId] != 0) { newId = newPts->InsertNextPoint(pts->GetPoint(pId)); ptMap[pId] = newId; } } output->SetPoints(newPts); // Update the point ids (in place). if (numLinesIds > 0) { UpdatePointUses(lines, numLinesIds, ptMap); } if (numPolysIds > 0) { UpdatePointUses(polys, numPolysIds, ptMap); } } // CleanOutputPoints } // anonymous namespace //------------------------------------------------------------------------------ // Instantiate object with empty loop. vtkContourLoopExtraction::vtkContourLoopExtraction() { this->LoopClosure = VTK_LOOP_CLOSURE_BOUNDARY; this->ScalarThresholding = false; this->ScalarRange[0] = 0.0; this->ScalarRange[1] = 1.0; this->Normal[0] = 0.0; this->Normal[1] = 0.0; this->Normal[2] = 1.0; this->OutputMode = VTK_OUTPUT_POLYGONS; this->CleanPoints = true; } //------------------------------------------------------------------------------ vtkContourLoopExtraction::~vtkContourLoopExtraction() = default; //------------------------------------------------------------------------------ int vtkContourLoopExtraction::RequestData(vtkInformation* vtkNotUsed(request), vtkInformationVector** inputVector, vtkInformationVector* outputVector) { // get the info objects vtkInformation* inInfo = inputVector[0]->GetInformationObject(0); vtkInformation* outInfo = outputVector->GetInformationObject(0); // get the input and output vtkPolyData* input = vtkPolyData::SafeDownCast(inInfo->Get(vtkDataObject::DATA_OBJECT())); vtkPolyData* output = vtkPolyData::SafeDownCast(outInfo->Get(vtkDataObject::DATA_OBJECT())); // Initialize and check data vtkDebugMacro(<< "Loop extraction..."); vtkPoints* points = input->GetPoints(); vtkIdType numPts; if (!points || (numPts = input->GetNumberOfPoints()) < 1) { vtkErrorMacro("Input contains no points"); return 1; } vtkCellArray* lines = input->GetLines(); vtkIdType numLines = lines->GetNumberOfCells(); if (numLines < 1) { vtkErrorMacro("Input contains no lines"); return 1; } vtkPointData* inPD = input->GetPointData(); vtkDataArray* scalars = nullptr; if (this->ScalarThresholding) { scalars = inPD->GetScalars(); } // Prepare output output->SetPoints(points); vtkSmartPointer outLines; vtkSmartPointer outPolys; if (this->OutputMode == VTK_OUTPUT_POLYLINES || this->OutputMode == VTK_OUTPUT_BOTH) { outLines.TakeReference(vtkCellArray::New()); output->SetLines(outLines); } if (this->OutputMode == VTK_OUTPUT_POLYGONS || this->OutputMode == VTK_OUTPUT_BOTH) { outPolys.TakeReference(vtkCellArray::New()); output->SetPolys(outPolys); } output->GetPointData()->PassData(inPD); // Create a clean polydata containing only line segments and without other // topological types. This simplifies the filter. vtkIdType npts; const vtkIdType* pts; vtkIdType lineId; vtkNew newLines; newLines->AllocateEstimate(numLines, 2); for (lineId = 0, lines->InitTraversal(); lines->GetNextCell(npts, pts); ++lineId) { for (int i = 0; i < (npts - 1); ++i) { newLines->InsertNextCell(2, pts + i); } } vtkNew polyData; polyData->SetPoints(points); polyData->SetLines(newLines); polyData->GetPointData()->PassData(inPD); polyData->BuildLinks(); // Keep track of what cells are visited numLines = newLines->GetNumberOfCells(); std::vector visited(numLines, 0); // Loop over all lines, visit each one. Build a loop from the seed line if // not visited. vtkIdType start, rightEnd; LoopPointType sortedPoints; double range[2]; for (lineId = 0, newLines->InitTraversal(); newLines->GetNextCell(npts, pts); ++lineId) { if (this->CheckAbort()) { break; } if (!visited[lineId]) { visited[lineId] = 1; start = pts[0]; sortedPoints.clear(); sortedPoints.emplace_back(0.0, start); range[0] = VTK_FLOAT_MAX; range[1] = VTK_FLOAT_MIN; UpdateRange(scalars, start, range); rightEnd = TraverseLoop(1.0, polyData, lineId, start, sortedPoints, visited, scalars, range); if (rightEnd == start) { // extract loop, we've traversed all the way around if (!scalars || (range[0] <= this->ScalarRange[1] && range[1] >= this->ScalarRange[0])) { OutputPolygon(sortedPoints, points, outLines, outPolys, this->LoopClosure); } } else { // go the other direction and see where we end up TraverseLoop(-1.0, polyData, lineId, start, sortedPoints, visited, scalars, range); std::sort(sortedPoints.begin(), sortedPoints.end(), &PointSorter); OutputPolygon(sortedPoints, points, outLines, outPolys, this->LoopClosure); } } // if not visited start a loop } if (this->CleanPoints) { CleanOutputPoints(output); } // Debug information if (outLines != nullptr) { vtkDebugMacro(<< "Generated " << outLines->GetNumberOfCells() << " lines\n"); } if (outPolys != nullptr) { vtkDebugMacro(<< "Generated " << outPolys->GetNumberOfCells() << " polygons\n"); } return 1; } //------------------------------------------------------------------------------ const char* vtkContourLoopExtraction::GetLoopClosureAsString() { if (this->LoopClosure == VTK_LOOP_CLOSURE_OFF) { return "LoopClosureOff"; } else if (this->LoopClosure == VTK_LOOP_CLOSURE_BOUNDARY) { return "LoopClosureBoundary"; } else { return "LoopClosureAll"; } } //------------------------------------------------------------------------------ const char* vtkContourLoopExtraction::GetOutputModeAsString() { if (this->OutputMode == VTK_OUTPUT_POLYGONS) { return "OutputModePolygons"; } else if (this->OutputMode == VTK_OUTPUT_POLYLINES) { return "OutputModePolylines"; } else { return "OutputModeBoth"; } } //------------------------------------------------------------------------------ void vtkContourLoopExtraction::PrintSelf(ostream& os, vtkIndent indent) { this->Superclass::PrintSelf(os, indent); os << indent << "Loop Closure: "; os << this->GetLoopClosureAsString() << "\n"; os << indent << "Scalar Thresholding: " << (this->ScalarThresholding ? "On\n" : "Off\n"); double* range = this->GetScalarRange(); os << indent << "Scalar Range: (" << range[0] << ", " << range[1] << ")\n"; double* n = this->GetNormal(); os << indent << "Normal: (" << n[0] << ", " << n[1] << ", " << n[2] << ")\n"; os << indent << "Output Mode: "; os << this->GetOutputModeAsString() << "\n"; os << indent << "Clean Points: " << (this->CleanPoints ? "On\n" : "Off\n"); } VTK_ABI_NAMESPACE_END