////////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2026 OVITO GmbH, Germany
//
//  This file is part of OVITO (Open Visualization Tool).
//
//  OVITO is free software; you can redistribute it and/or modify it either under the
//  terms of the GNU General Public License version 3 as published by the Free Software
//  Foundation (the "GPL") or, at your option, under the terms of the MIT License.
//  If you do not alter this notice, a recipient may use your version of this
//  file under either the GPL or the MIT License.
//
//  You should have received a copy of the GPL along with this program in a
//  file LICENSE.GPL.txt.  You should have received a copy of the MIT License along
//  with this program in a file LICENSE.MIT.txt
//
//  This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
//  either express or implied. See the GPL or the MIT License for the specific language
//  governing rights and limitations.
//
////////////////////////////////////////////////////////////////////////////////////////

#include <ovito/grid/Grid.h>
#include <ovito/grid/objects/VoxelGrid.h>
#include <ovito/mesh/surface/SurfaceMesh.h>
#include <ovito/mesh/surface/SurfaceMeshBuilder.h>
#include <ovito/mesh/surface/SurfaceMeshVis.h>
#include <ovito/stdobj/simcell/SimulationCell.h>
#include <ovito/core/dataset/DataSet.h>
#include <ovito/core/app/Application.h>
#include <ovito/core/dataset/pipeline/ModificationNode.h>
#include <ovito/core/utilities/concurrent/ParallelFor.h>
#include "CreateIsosurfaceModifier.h"
#include "MarchingCubes.h"

namespace Ovito {

IMPLEMENT_CREATABLE_OVITO_CLASS(CreateIsosurfaceModifier);
OVITO_CLASSINFO(CreateIsosurfaceModifier, "DisplayName", "Create isosurface");
OVITO_CLASSINFO(CreateIsosurfaceModifier, "Description", "Compute the isosurface of a scalar value field.");
OVITO_CLASSINFO(CreateIsosurfaceModifier, "ModifierCategory", "Visualization");
DEFINE_PROPERTY_FIELD(CreateIsosurfaceModifier, subject);
DEFINE_PROPERTY_FIELD(CreateIsosurfaceModifier, sourceProperty);
DEFINE_PROPERTY_FIELD(CreateIsosurfaceModifier, transferFieldValues);
DEFINE_PROPERTY_FIELD(CreateIsosurfaceModifier, smoothingLevel);
DEFINE_PROPERTY_FIELD(CreateIsosurfaceModifier, identifyRegions);
DEFINE_REFERENCE_FIELD(CreateIsosurfaceModifier, isolevelController);
DEFINE_REFERENCE_FIELD(CreateIsosurfaceModifier, surfaceMeshVis);
SET_PROPERTY_FIELD_LABEL(CreateIsosurfaceModifier, sourceProperty, "Source property");
SET_PROPERTY_FIELD_LABEL(CreateIsosurfaceModifier, isolevelController, "Isolevel");
SET_PROPERTY_FIELD_LABEL(CreateIsosurfaceModifier, transferFieldValues, "Transfer field values to surface");
SET_PROPERTY_FIELD_LABEL(CreateIsosurfaceModifier, identifyRegions, "Identify volumetric regions");
SET_PROPERTY_FIELD_LABEL(CreateIsosurfaceModifier, smoothingLevel, "Smoothing level");
SET_PROPERTY_FIELD_UNITS_AND_MINIMUM(CreateIsosurfaceModifier, smoothingLevel, IntegerParameterUnit, 0);

/******************************************************************************
* Constructor.
******************************************************************************/
void CreateIsosurfaceModifier::initializeObject(ObjectInitializationFlags flags)
{
    Modifier::initializeObject(flags);

    if(!flags.testFlag(ObjectInitializationFlag::DontInitializeObject)) {
        setIsolevelController(ControllerManager::createFloatController());

        // Create the vis element for rendering the surface generated by the modifier.
        setSurfaceMeshVis(OORef<SurfaceMeshVis>::create(flags));
        surfaceMeshVis()->setShowCap(false);
        surfaceMeshVis()->setSmoothShading(true);
        surfaceMeshVis()->setObjectTitle(tr("Isosurface"));
    }
}

/******************************************************************************
* Is called when the value of a property of this object has changed.
******************************************************************************/
void CreateIsosurfaceModifier::propertyChanged(const PropertyFieldDescriptor* field)
{
    if(field == PROPERTY_FIELD(CreateIsosurfaceModifier::sourceProperty) && !shouldIgnoreChanges()) {
        // Changes of some the modifier's parameters affect the result of getPipelineEditorShortInfo().
        notifyDependents(ReferenceEvent::ObjectStatusChanged);
    }

    Modifier::propertyChanged(field);
}

/******************************************************************************
* Is called when a RefTarget referenced by this object generated an event.
******************************************************************************/
bool CreateIsosurfaceModifier::referenceEvent(RefTarget* source, const ReferenceEvent& event)
{
    if(event.type() == ReferenceEvent::TargetChanged && source == isolevelController()) {
        // Changes of some the modifier's parameters affect the result of getPipelineEditorShortInfo().
        notifyDependents(ReferenceEvent::ObjectStatusChanged);
    }

    return Modifier::referenceEvent(source, event);
}

/******************************************************************************
* Asks the modifier whether it can be applied to the given input data.
******************************************************************************/
bool CreateIsosurfaceModifier::OOMetaClass::isApplicableTo(const DataCollection& input) const
{
    return input.containsObject<VoxelGrid>();
}

/******************************************************************************
* This method is called by the system when the modifier has been inserted
* into a pipeline.
******************************************************************************/
void CreateIsosurfaceModifier::initializeModifier(const ModifierInitializationRequest& request)
{
    Modifier::initializeModifier(request);

    // Use the first available voxel grid from the input state as data source when the modifier is newly created.
    if(!sourceProperty() && subject().dataPath().isEmpty() && this_task::isInteractive()) {
        const PipelineFlowState& input = request.modificationNode()->evaluateInput(request).blockForResult();
        if(const VoxelGrid* grid = input.getObject<VoxelGrid>()) {
            setSubject(PropertyContainerReference(&grid->getOOMetaClass(), grid->identifier()));
        }
    }

    // Use the first available property from the input grid as data source when the modifier is newly created.
    if(!sourceProperty() && subject() && this_task::isInteractive()) {
        const PipelineFlowState& input = request.modificationNode()->evaluateInput(request).blockForResult();
        if(const VoxelGrid* grid = dynamic_object_cast<VoxelGrid>(input.getLeafObject(subject()))) {
            for(const Property* property : grid->properties()) {
                setSourceProperty(PropertyReference(property, (property->componentCount() > 1) ? 0 : -1));
                break;
            }
        }
    }
}

/******************************************************************************
* This function is called by the pipeline system before a new modifier evaluation begins.
******************************************************************************/
void CreateIsosurfaceModifier::preevaluateModifier(const ModifierEvaluationRequest& request, PipelineEvaluationResult::EvaluationTypes& evaluationTypes, TimeInterval& validityInterval) const
{
    // Indicate that we cannot handle interactive requests, because isosurface calculation is a computationally intensive operation.
    if(request.interactiveMode()) {
        evaluationTypes = PipelineEvaluationResult::EvaluationType::Interactive;
        return;
    }

    if(isolevelController())
        validityInterval.intersect(isolevelController()->validityInterval(request.time()));

    evaluationTypes = PipelineEvaluationResult::EvaluationType::Noninteractive;
}

/******************************************************************************
* Modifies the input data.
******************************************************************************/
Future<PipelineFlowState> CreateIsosurfaceModifier::evaluateModifier(const ModifierEvaluationRequest& request, PipelineFlowState&& state)
{
    if(!subject())
        throw Exception(tr("No input voxel grid set."));
    if(subject().dataClass() != &VoxelGrid::OOClass())
        throw Exception(tr("Selected modifier input is not a voxel data grid."));
    if(!sourceProperty())
        throw Exception(tr("Please select an input field quantity for the isosurface calculation."));

    // In interactive mode, fetch and return outdated results from the pipeline cache if available.
    if(request.interactiveMode()) {
        if(PipelineFlowState cachedState = request.modificationNode()->getCachedPipelineNodeOutput(request.time(), true)) {
            if(const SurfaceMesh* cachedSurface = cachedState.getObjectBy<SurfaceMesh>(request.modificationNode(), QStringLiteral("isosurface"))) {
                state.addObject(cachedSurface);
            }
            if(const DataTable* cachedTable = cachedState.getObjectBy<DataTable>(request.modificationNode(), QStringLiteral("isosurface-histogram"))) {
                state.addObject(cachedTable);
            }
            // Adopt all global attributes computed by the modifier from the cached state.
            state.adoptAttributesFrom(cachedState, request.modificationNode());
        }
        return std::move(state);
    }

    // Get modifier inputs.
    const VoxelGrid* voxelGrid = static_object_cast<VoxelGrid>(state.expectLeafObject(subject()));
    voxelGrid->verifyIntegrity();
    OVITO_ASSERT(voxelGrid->domain());
    if(voxelGrid->domain()->is2D())
        throw Exception(tr("Cannot generate isosurface for a two-dimensional voxel grid. Input must be a 3d grid."));

    // Look up input voxel property.
    ConstPropertyPtr property;
    int vectorComponent;
    QString errorDescription;
    std::tie(property, vectorComponent) = sourceProperty().findInContainerWithComponent(voxelGrid, errorDescription);
    if(!property)
        throw Exception(std::move(errorDescription));
    if(property->dataType() != Property::FloatDefault)
        throw Exception(tr("Wrong data type. Can construct isosurface only for standard-precision floating-point values."));

    for(size_t dim = 0; dim < 3; dim++)
        if(voxelGrid->shape()[dim] <= 1)
            throw Exception(tr("Cannot generate isosurface for this voxel grid with dimensions %1 x %2 x %3. Must be at least 2 voxels wide in each spatial direction.")
                .arg(voxelGrid->shape()[0]).arg(voxelGrid->shape()[1]).arg(voxelGrid->shape()[2]));

    TimeInterval validityInterval = state.stateValidity();
    FloatType isolevel = isolevelController() ? isolevelController()->getFloatValue(request.time(), validityInterval) : 0;

    // Collect the set of voxel grid properties that should be transferred over to the isosurface mesh vertices.
    std::vector<ConstPropertyPtr> auxiliaryProperties;
    if(transferFieldValues()) {
        for(const Property* property : voxelGrid->properties()) {
            auxiliaryProperties.push_back(property);
        }
    }

    // Create an empty surface mesh object.
    SurfaceMesh* mesh = state.createObjectWithVis<SurfaceMesh>(QStringLiteral("isosurface"), request.modificationNode(), surfaceMeshVis(), tr("Isosurface"));
    mesh->setDomain(voxelGrid->domain());

    // Create an empty data table for the field value histogram.
    DataTable* histogram = state.createObject<DataTable>(QStringLiteral("isosurface-histogram"), request.modificationNode(), DataTable::Histogram, sourceProperty().nameWithComponent());
    histogram->setAxisLabelX(sourceProperty().nameWithComponent());

    // The actual computation can be performed in a separate worker thread.
    return asyncLaunch([
            state = std::move(state),
            gridShape = voxelGrid->shape(),
            gridType = voxelGrid->gridType(),
            property = std::move(property),
            vectorComponent,
            mesh = std::move(mesh),
            isolevel,
            identifyRegions = identifyRegions(),
            smoothingLevel = smoothingLevel(),
            auxiliaryProperties = std::move(auxiliaryProperties),
            histogram = std::move(histogram),
            createdByNode = request.modificationNodeWeak()]() mutable
    {
        TaskProgress progress(this_task::ui());
        progress.setText(tr("Constructing isosurface"));

        // Set up callback function returning the field value, which will be passed to the marching cubes algorithm.
        BufferReadAccess<FloatType*> data(property);
        auto getFieldValue = [
                _data = data.cbegin() + vectorComponent,
                _pbcFlags = mesh->domain() ? mesh->domain()->pbcFlags() : std::array<bool,3>{{false,false,false}},
                _gridShape = gridShape,
                _dataStride = data.componentCount()
                ](int i, int j, int k) -> FloatType {
            if(_pbcFlags[0]) {
                if(i == _gridShape[0]) i = 0;
            }
            else {
                if(i == 0 || i > _gridShape[0]) return std::numeric_limits<FloatType>::lowest();
                i--;
            }
            if(_pbcFlags[1]) {
                if(j == _gridShape[1]) j = 0;
            }
            else {
                if(j == 0 || j > _gridShape[1]) return std::numeric_limits<FloatType>::lowest();
                j--;
            }
            if(_pbcFlags[2]) {
                if(k == _gridShape[2]) k = 0;
            }
            else {
                if(k == 0 || k > _gridShape[2]) return std::numeric_limits<FloatType>::lowest();
                k--;
            }
            OVITO_ASSERT(i >= 0 && i < _gridShape[0]);
            OVITO_ASSERT(j >= 0 && j < _gridShape[1]);
            OVITO_ASSERT(k >= 0 && k < _gridShape[2]);
            return _data[(i + j*_gridShape[0] + k*_gridShape[0]*_gridShape[1]) * _dataStride];
        };

        // Prepare the output mesh structure.
        SurfaceMeshBuilder meshBuilder(mesh);

        // Request identification of regions in Marching Cubes algorithm.
        if(identifyRegions) {
            meshBuilder.createFaceProperty(DataBuffer::Uninitialized, SurfaceMeshFaces::RegionProperty);
        }

        // Invoke marching cubes algorithm.
        MarchingCubes mc(meshBuilder, gridShape[0], gridShape[1], gridShape[2], false, std::move(getFieldValue));
        mc.generateIsosurface(isolevel, progress);

        // Copy field values from voxel grid to surface mesh vertices.
        transferPropertiesFromGridToMesh(meshBuilder, auxiliaryProperties, *meshBuilder.mesh()->domain(), gridShape, gridType, progress);
        this_task::throwIfCanceled();

        // Adjust for non-periodic point-based grids.
        VoxelGrid::GridDimensions mcShape = gridShape;
        if(gridType == VoxelGrid::GridType::PointData) {
            if(!meshBuilder.hasPbc(0) && mcShape[0] >= 2) mcShape[0]--;
            if(!meshBuilder.hasPbc(1) && mcShape[1] >= 2) mcShape[1]--;
            if(!meshBuilder.hasPbc(2) && mcShape[2] >= 2) mcShape[2]--;
        }

        // Transform mesh vertices from orthogonal grid space to world space.
        const AffineTransformation tm = meshBuilder.domain().cellMatrix() * Matrix3(
            FloatType(1) / mcShape[0], 0, 0,
            0, FloatType(1) / mcShape[1], 0,
            0, 0, FloatType(1) / mcShape[2]) *
            AffineTransformation::translation(Vector3(gridType == VoxelGrid::GridType::PointData ? 0.0 : 0.5));
        meshBuilder.transformVertices(tm);

        // Map mesh region volumes from orthogonal grid space to world space.
        FloatType tmDeterminant = tm.determinant();
        BufferWriteAccess<FloatType, access_mode::read_write> regionVolumes = meshBuilder.mutableRegionProperty(SurfaceMeshRegions::VolumeProperty);
        for(SurfaceMesh::region_index region : meshBuilder.regionsRange()) {
            regionVolumes[region] *= tmDeterminant;
        }
        regionVolumes.reset();

        // Flip surface orientation if cell matrix is a mirror transformation.
        if(tmDeterminant < 0)
            meshBuilder.flipFaces();
        this_task::throwIfCanceled();

        if(!meshBuilder.connectOppositeHalfedges())
            throw Exception(tr("Something went wrong. Isosurface mesh is not closed."));
        this_task::throwIfCanceled();

        meshBuilder.smoothMesh(smoothingLevel, progress);
        this_task::throwIfCanceled();

        FloatType totalSurfaceArea;
        SurfaceMeshBuilder::AggregateVolumes aggregateVolumes;
        if(identifyRegions) {
            meshBuilder.setExternalRegionVolumeInfinityIfNonPeriodic();
            totalSurfaceArea = meshBuilder.computeSurfaceAreaWithRegions();
            aggregateVolumes = meshBuilder.computeAggregateVolumes();
        }
        else {
            totalSurfaceArea = meshBuilder.computeTotalSurfaceArea();
        }
        this_task::throwIfCanceled();

        // Determine min-max range of input field values.
        // Only used for informational purposes for the user.
        FloatType minValue =  FLOATTYPE_MAX;
        FloatType maxValue = -FLOATTYPE_MAX;
        for(FloatType v : data.componentRange(vectorComponent)) {
            if(v < minValue) minValue = v;
            if(v > maxValue) maxValue = v;
        }

        // Compute a histogram of the input field values.
        histogram->setElementCount(64);
        Property* histogramValues = histogram->createProperty(DataBuffer::Initialized, QStringLiteral("Count"), Property::Int64);
        FloatType binSize = (maxValue - minValue) / histogramValues->size();
        int histogramSizeMin1 = histogramValues->size() - 1;
        BufferWriteAccess<int64_t, access_mode::read_write> histogramAccess(histogramValues);
        for(const FloatType v : data.componentRange(vectorComponent)) {
            int binIndex = (v - minValue) / binSize;
            histogramAccess[std::max(0, std::min(binIndex, histogramSizeMin1))]++;
        }
        histogram->setY(std::move(histogramValues));
        histogram->setIntervalStart(minValue);
        histogram->setIntervalEnd(maxValue);

        // Release data that is no longer needed to reduce memory footprint.
        property.reset();
        auxiliaryProperties.clear();

        // Set the status message of the pipeline.
        QStringList statusMessage;
        statusMessage.append(tr("Field value range: [%1, %2]").arg(histogram->intervalStart()).arg(histogram->intervalEnd()));

        // Output total surface area.
        state.addAttribute(QStringLiteral("CreateIsosurface.surface_area"), QVariant::fromValue(totalSurfaceArea), createdByNode);

        if(identifyRegions) {
            const SimulationCell& simCell = *(state.expectObject<SimulationCell>());
            bool periodic = simCell.pbcX() && simCell.pbcY() && simCell.pbcZ();
            FloatType totalCellVolume = (periodic) ? simCell.volume3D() : std::numeric_limits<FloatType>::quiet_NaN();

            // Output more global attributes.
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.cell_volume"), QVariant::fromValue(totalCellVolume), createdByNode);
            state.addAttribute(
                QStringLiteral("ConstructSurfaceMesh.specific_surface_area"),
                QVariant::fromValue(totalCellVolume ? (totalSurfaceArea / totalCellVolume) : std::numeric_limits<FloatType>::quiet_NaN()), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.filled_volume"), QVariant::fromValue(aggregateVolumes.totalFilledVolume), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.filled_fraction"),
                            QVariant::fromValue(totalCellVolume ? (aggregateVolumes.totalFilledVolume / totalCellVolume)
                                                                : std::numeric_limits<FloatType>::quiet_NaN()), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.filled_region_count"),
                            QVariant::fromValue(aggregateVolumes.filledRegionCount), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.empty_volume"), QVariant::fromValue(aggregateVolumes.totalEmptyVolume), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.empty_fraction"),
                            QVariant::fromValue(totalCellVolume ? (aggregateVolumes.totalEmptyVolume / totalCellVolume)
                                                                : std::numeric_limits<FloatType>::quiet_NaN()), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.empty_region_count"),
                            QVariant::fromValue(aggregateVolumes.emptyRegionCount), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.void_volume"), QVariant::fromValue(aggregateVolumes.totalVoidVolume), createdByNode);
            state.addAttribute(QStringLiteral("ConstructSurfaceMesh.void_region_count"), QVariant::fromValue(aggregateVolumes.voidRegionCount), createdByNode);

            statusMessage.append(
                tr("Surface area: %1\n# filled regions (volume): %2 (%3)\n# empty regions (volume): %4 (%5)\n# void regions (volume): %6 (%7)")
                    .arg(totalSurfaceArea)
                    .arg(aggregateVolumes.filledRegionCount)
                    .arg(aggregateVolumes.totalFilledVolume)
                    .arg(aggregateVolumes.emptyRegionCount)
                    .arg(aggregateVolumes.totalEmptyVolume)
                    .arg(aggregateVolumes.voidRegionCount)
                    .arg(aggregateVolumes.totalVoidVolume));
        }
        else {
            statusMessage.append(tr("Surface area: %1").arg(totalSurfaceArea));
        }
        state.setStatus(statusMessage.join('\n'));

        return std::move(state);
    });
}

/******************************************************************************
* Transfers voxel grid properties to the vertices of a surfaces mesh.
******************************************************************************/
void CreateIsosurfaceModifier::transferPropertiesFromGridToMesh(SurfaceMeshBuilder& mesh, const std::vector<ConstPropertyPtr>& fieldProperties, const SimulationCell& gridDomain, VoxelGrid::GridDimensions gridShape, VoxelGrid::GridType gridType, TaskProgress& progress)
{
    OVITO_ASSERT(this_task::get());

    // Create destination properties for transferring voxel values to the surface vertices.
    std::vector<std::pair<RawBufferReadAccess, RawBufferAccess<access_mode::discard_write>>> propertyMapping;
    for(const ConstPropertyPtr& fieldProperty : fieldProperties) {
        Property* vertexProperty;
        if(fieldProperty->typeId() < Property::FirstSpecificProperty && SurfaceMeshVertices::OOClass().isValidStandardPropertyId(fieldProperty->typeId())) {
            // Input voxel property is also a standard property for mesh vertices.
            vertexProperty = mesh.createVertexProperty(DataBuffer::Initialized, static_cast<SurfaceMeshVertices::Type>(fieldProperty->typeId()));
            OVITO_ASSERT(vertexProperty->dataType() == fieldProperty->dataType());
            OVITO_ASSERT(vertexProperty->stride() == fieldProperty->stride());
        }
        else if(SurfaceMeshVertices::OOClass().standardPropertyTypeId(fieldProperty->name()) != 0) {
            // Input property name is that of a standard property for mesh vertices.
            // Must rename the property to avoid conflict, because user properties may not have a standard property name.
            // Always use floating point data type for vertex property, because field values get blended.
            QString newPropertyName = fieldProperty->name() + tr("_field");
            vertexProperty = mesh.createVertexProperty(DataBuffer::Initialized, newPropertyName, DataBuffer::FloatDefault, fieldProperty->componentCount(), fieldProperty->componentNames());
        }
        else {
            // Input property is a user property for mesh vertices.
            // Always use floating point data type for vertex property, because field values get blended.
            vertexProperty = mesh.createVertexProperty(DataBuffer::Initialized, fieldProperty->name(), DataBuffer::FloatDefault, fieldProperty->componentCount(), fieldProperty->componentNames());
        }
        propertyMapping.emplace_back(fieldProperty, std::move(vertexProperty));
    }

    // Transfer values of field properties to the created mesh vertices.
    if(!propertyMapping.empty()) {
        std::array<bool,3> pbcFlags = gridDomain.pbcFlagsCorrected();
        BufferReadAccess<Point3> vertexPositions = mesh.expectVertexProperty(SurfaceMeshVertices::PositionProperty);

        parallelFor(mesh.vertexCount(), 4096, progress, [&](size_t vertexIndex) {
            // Trilinear interpolation scheme.
            size_t cornerIndices[8];
            FloatType cornerWeights[8];
            OVITO_ASSERT(mesh.firstVertexEdge(vertexIndex) != SurfaceMesh::InvalidIndex);
            Point3 p = vertexPositions[vertexIndex];
            if(gridType == VoxelGrid::GridType::PointData)
                p += Vector3(FloatType(0.5));
            Vector3 x0, x1;
            Vector3I x0_vc, x1_vc;
            for(size_t dim = 0; dim < 3; dim++) {
                OVITO_ASSERT(p[dim] >= -0.5 - Ovito::epsilon);
                OVITO_ASSERT(p[dim] <= (FloatType)gridShape[dim] + Ovito::epsilon + 0.5);
                FloatType fl = std::floor(p[dim]);
                x1[dim] = p[dim] - fl;
                x0[dim] = FloatType(1) - x1[dim];
                if(!pbcFlags[dim]) {
                    x0_vc[dim] = qBound(0, (int)fl, (int)gridShape[dim] - 1);
                    x1_vc[dim] = qBound(0, (int)fl + 1, (int)gridShape[dim] - 1);
                }
                else {
                    x0_vc[dim] = SimulationCell::modulo((int)fl, gridShape[dim]);
                    x1_vc[dim] = SimulationCell::modulo((int)fl + 1, gridShape[dim]);
                }
            }
            cornerWeights[0] = x0.x() * x0.y() * x0.z();
            cornerWeights[1] = x1.x() * x0.y() * x0.z();
            cornerWeights[2] = x0.x() * x1.y() * x0.z();
            cornerWeights[3] = x0.x() * x0.y() * x1.z();
            cornerWeights[4] = x1.x() * x0.y() * x1.z();
            cornerWeights[5] = x0.x() * x1.y() * x1.z();
            cornerWeights[6] = x1.x() * x1.y() * x0.z();
            cornerWeights[7] = x1.x() * x1.y() * x1.z();
            cornerIndices[0] = x0_vc.x() + x0_vc.y() * gridShape[0] + x0_vc.z() * gridShape[0] * gridShape[1];
            cornerIndices[1] = x1_vc.x() + x0_vc.y() * gridShape[0] + x0_vc.z() * gridShape[0] * gridShape[1];
            cornerIndices[2] = x0_vc.x() + x1_vc.y() * gridShape[0] + x0_vc.z() * gridShape[0] * gridShape[1];
            cornerIndices[3] = x0_vc.x() + x0_vc.y() * gridShape[0] + x1_vc.z() * gridShape[0] * gridShape[1];
            cornerIndices[4] = x1_vc.x() + x0_vc.y() * gridShape[0] + x1_vc.z() * gridShape[0] * gridShape[1];
            cornerIndices[5] = x0_vc.x() + x1_vc.y() * gridShape[0] + x1_vc.z() * gridShape[0] * gridShape[1];
            cornerIndices[6] = x1_vc.x() + x1_vc.y() * gridShape[0] + x0_vc.z() * gridShape[0] * gridShape[1];
            cornerIndices[7] = x1_vc.x() + x1_vc.y() * gridShape[0] + x1_vc.z() * gridShape[0] * gridShape[1];
            for(auto& prop : propertyMapping) {
                for(size_t component = 0; component < prop.first.componentCount(); component++) {
                    FloatType v = 0;
                    for(size_t i = 0; i < 8; i++)
                        v += cornerWeights[i] * prop.first.get<FloatType>(cornerIndices[i], component);
                    prop.second.set<FloatType>(vertexIndex, component, v);
                }
            }
        });
    }
}

}   // End of namespace
