2019-02-21 22:48:15 +00:00
|
|
|
#include <QMatrix4x4>
|
2019-08-13 13:01:10 +00:00
|
|
|
#include <unordered_set>
|
2019-12-14 13:28:14 +00:00
|
|
|
#include <QDebug>
|
|
|
|
#include "strokemeshbuilder.h"
|
|
|
|
#include "meshstitcher.h"
|
|
|
|
#include "util.h"
|
2020-04-07 23:15:20 +00:00
|
|
|
#include "boxmesh.h"
|
2019-02-18 12:57:18 +00:00
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
size_t StrokeMeshBuilder::Node::nextOrNeighborOtherThan(size_t neighborIndex) const
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
if (this->next != neighborIndex && this->next != this->index)
|
|
|
|
return this->next;
|
|
|
|
for (const auto &it: this->neighbors) {
|
|
|
|
if (it != neighborIndex)
|
|
|
|
return it;
|
|
|
|
}
|
|
|
|
return this->index;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::enableBaseNormalOnX(bool enabled)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_baseNormalOnX = enabled;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::enableBaseNormalOnY(bool enabled)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_baseNormalOnY = enabled;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::enableBaseNormalOnZ(bool enabled)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_baseNormalOnZ = enabled;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::enableBaseNormalAverage(bool enabled)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_baseNormalAverageEnabled = enabled;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::setDeformThickness(float thickness)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_deformThickness = thickness;
|
2019-08-13 13:01:10 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::setDeformWidth(float width)
|
2019-08-13 13:01:10 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_deformWidth = width;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::setDeformMapImage(const QImage *image)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_deformMapImage = image;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::setHollowThickness(float hollowThickness)
|
2019-07-07 10:55:42 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_hollowThickness = hollowThickness;
|
2019-07-07 10:55:42 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::setDeformMapScale(float scale)
|
2019-07-27 04:42:07 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
m_deformMapScale = scale;
|
2019-07-27 04:42:07 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::setNodeOriginInfo(size_t nodeIndex, int nearOriginNodeIndex, int farOriginNodeIndex)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
|
|
|
auto &node = m_nodes[nodeIndex];
|
2020-04-07 23:15:20 +00:00
|
|
|
node.nearOriginNodeIndex = nearOriginNodeIndex;
|
|
|
|
node.farOriginNodeIndex = farOriginNodeIndex;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
const std::vector<QVector3D> &StrokeMeshBuilder::generatedVertices()
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_generatedVertices;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
const std::vector<std::vector<size_t>> &StrokeMeshBuilder::generatedFaces()
|
2019-02-22 14:33:04 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_generatedFaces;
|
2019-02-22 14:33:04 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
const std::vector<size_t> &StrokeMeshBuilder::generatedVerticesSourceNodeIndices()
|
2019-02-22 14:33:04 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_generatedVerticesSourceNodeIndices;
|
2019-02-22 14:33:04 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
const std::vector<StrokeMeshBuilder::Node> &StrokeMeshBuilder::nodes() const
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_nodes;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
const std::vector<size_t> &StrokeMeshBuilder::nodeIndices() const
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_nodeIndices;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
const QVector3D &StrokeMeshBuilder::nodeTraverseDirection(size_t nodeIndex) const
|
2019-07-22 09:40:37 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_nodes[nodeIndex].traverseDirection;
|
2019-07-22 09:40:37 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
const QVector3D &StrokeMeshBuilder::nodeBaseNormal(size_t nodeIndex) const
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_nodes[nodeIndex].baseNormal;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
size_t StrokeMeshBuilder::nodeTraverseOrder(size_t nodeIndex) const
|
2019-05-20 13:38:01 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return m_nodes[nodeIndex].traverseOrder;
|
2019-05-20 13:38:01 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
size_t StrokeMeshBuilder::addNode(const QVector3D &position, float radius, const std::vector<QVector2D> &cutTemplate, float cutRotation)
|
2019-05-20 13:38:01 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
size_t nodeIndex = m_nodes.size();
|
|
|
|
|
|
|
|
Node node;
|
|
|
|
node.position = position;
|
|
|
|
node.radius = radius;
|
|
|
|
node.cutTemplate = cutTemplate;
|
|
|
|
node.cutRotation = cutRotation;
|
|
|
|
node.next = nodeIndex;
|
|
|
|
node.index = nodeIndex;
|
|
|
|
|
|
|
|
m_nodes.push_back(node);
|
|
|
|
|
|
|
|
return nodeIndex;
|
2019-05-20 13:38:01 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::addEdge(size_t firstNodeIndex,
|
|
|
|
size_t secondNodeIndex)
|
2019-05-20 13:38:01 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
auto &fromNode = m_nodes[firstNodeIndex];
|
|
|
|
fromNode.next = secondNodeIndex;
|
|
|
|
fromNode.neighbors.push_back(secondNodeIndex);
|
|
|
|
|
|
|
|
auto &toNode = m_nodes[secondNodeIndex];
|
|
|
|
toNode.neighbors.push_back(firstNodeIndex);
|
2019-05-20 13:38:01 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
QVector3D StrokeMeshBuilder::calculateBaseNormalFromTraverseDirection(const QVector3D &traverseDirection)
|
2019-06-24 22:44:04 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
const std::vector<QVector3D> axisList = {
|
|
|
|
QVector3D {1, 0, 0},
|
|
|
|
QVector3D {0, 1, 0},
|
|
|
|
QVector3D {0, 0, 1},
|
|
|
|
};
|
|
|
|
float maxDot = -1;
|
|
|
|
size_t nearAxisIndex = 0;
|
|
|
|
bool reversed = false;
|
|
|
|
for (size_t i = 0; i < axisList.size(); ++i) {
|
|
|
|
const auto axis = axisList[i];
|
|
|
|
auto dot = QVector3D::dotProduct(axis, traverseDirection);
|
|
|
|
auto positiveDot = abs(dot);
|
|
|
|
if (positiveDot >= maxDot) {
|
|
|
|
reversed = dot < 0;
|
|
|
|
maxDot = positiveDot;
|
|
|
|
nearAxisIndex = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// axisList[nearAxisIndex] align with the traverse direction,
|
|
|
|
// So we pick the next axis to do cross product with traverse direction
|
|
|
|
const auto& choosenAxis = axisList[(nearAxisIndex + 1) % 3];
|
|
|
|
auto baseNormal = QVector3D::crossProduct(traverseDirection, choosenAxis).normalized();
|
|
|
|
return reversed ? -baseNormal : baseNormal;
|
2019-06-24 22:44:04 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<QVector3D> StrokeMeshBuilder::makeCut(const QVector3D &cutCenter,
|
|
|
|
float radius,
|
|
|
|
const std::vector<QVector2D> &cutTemplate,
|
|
|
|
const QVector3D &cutNormal,
|
|
|
|
const QVector3D &baseNormal)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<QVector3D> resultCut;
|
|
|
|
QVector3D u = QVector3D::crossProduct(cutNormal, baseNormal).normalized();
|
|
|
|
QVector3D v = QVector3D::crossProduct(u, cutNormal).normalized();
|
|
|
|
auto uFactor = u * radius;
|
|
|
|
auto vFactor = v * radius;
|
|
|
|
for (const auto &t: cutTemplate) {
|
|
|
|
resultCut.push_back(cutCenter + (uFactor * t.x() + vFactor * t.y()));
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
return resultCut;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2019-12-14 13:28:14 +00:00
|
|
|
void StrokeMeshBuilder::insertCutVertices(const std::vector<QVector3D> &cut,
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<size_t> *vertices,
|
2019-08-03 10:21:27 +00:00
|
|
|
size_t nodeIndex,
|
2020-04-07 23:15:20 +00:00
|
|
|
const QVector3D &cutNormal)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2019-08-03 10:21:27 +00:00
|
|
|
size_t indexInCut = 0;
|
2019-02-18 12:57:18 +00:00
|
|
|
for (const auto &position: cut) {
|
|
|
|
size_t vertexIndex = m_generatedVertices.size();
|
|
|
|
m_generatedVertices.push_back(position);
|
|
|
|
m_generatedVerticesSourceNodeIndices.push_back(nodeIndex);
|
2020-04-07 23:15:20 +00:00
|
|
|
m_generatedVerticesCutDirects.push_back(cutNormal);
|
2019-08-03 10:21:27 +00:00
|
|
|
|
|
|
|
GeneratedVertexInfo info;
|
2020-04-07 23:15:20 +00:00
|
|
|
info.orderInCut = indexInCut;
|
2019-08-03 10:21:27 +00:00
|
|
|
info.cutSize = cut.size();
|
|
|
|
m_generatedVerticesInfos.push_back(info);
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
vertices->push_back(vertexIndex);
|
2019-08-03 10:21:27 +00:00
|
|
|
|
|
|
|
++indexInCut;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<size_t> StrokeMeshBuilder::edgeloopFlipped(const std::vector<size_t> &edgeLoop)
|
2019-07-02 21:49:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
auto reversed = edgeLoop;
|
|
|
|
std::reverse(reversed.begin(), reversed.end());
|
|
|
|
std::rotate(reversed.rbegin(), reversed.rbegin() + 1, reversed.rend());
|
|
|
|
return reversed;
|
2019-07-02 21:49:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::buildMesh()
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
if (1 == m_nodes.size()) {
|
|
|
|
const Node &node = m_nodes[0];
|
|
|
|
int subdivideTimes = (int)(node.cutTemplate.size() / 4) - 1;
|
|
|
|
if (subdivideTimes < 0)
|
|
|
|
subdivideTimes = 0;
|
|
|
|
boxmesh(node.position, node.radius, subdivideTimes, m_generatedVertices, m_generatedFaces);
|
|
|
|
m_generatedVerticesSourceNodeIndices.resize(m_generatedVertices.size(), 0);
|
|
|
|
m_generatedVerticesCutDirects.resize(m_generatedVertices.size(), node.traverseDirection);
|
|
|
|
return;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<std::vector<size_t>> cuts;
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
if (!qFuzzyIsNull(node.cutRotation)) {
|
|
|
|
float degree = node.cutRotation * 180;
|
|
|
|
QMatrix4x4 rotation;
|
|
|
|
rotation.rotate(degree, node.traverseDirection);
|
|
|
|
node.baseNormal = rotation * node.baseNormal;
|
|
|
|
}
|
|
|
|
auto cutVertices = makeCut(node.position, node.radius, node.cutTemplate,
|
|
|
|
node.traverseDirection, node.baseNormal);
|
|
|
|
std::vector<size_t> cut;
|
|
|
|
insertCutVertices(cutVertices, &cut, node.index, node.traverseDirection);
|
|
|
|
cuts.push_back(cut);
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
// Stich cuts
|
|
|
|
for (size_t i = m_isRing ? 0 : 1; i < m_nodeIndices.size(); ++i) {
|
|
|
|
size_t h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size();
|
|
|
|
const auto &nodeH = m_nodes[m_nodeIndices[h]];
|
|
|
|
const auto &nodeI = m_nodes[m_nodeIndices[i]];
|
|
|
|
const auto &cutH = cuts[h];
|
|
|
|
auto reversedCutI = edgeloopFlipped(cuts[i]);
|
|
|
|
std::vector<std::pair<std::vector<size_t>, QVector3D>> edgeLoops = {
|
|
|
|
{cutH, -nodeH.traverseDirection},
|
|
|
|
{reversedCutI, nodeI.traverseDirection},
|
|
|
|
};
|
|
|
|
MeshStitcher stitcher;
|
|
|
|
stitcher.setVertices(&m_generatedVertices);
|
|
|
|
stitcher.stitch(edgeLoops);
|
|
|
|
for (const auto &face: stitcher.newlyGeneratedFaces()) {
|
|
|
|
m_generatedFaces.push_back(face);
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
|
|
|
|
// Fill endpoints
|
|
|
|
if (!m_isRing) {
|
|
|
|
if (cuts.size() < 2)
|
|
|
|
return;
|
|
|
|
if (!qFuzzyIsNull(m_hollowThickness)) {
|
|
|
|
// Generate mesh for hollow
|
|
|
|
size_t startVertexIndex = m_generatedVertices.size();
|
|
|
|
for (size_t i = 0; i < startVertexIndex; ++i) {
|
|
|
|
const auto &position = m_generatedVertices[i];
|
|
|
|
const auto &node = m_nodes[m_generatedVerticesSourceNodeIndices[i]];
|
|
|
|
auto ray = position - node.position;
|
|
|
|
|
|
|
|
auto newPosition = position - ray * m_hollowThickness;
|
|
|
|
m_generatedVertices.push_back(newPosition);
|
|
|
|
m_generatedVerticesCutDirects.push_back(m_generatedVerticesCutDirects[i]);
|
|
|
|
m_generatedVerticesSourceNodeIndices.push_back(m_generatedVerticesSourceNodeIndices[i]);
|
|
|
|
m_generatedVerticesInfos.push_back(m_generatedVerticesInfos[i]);
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
|
|
|
|
size_t oldFaceNum = m_generatedFaces.size();
|
|
|
|
for (size_t i = 0; i < oldFaceNum; ++i) {
|
|
|
|
auto newFace = m_generatedFaces[i];
|
|
|
|
std::reverse(newFace.begin(), newFace.end());
|
|
|
|
for (auto &it: newFace)
|
|
|
|
it += startVertexIndex;
|
|
|
|
m_generatedFaces.push_back(newFace);
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
|
|
|
|
std::vector<std::vector<size_t>> revisedCuts = {cuts[0],
|
|
|
|
edgeloopFlipped(cuts[cuts.size() - 1])};
|
|
|
|
for (const auto &cut: revisedCuts) {
|
|
|
|
for (size_t i = 0; i < cut.size(); ++i) {
|
|
|
|
size_t j = (i + 1) % cut.size();
|
|
|
|
std::vector<size_t> quad;
|
|
|
|
quad.push_back(cut[i]);
|
|
|
|
quad.push_back(cut[j]);
|
|
|
|
quad.push_back(startVertexIndex + cut[j]);
|
|
|
|
quad.push_back(startVertexIndex + cut[i]);
|
|
|
|
m_generatedFaces.push_back(quad);
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
} else {
|
|
|
|
m_generatedFaces.push_back(cuts[0]);
|
|
|
|
m_generatedFaces.push_back(edgeloopFlipped(cuts[cuts.size() - 1]));
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StrokeMeshBuilder::reviseTraverseDirections()
|
|
|
|
{
|
|
|
|
std::vector<std::pair<size_t, QVector3D>> revised;
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
const auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
if (-1 != node.nearOriginNodeIndex && -1 != node.farOriginNodeIndex) {
|
|
|
|
const auto &nearOriginNode = m_nodes[node.nearOriginNodeIndex];
|
|
|
|
const auto &farOriginNode = m_nodes[node.farOriginNodeIndex];
|
|
|
|
float nearDistance = node.position.distanceToPoint(nearOriginNode.position);
|
|
|
|
float farDistance = node.position.distanceToPoint(farOriginNode.position);
|
|
|
|
float totalDistance = nearDistance + farDistance;
|
|
|
|
float distanceFactor = nearDistance / totalDistance;
|
|
|
|
const QVector3D *revisedNearCutNormal = nullptr;
|
|
|
|
const QVector3D *revisedFarCutNormal = nullptr;
|
|
|
|
if (distanceFactor <= 0.5) {
|
|
|
|
revisedNearCutNormal = &nearOriginNode.traverseDirection;
|
|
|
|
revisedFarCutNormal = &node.traverseDirection;
|
|
|
|
} else {
|
|
|
|
distanceFactor = (1.0 - distanceFactor);
|
|
|
|
revisedNearCutNormal = &farOriginNode.traverseDirection;
|
|
|
|
revisedFarCutNormal = &node.traverseDirection;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
distanceFactor *= 1.75;
|
|
|
|
QVector3D newTraverseDirection;
|
|
|
|
if (QVector3D::dotProduct(*revisedNearCutNormal, *revisedFarCutNormal) <= 0)
|
|
|
|
newTraverseDirection = (*revisedNearCutNormal * (1.0 - distanceFactor) - *revisedFarCutNormal * distanceFactor).normalized();
|
|
|
|
else
|
|
|
|
newTraverseDirection = (*revisedNearCutNormal * (1.0 - distanceFactor) + *revisedFarCutNormal * distanceFactor).normalized();
|
|
|
|
if (QVector3D::dotProduct(newTraverseDirection, node.traverseDirection) <= 0)
|
|
|
|
newTraverseDirection = -newTraverseDirection;
|
|
|
|
revised.push_back({node.index, newTraverseDirection});
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
for (const auto &it: revised)
|
|
|
|
m_nodes[it.first].traverseDirection = it.second;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::localAverageBaseNormals()
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<std::pair<size_t, QVector3D>> averaged;
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
size_t h, j;
|
|
|
|
if (m_isRing) {
|
|
|
|
h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size();
|
|
|
|
j = (j + 1) % m_nodeIndices.size();
|
|
|
|
} else {
|
|
|
|
h = i > 0 ? i - 1 : i;
|
|
|
|
j = i + 1 < m_nodeIndices.size() ? i + 1 : i;
|
|
|
|
}
|
|
|
|
const auto &nodeH = m_nodes[m_nodeIndices[h]];
|
|
|
|
const auto &nodeI = m_nodes[m_nodeIndices[i]];
|
|
|
|
const auto &nodeJ = m_nodes[m_nodeIndices[j]];
|
|
|
|
averaged.push_back({
|
|
|
|
nodeI.index,
|
|
|
|
(nodeH.baseNormal + nodeI.baseNormal + nodeJ.baseNormal).normalized()
|
|
|
|
});
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
for (const auto &it: averaged)
|
|
|
|
m_nodes[it.first].baseNormal = it.second;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2019-12-14 13:28:14 +00:00
|
|
|
void StrokeMeshBuilder::unifyBaseNormals()
|
2019-02-26 13:24:58 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
for (size_t i = 1; i < m_nodeIndices.size(); ++i) {
|
|
|
|
size_t h = m_nodeIndices[i - 1];
|
|
|
|
const auto &nodeH = m_nodes[m_nodeIndices[h]];
|
|
|
|
auto &nodeI = m_nodes[m_nodeIndices[i]];
|
|
|
|
if (QVector3D::dotProduct(nodeI.baseNormal, nodeH.baseNormal) < 0)
|
|
|
|
nodeI.baseNormal = -nodeI.baseNormal;
|
2019-02-26 13:24:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
void StrokeMeshBuilder::reviseNodeBaseNormal(Node &node)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
QVector3D orientedBaseNormal = QVector3D::dotProduct(node.traverseDirection, node.baseNormal) >= 0 ?
|
|
|
|
node.baseNormal : -node.baseNormal;
|
|
|
|
if (orientedBaseNormal.isNull()) {
|
|
|
|
orientedBaseNormal = calculateBaseNormalFromTraverseDirection(node.traverseDirection);
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
node.baseNormal = orientedBaseNormal;
|
2019-02-26 13:24:58 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
bool StrokeMeshBuilder::prepare()
|
2019-02-26 13:24:58 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
if (m_nodes.empty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (1 == m_nodes.size()) {
|
|
|
|
auto &node = m_nodes[0];
|
|
|
|
node.traverseOrder = 0;
|
|
|
|
node.traverseDirection = QVector3D(0, 1, 0);
|
|
|
|
node.baseNormal = QVector3D(0, 0, 1);
|
|
|
|
return true;
|
2019-07-02 21:49:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
|
|
|
|
m_nodeIndices = sortedNodeIndices(&m_isRing);
|
|
|
|
|
|
|
|
if (m_nodeIndices.empty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
std::vector<QVector3D> edgeDirections;
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
m_nodes[m_nodeIndices[i]].traverseOrder = i;
|
|
|
|
size_t j;
|
|
|
|
if (m_isRing) {
|
|
|
|
j = (i + 1) % m_nodeIndices.size();
|
|
|
|
} else {
|
|
|
|
j = i + 1 < m_nodeIndices.size() ? i + 1 : i;
|
|
|
|
}
|
|
|
|
edgeDirections.push_back((m_nodes[m_nodeIndices[j]].position - m_nodes[m_nodeIndices[i]].position).normalized());
|
2019-02-21 22:48:15 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
size_t h;
|
|
|
|
if (m_isRing) {
|
|
|
|
h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size();
|
|
|
|
} else {
|
|
|
|
h = i > 0 ? i - 1 : i;
|
2019-02-21 22:48:15 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
m_nodes[m_nodeIndices[i]].traverseDirection = (edgeDirections[h] + edgeDirections[i]).normalized();
|
2019-02-21 22:48:15 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
reviseTraverseDirections();
|
|
|
|
|
|
|
|
// Base plane constraints
|
|
|
|
if (!m_baseNormalOnX || !m_baseNormalOnY || !m_baseNormalOnZ) {
|
|
|
|
for (auto &it: edgeDirections) {
|
|
|
|
if (!m_baseNormalOnX)
|
|
|
|
it.setX(0);
|
|
|
|
if (!m_baseNormalOnY)
|
|
|
|
it.setY(0);
|
|
|
|
if (!m_baseNormalOnZ)
|
|
|
|
it.setZ(0);
|
|
|
|
}
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<size_t> validBaseNormalPosArray;
|
|
|
|
for (size_t i = m_isRing ? 0 : 1; i < m_nodeIndices.size(); ++i) {
|
|
|
|
size_t h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size();
|
|
|
|
// >15 degrees && < 165 degrees
|
|
|
|
if (abs(QVector3D::dotProduct(edgeDirections[h], edgeDirections[i])) < 0.966) {
|
|
|
|
auto baseNormal = QVector3D::crossProduct(edgeDirections[h], edgeDirections[i]);
|
|
|
|
if (!baseNormal.isNull()) {
|
|
|
|
if (!validBaseNormalPosArray.empty()) {
|
|
|
|
const auto &lastNode = m_nodes[m_nodeIndices[validBaseNormalPosArray[validBaseNormalPosArray.size() - 1]]];
|
|
|
|
if (QVector3D::dotProduct(lastNode.baseNormal, baseNormal) < 0)
|
|
|
|
baseNormal = -baseNormal;
|
|
|
|
}
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
node.baseNormal = baseNormal;
|
|
|
|
validBaseNormalPosArray.push_back(i);
|
|
|
|
continue;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
if (validBaseNormalPosArray.empty()) {
|
|
|
|
QVector3D baseNormal;
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
const auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
baseNormal += calculateBaseNormalFromTraverseDirection(node.traverseDirection);
|
|
|
|
}
|
|
|
|
baseNormal.normalize();
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
node.baseNormal = baseNormal;
|
|
|
|
}
|
|
|
|
} else if (1 == validBaseNormalPosArray.size()) {
|
|
|
|
auto baseNormal = m_nodes[m_nodeIndices[validBaseNormalPosArray[0]]].baseNormal;
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
node.baseNormal = baseNormal;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!m_isRing) {
|
|
|
|
auto prePos = validBaseNormalPosArray[0];
|
|
|
|
const auto &preNode = m_nodes[m_nodeIndices[prePos]];
|
|
|
|
auto preBaseNormal = preNode.baseNormal;
|
|
|
|
auto afterPos = validBaseNormalPosArray[validBaseNormalPosArray.size() - 1];
|
|
|
|
const auto &afterNode = m_nodes[m_nodeIndices[afterPos]];
|
|
|
|
auto afterBaseNormal = afterNode.baseNormal;
|
|
|
|
for (size_t i = 0; i < prePos; ++i) {
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
node.baseNormal = preBaseNormal;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
for (size_t i = afterPos + 1; i < m_nodeIndices.size(); ++i) {
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
node.baseNormal = afterBaseNormal;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-07 23:15:20 +00:00
|
|
|
auto updateInBetweenBaseNormal = [this](const Node &nodeU, const Node &nodeV, Node &updateNode) {
|
|
|
|
float distanceU = updateNode.position.distanceToPoint(nodeU.position);
|
|
|
|
float distanceV = updateNode.position.distanceToPoint(nodeV.position);
|
|
|
|
float totalDistance = distanceU + distanceV;
|
|
|
|
float factorU = 1.0 - distanceU / totalDistance;
|
|
|
|
float factorV = 1.0 - factorU;
|
|
|
|
auto baseNormal = (nodeU.baseNormal * factorU + nodeV.baseNormal * factorV).normalized();
|
|
|
|
updateNode.baseNormal = baseNormal;
|
|
|
|
};
|
|
|
|
for (size_t k = m_isRing ? 0 : 1; k < validBaseNormalPosArray.size(); ++k) {
|
|
|
|
size_t u = validBaseNormalPosArray[(k + validBaseNormalPosArray.size() - 1) % validBaseNormalPosArray.size()];
|
|
|
|
size_t v = validBaseNormalPosArray[k];
|
|
|
|
const auto &nodeU = m_nodes[m_nodeIndices[u]];
|
|
|
|
const auto &nodeV = m_nodes[m_nodeIndices[v]];
|
|
|
|
for (size_t i = (u + 1) % m_nodeIndices.size();
|
|
|
|
i != v;
|
|
|
|
i = (i + 1) % m_nodeIndices.size()) {
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
updateInBetweenBaseNormal(nodeU, nodeV, node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (m_baseNormalAverageEnabled) {
|
|
|
|
QVector3D baseNormal;
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
const auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
baseNormal += node.baseNormal;
|
|
|
|
}
|
|
|
|
baseNormal.normalize();
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
auto &node = m_nodes[m_nodeIndices[i]];
|
|
|
|
node.baseNormal = baseNormal;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
unifyBaseNormals();
|
|
|
|
localAverageBaseNormals();
|
|
|
|
for (size_t i = 0; i < m_nodeIndices.size(); ++i) {
|
|
|
|
reviseNodeBaseNormal(m_nodes[m_nodeIndices[i]]);
|
|
|
|
}
|
|
|
|
unifyBaseNormals();
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
return true;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<size_t> StrokeMeshBuilder::sortedNodeIndices(bool *isRing)
|
2019-08-03 10:21:27 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
std::vector<size_t> nodeIndices;
|
|
|
|
|
|
|
|
size_t startingNodeIndex = 0;
|
|
|
|
if (!calculateStartingNodeIndex(&startingNodeIndex, isRing))
|
|
|
|
return nodeIndices;
|
|
|
|
|
|
|
|
size_t fromNodeIndex = startingNodeIndex;
|
|
|
|
std::unordered_set<size_t> visited;
|
|
|
|
auto nodeIndex = fromNodeIndex;
|
|
|
|
while (true) {
|
|
|
|
if (visited.find(nodeIndex) != visited.end())
|
|
|
|
break;
|
|
|
|
visited.insert(nodeIndex);
|
|
|
|
nodeIndices.push_back(nodeIndex);
|
|
|
|
const auto &node = m_nodes[nodeIndex];
|
|
|
|
size_t neighborIndex = node.nextOrNeighborOtherThan(fromNodeIndex);
|
|
|
|
if (neighborIndex == nodeIndex)
|
|
|
|
break;
|
|
|
|
fromNodeIndex = nodeIndex;
|
|
|
|
nodeIndex = neighborIndex;
|
|
|
|
};
|
|
|
|
|
|
|
|
return nodeIndices;
|
2019-08-03 10:21:27 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
bool StrokeMeshBuilder::calculateStartingNodeIndex(size_t *startingNodeIndex,
|
|
|
|
bool *isRing)
|
2019-08-18 12:02:39 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
if (m_nodes.empty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (1 == m_nodes.size()) {
|
|
|
|
*startingNodeIndex = 0;
|
|
|
|
*isRing = false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto findNearestNodeWithWorldCenter = [&](const std::vector<size_t> &nodeIndices) {
|
|
|
|
std::vector<std::pair<size_t, float>> dist2Array(m_nodes.size());
|
|
|
|
for (const auto &i: nodeIndices) {
|
|
|
|
dist2Array.push_back({i, (float)m_nodes[i].position.lengthSquared()});
|
|
|
|
}
|
|
|
|
return std::min_element(dist2Array.begin(), dist2Array.end(),
|
|
|
|
[](const std::pair<size_t, float> &first,
|
|
|
|
const std::pair<size_t, float> &second) {
|
|
|
|
return first.second < second.second;
|
|
|
|
})->first;
|
|
|
|
};
|
|
|
|
|
|
|
|
auto findEndpointNodeIndices = [&]() {
|
|
|
|
std::vector<size_t> endpointIndices;
|
|
|
|
for (const auto &it: m_nodes) {
|
|
|
|
if (1 == it.neighbors.size())
|
|
|
|
endpointIndices.push_back(it.index);
|
|
|
|
}
|
|
|
|
return endpointIndices;
|
|
|
|
};
|
|
|
|
|
|
|
|
auto endpointIndices = findEndpointNodeIndices();
|
|
|
|
if (2 != endpointIndices.size()) {
|
|
|
|
// Invalid endpoint count, there must be a ring, choose the node which is nearest with world center
|
|
|
|
std::vector<size_t> nodeIndices(m_nodes.size());
|
|
|
|
for (size_t i = 0; i < m_nodes.size(); ++i) {
|
|
|
|
if (2 != m_nodes[i].neighbors.size())
|
|
|
|
return false;
|
|
|
|
nodeIndices[i] = i;
|
|
|
|
}
|
|
|
|
*startingNodeIndex = findNearestNodeWithWorldCenter(nodeIndices);
|
|
|
|
*isRing = true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto countAlignedDirections = [&](size_t nodeIndex) {
|
|
|
|
size_t alignedCount = 0;
|
|
|
|
size_t fromNodeIndex = nodeIndex;
|
|
|
|
std::unordered_set<size_t> visited;
|
|
|
|
while (true) {
|
|
|
|
if (visited.find(nodeIndex) != visited.end())
|
|
|
|
break;
|
|
|
|
visited.insert(nodeIndex);
|
|
|
|
const auto &node = m_nodes[nodeIndex];
|
|
|
|
size_t neighborIndex = node.nextOrNeighborOtherThan(fromNodeIndex);
|
|
|
|
if (neighborIndex == nodeIndex)
|
|
|
|
break;
|
|
|
|
if (node.next == neighborIndex)
|
|
|
|
++alignedCount;
|
|
|
|
fromNodeIndex = nodeIndex;
|
|
|
|
nodeIndex = neighborIndex;
|
|
|
|
};
|
|
|
|
return alignedCount;
|
|
|
|
};
|
|
|
|
|
|
|
|
auto chooseStartingEndpointByAlignedDirections = [&](const std::vector<size_t> &endpointIndices) {
|
|
|
|
std::vector<std::pair<size_t, size_t>> alignedDirections(endpointIndices.size());
|
|
|
|
for (size_t i = 0; i < endpointIndices.size(); ++i) {
|
|
|
|
auto nodeIndex = endpointIndices[i];
|
|
|
|
alignedDirections[i] = {nodeIndex, countAlignedDirections(nodeIndex)};
|
|
|
|
}
|
|
|
|
std::sort(alignedDirections.begin(), alignedDirections.end(), [](const std::pair<size_t, size_t> &first,
|
|
|
|
const std::pair<size_t, size_t> &second) {
|
|
|
|
return first.second > second.second;
|
|
|
|
});
|
|
|
|
if (alignedDirections[0].second > alignedDirections[1].second)
|
|
|
|
return alignedDirections[0].first;
|
|
|
|
std::vector<size_t> nodeIndices = {alignedDirections[0].first, alignedDirections[1].first};
|
|
|
|
return findNearestNodeWithWorldCenter(nodeIndices);
|
|
|
|
};
|
2019-08-18 12:02:39 +00:00
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
*startingNodeIndex = chooseStartingEndpointByAlignedDirections(endpointIndices);
|
|
|
|
*isRing = false;
|
|
|
|
return true;
|
2019-08-03 10:21:27 +00:00
|
|
|
}
|
|
|
|
|
2019-12-14 13:28:14 +00:00
|
|
|
QVector3D StrokeMeshBuilder::calculateDeformPosition(const QVector3D &vertexPosition, const QVector3D &ray, const QVector3D &deformNormal, float deformFactor)
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
|
|
|
QVector3D revisedNormal = QVector3D::dotProduct(ray, deformNormal) < 0.0 ? -deformNormal : deformNormal;
|
|
|
|
QVector3D projectRayOnRevisedNormal = revisedNormal * (QVector3D::dotProduct(ray, revisedNormal) / revisedNormal.lengthSquared());
|
|
|
|
auto scaledProjct = projectRayOnRevisedNormal * deformFactor;
|
|
|
|
return vertexPosition + (scaledProjct - projectRayOnRevisedNormal);
|
|
|
|
}
|
|
|
|
|
2019-12-14 13:28:14 +00:00
|
|
|
void StrokeMeshBuilder::applyDeform()
|
2019-02-18 12:57:18 +00:00
|
|
|
{
|
|
|
|
for (size_t i = 0; i < m_generatedVertices.size(); ++i) {
|
|
|
|
auto &position = m_generatedVertices[i];
|
|
|
|
const auto &node = m_nodes[m_generatedVerticesSourceNodeIndices[i]];
|
|
|
|
const auto &cutDirect = m_generatedVerticesCutDirects[i];
|
|
|
|
auto ray = position - node.position;
|
2019-08-03 10:21:27 +00:00
|
|
|
if (nullptr != m_deformMapImage) {
|
2019-12-14 13:28:14 +00:00
|
|
|
float degrees = angleInRangle360BetweenTwoVectors(node.baseNormal, ray.normalized(), node.traverseDirection);
|
2020-04-07 23:15:20 +00:00
|
|
|
int x = (int)node.traverseOrder * m_deformMapImage->width() / m_nodes.size();
|
2019-08-17 10:13:11 +00:00
|
|
|
int y = degrees * m_deformMapImage->height() / 360.0;
|
2019-08-18 01:13:48 +00:00
|
|
|
if (y >= m_deformMapImage->height())
|
|
|
|
y = m_deformMapImage->height() - 1;
|
2019-08-03 10:21:27 +00:00
|
|
|
float gray = (float)(qGray(m_deformMapImage->pixelColor(x, y).rgb()) - 127) / 127;
|
|
|
|
position += m_deformMapScale * gray * ray;
|
|
|
|
ray = position - node.position;
|
|
|
|
}
|
2019-02-18 12:57:18 +00:00
|
|
|
QVector3D sum;
|
|
|
|
size_t count = 0;
|
|
|
|
if (!qFuzzyCompare(m_deformThickness, (float)1.0)) {
|
|
|
|
auto deformedPosition = calculateDeformPosition(position, ray, node.baseNormal, m_deformThickness);
|
|
|
|
sum += deformedPosition;
|
|
|
|
++count;
|
|
|
|
}
|
|
|
|
if (!qFuzzyCompare(m_deformWidth, (float)1.0)) {
|
|
|
|
auto deformedPosition = calculateDeformPosition(position, ray, QVector3D::crossProduct(node.baseNormal, cutDirect), m_deformWidth);
|
|
|
|
sum += deformedPosition;
|
|
|
|
++count;
|
|
|
|
}
|
|
|
|
if (count > 0)
|
|
|
|
position = sum / count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
bool StrokeMeshBuilder::buildBaseNormalsOnly()
|
2019-08-17 10:13:11 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
return prepare();
|
2019-08-17 10:13:11 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 23:15:20 +00:00
|
|
|
bool StrokeMeshBuilder::build()
|
2019-12-14 13:28:14 +00:00
|
|
|
{
|
2020-04-07 23:15:20 +00:00
|
|
|
if (!prepare())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
buildMesh();
|
|
|
|
applyDeform();
|
|
|
|
return true;
|
2019-02-18 12:57:18 +00:00
|
|
|
}
|