From 75fedd545d867db29bca5d5da117553bdadc2192 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Thu, 12 Feb 2026 14:23:24 +0300 Subject: [PATCH] Workaround floating point determinism by using doubles --- .../BrushStrokeCanvasComponent.cpp | 205 +++++++++--------- .../BrushStrokeCanvasComponent.hpp | 17 +- 2 files changed, 120 insertions(+), 102 deletions(-) diff --git a/src/CanvasComponents/BrushStrokeCanvasComponent.cpp b/src/CanvasComponents/BrushStrokeCanvasComponent.cpp index d800ca7..cac1279 100644 --- a/src/CanvasComponents/BrushStrokeCanvasComponent.cpp +++ b/src/CanvasComponents/BrushStrokeCanvasComponent.cpp @@ -16,6 +16,12 @@ #define AABB_PRECHECK_BVH_LEVELS 2 #define DEFAULT_SMOOTHNESS 5 +BrushStrokeCanvasComponentPointDouble::BrushStrokeCanvasComponentPointDouble() {} +BrushStrokeCanvasComponentPointDouble::BrushStrokeCanvasComponentPointDouble(const BrushStrokeCanvasComponentPoint& p): + pos(p.pos.cast()), + width(p.width) +{} + void BrushStrokeCanvasComponent::save(cereal::PortableBinaryOutputArchive& a) const { a(*d.points, d.color, d.hasRoundCaps); } @@ -75,7 +81,7 @@ void BrushStrokeCanvasComponent::draw(SkCanvas* canvas, const DrawData& drawData } std::shared_ptr BrushStrokeCanvasComponent::get_predraw_data_accurate(const DrawData& drawData, const CoordSpaceHelper& coords) const { - std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); + std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); SCollision::AABB viewGenerousColliderInObjSpace = coords.world_collider_to_coords>(drawData.cam.viewingAreaGenerousCollider); viewGenerousColliderInObjSpace.min -= Vector2f{1.0f, 1.0f}; @@ -83,8 +89,8 @@ std::shared_ptr BrushStrokeCanvasComponent::get_predraw_data_accurate(cons std::vector> finalTrianglePoints; - auto triangleFunc = [&](Vector2f a, Vector2f b, Vector2f c) { - SCollision::Triangle triCollider = {a, b, c}; + auto triangleFunc = [&](Vector2d a, Vector2d b, Vector2d c) { + SCollision::Triangle triCollider = {a.cast(), b.cast(), c.cast()}; if(SCollision::collide(triCollider.bounds, viewGenerousColliderInObjSpace)) { auto clipListFunc = [](const std::vector>& clipList, const std::array& axisLineSegment, const std::function& isInClippingAreaFunc) { @@ -95,7 +101,7 @@ std::shared_ptr BrushStrokeCanvasComponent::get_predraw_data_accurate(cons }; std::vector> clipList; - clipList.emplace_back(std::array{coords.from_space(a), coords.from_space(b), coords.from_space(c)}); + clipList.emplace_back(std::array{coords.from_space(triCollider.p[0]), coords.from_space(triCollider.p[1]), coords.from_space(triCollider.p[2])}); clipList = clipListFunc(clipList, {drawData.cam.viewingAreaGenerousCollider.min, drawData.cam.viewingAreaGenerousCollider.top_right()}, [&](const WorldVec& p) { return p.y() > drawData.cam.viewingAreaGenerousCollider.min.y(); }); @@ -192,8 +198,8 @@ void BrushStrokeCanvasComponent::set_data_from(const CanvasComponent& other) { void BrushStrokeCanvasComponent::initialize_draw_data(DrawingProgram& drawP) { if(!d.points->empty()) { - std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); - auto triangleFunc = [&](Vector2f a, Vector2f b, Vector2f c) { + std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); + auto triangleFunc = [&](Vector2d a, Vector2d b, Vector2d c) { return false; }; @@ -212,35 +218,37 @@ void BrushStrokeCanvasComponent::initialize_draw_data(DrawingProgram& drawP) { create_collider(); } -std::vector BrushStrokeCanvasComponent::get_wedge_indices(const std::vector& points) const { +std::vector BrushStrokeCanvasComponent::get_wedge_indices(const std::vector& points) const { if(points.size() < 2) return {}; std::vector toRet; toRet.emplace_back(0); for(size_t i = 1; i < points.size() - 1; i++) { - Vector2f v1Norm = (points[i - 1].pos - points[i].pos).normalized(); - Vector2f v2Norm = (points[i + 1].pos - points[i].pos).normalized(); - if(v1Norm.dot(v2Norm) > -0.6f) + Vector2d v1Norm = (points[i - 1].pos - points[i].pos).normalized(); + Vector2d v2Norm = (points[i + 1].pos - points[i].pos).normalized(); + if(v1Norm.dot(v2Norm) > -0.6) toRet.emplace_back(i); } toRet.emplace_back(points.size() - 1); return toRet; } -std::vector BrushStrokeCanvasComponent::smooth_points(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const { - std::vector& points = *d.points; +std::vector BrushStrokeCanvasComponent::smooth_points(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const { + std::vector points;// = *d.points; + for(auto& p : *d.points) + points.emplace_back(p); size_t pointsSize = endIndex - beginIndex + 1; if(pointsSize < 2) return {points[beginIndex], points[endIndex]}; - std::vector toRet; + std::vector toRet; - BrushStrokeCanvasComponentPoint p0 = points[beginIndex]; + BrushStrokeCanvasComponentPointDouble p0 = points[beginIndex]; p0.pos = p0.pos + p0.width * (p0.pos - points[beginIndex + 1].pos).normalized() * DRAW_MINIMUM_LIMIT; - BrushStrokeCanvasComponentPoint p1 = points[beginIndex]; - BrushStrokeCanvasComponentPoint p2 = points[beginIndex + 1]; - BrushStrokeCanvasComponentPoint p3; + BrushStrokeCanvasComponentPointDouble p1 = points[beginIndex]; + BrushStrokeCanvasComponentPointDouble p2 = points[beginIndex + 1]; + BrushStrokeCanvasComponentPointDouble p3; for(size_t i = beginIndex; i < endIndex; i++) { @@ -253,10 +261,10 @@ std::vector BrushStrokeCanvasComponent::smooth_ toRet.emplace_back(p1); - float tMove = 1.0 / static_cast(numOfDivisions); + double tMove = 1.0 / static_cast(numOfDivisions); for(unsigned d = 1; d < numOfDivisions; d++) { - float t = tMove * static_cast(d); - BrushStrokeCanvasComponentPoint pToAdd; + double t = tMove * static_cast(d); + BrushStrokeCanvasComponentPointDouble pToAdd; pToAdd.pos = catmull_rom(p0.pos, p1.pos, p2.pos, p3.pos, t); pToAdd.width = std::lerp(p1.width, p2.width, t); if(!(std::isnan(pToAdd.pos.x()) || std::isnan(pToAdd.pos.y()))) // Don't add NAN values @@ -271,39 +279,39 @@ std::vector BrushStrokeCanvasComponent::smooth_ return toRet; } -std::vector BrushStrokeCanvasComponent::smooth_points_avg(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const { - std::vector& points = *d.points; +//std::vector BrushStrokeCanvasComponent::smooth_points_avg(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const { +// std::vector& points = *d.points; +// +// size_t pointsSize = endIndex - beginIndex + 1; +// if(pointsSize < 2) +// return {points[beginIndex], points[endIndex]}; +// +// std::vector toRet; +// +// for(size_t i = beginIndex; i < endIndex; i++) { +// const BrushStrokeCanvasComponentPoint& p1 = points[i]; +// const BrushStrokeCanvasComponentPoint& p2 = points[i + 1]; +// +// float tMove = 1.0 / static_cast(numOfDivisions); +// for(unsigned d = 0; d < numOfDivisions; d++) { +// float t = tMove * static_cast(d); +// BrushStrokeCanvasComponentPoint pToAdd; +// pToAdd.pos = lerp_vec(p1.pos, p2.pos, t); +// pToAdd.width = std::lerp(p1.width, p2.width, t); +// if(!(std::isnan(pToAdd.pos.x()) || std::isnan(pToAdd.pos.y()))) // Don't add NAN values +// toRet.emplace_back(pToAdd); +// } +// } +// +// toRet.emplace_back(points[endIndex]); +// +// return toRet; +//} - size_t pointsSize = endIndex - beginIndex + 1; - if(pointsSize < 2) - return {points[beginIndex], points[endIndex]}; - - std::vector toRet; - - for(size_t i = beginIndex; i < endIndex; i++) { - const BrushStrokeCanvasComponentPoint& p1 = points[i]; - const BrushStrokeCanvasComponentPoint& p2 = points[i + 1]; - - float tMove = 1.0 / static_cast(numOfDivisions); - for(unsigned d = 0; d < numOfDivisions; d++) { - float t = tMove * static_cast(d); - BrushStrokeCanvasComponentPoint pToAdd; - pToAdd.pos = lerp_vec(p1.pos, p2.pos, t); - pToAdd.width = std::lerp(p1.width, p2.width, t); - if(!(std::isnan(pToAdd.pos.x()) || std::isnan(pToAdd.pos.y()))) // Don't add NAN values - toRet.emplace_back(pToAdd); - } - } - - toRet.emplace_back(points[endIndex]); - - return toRet; -} - -std::vector BrushStrokeCanvasComponent::every_nth_point_include_front_and_back(const std::vector& pts, size_t n) const { +std::vector BrushStrokeCanvasComponent::every_nth_point_include_front_and_back(const std::vector& pts, size_t n) const { if(pts.size() <= 3 || n == 0) return pts; - std::vector toRet; + std::vector toRet; toRet.emplace_back(pts.front()); for(size_t i = n; i < pts.size() - 1; i += n) toRet.emplace_back(pts[i]); @@ -311,22 +319,25 @@ std::vector BrushStrokeCanvasComponent::every_n return toRet; } -void BrushStrokeCanvasComponent::create_triangles(const std::function& passTriangleFunc, const std::vector& smoothedPoints, size_t skipVertexCount, std::shared_ptr bPathBuilder) const { +void BrushStrokeCanvasComponent::create_triangles(const std::function& passTriangleFunc, const std::vector& smoothedPoints, size_t skipVertexCount, std::shared_ptr bPathBuilder) const { const int ARC_SMOOTHNESS = 10; const int CIRCLE_SMOOTHNESS = 20; - const std::vector& pointsN = *d.points; std::vector topPoints; std::vector bottomPoints; - if(pointsN.size() == 1 && d.hasRoundCaps) { - std::vector circlePoints = gen_circle_points(pointsN.front().pos, pointsN.front().width / 2.0f, CIRCLE_SMOOTHNESS); + if((*d.points).size() == 1 && d.hasRoundCaps) { + std::vector pointsN; + for(auto& c : *d.points) + pointsN.emplace_back(c); + + std::vector circlePoints = gen_circle_points(pointsN.front().pos, pointsN.front().width / 2.0, CIRCLE_SMOOTHNESS); for(size_t i = 0; i < circlePoints.size(); i++) { if(passTriangleFunc(pointsN.front().pos, circlePoints[i], circlePoints[(i + 1) % circlePoints.size()])) return; topPoints.emplace_back(convert_vec2(circlePoints[i])); } } - if(pointsN.size() < 2) { + if((*d.points).size() < 2) { if(bPathBuilder && !topPoints.empty()) bPathBuilder->addPolygon({topPoints.data(), topPoints.size()}, false); return; @@ -340,25 +351,25 @@ void BrushStrokeCanvasComponent::create_triangles(const std::function vertArray; + std::array vertArray; size_t newIndex = 0; - Vector2f perpPrev = perpendicular_vec2((points[pointsBegin + 1].pos - points[pointsBegin].pos).eval()); + Vector2d perpPrev = perpendicular_vec2((points[pointsBegin + 1].pos - points[pointsBegin].pos).eval()); perpPrev.normalize(); - vertArray[0] = points[pointsBegin].pos + perpPrev * points[pointsBegin].width * 0.5f; + vertArray[0] = points[pointsBegin].pos + perpPrev * points[pointsBegin].width * 0.5; topPoints.emplace_back(convert_vec2(vertArray[0])); - vertArray[1] = points[pointsBegin].pos - perpPrev * points[pointsBegin].width * 0.5f; + vertArray[1] = points[pointsBegin].pos - perpPrev * points[pointsBegin].width * 0.5; bottomPoints.emplace_back(convert_vec2(vertArray[1])); newIndex = 2; if(d.hasRoundCaps && pointsBegin == 0) { - Vector2f arcDirStart = (convert_vec2(vertArray[0]) - points[0].pos).normalized(); - Vector2f arcDirEnd = (convert_vec2(vertArray[1]) - points[0].pos).normalized(); - Vector2f arcDirCenter = (points[0].pos - points[1].pos).normalized(); - std::vector arcPoints = arc_vec(points[0].pos, arcDirStart, arcDirEnd, arcDirCenter, points[0].width * 0.5f, ARC_SMOOTHNESS); - arcPoints.emplace(arcPoints.begin(), convert_vec2(vertArray[0])); - arcPoints.emplace_back(convert_vec2(vertArray[1])); + Vector2d arcDirStart = (vertArray[0] - points[0].pos).normalized(); + Vector2d arcDirEnd = (vertArray[1] - points[0].pos).normalized(); + Vector2d arcDirCenter = (points[0].pos - points[1].pos).normalized(); + std::vector arcPoints = arc_vec(points[0].pos, arcDirStart, arcDirEnd, arcDirCenter, points[0].width * 0.5, ARC_SMOOTHNESS); + arcPoints.emplace(arcPoints.begin(), vertArray[0]); + arcPoints.emplace_back(vertArray[1]); for(size_t i = 0; i < arcPoints.size() - 1; i++) { if(passTriangleFunc(points[0].pos, arcPoints[i], arcPoints[i + 1])) return; bottomPoints.emplace_back(convert_vec2(arcPoints[i])); @@ -370,9 +381,9 @@ void BrushStrokeCanvasComponent::create_triangles(const std::function= pointsEnd) j = pointsEnd - 1; - Vector2f v1 = points[j - 1].pos - points[j].pos; - Vector2f v2 = points[j].pos - points[j + 1].pos; - Vector2f perp = perpendicular_vec2((v1 + v2).eval()).normalized(); + Vector2d v1 = points[j - 1].pos - points[j].pos; + Vector2d v2 = points[j].pos - points[j + 1].pos; + Vector2d perp = perpendicular_vec2((v1 + v2).eval()).normalized(); if(std::isnan(perp.dot(perpPrev))) { std::cout << "NAN FOUND" << std::endl; std::cout << v1 << std::endl; @@ -384,12 +395,12 @@ void BrushStrokeCanvasComponent::create_triangles(const std::function(vertArray[newIndex])); newIndex = (newIndex + 1) % 3; if(passTriangleFunc(vertArray[0], vertArray[1], vertArray[2])) return; - vertArray[newIndex] = points[j].pos - perp * 0.5f * points[j].width; + vertArray[newIndex] = points[j].pos - perp * 0.5 * points[j].width; bottomPoints.emplace_back(convert_vec2(vertArray[newIndex])); newIndex = (newIndex + 1) % 3; if(passTriangleFunc(vertArray[0], vertArray[1], vertArray[2])) return; @@ -397,9 +408,9 @@ void BrushStrokeCanvasComponent::create_triangles(const std::function(topPoints.back()); + Vector2d wedgeP1 = ((currentP1 - currentP2).dot(nextRectDir)) <= 0.0 ? currentP1 : currentP2; + Vector2d wedgeP2 = ((nextP1 - nextP2).dot(rectDir)) <= 0.0 ? nextP1 : nextP2; + bool isTopWedge = convert_vec2(wedgeP1) == topPoints.back(); - float arcRadius; + double arcRadius; - std::vector arcPoints; + std::vector arcPoints; - if(wedgeCenter.x() == std::numeric_limits::max()) { + if(wedgeCenter.x() == std::numeric_limits::max()) { wedgeCenter = (currentP1 + currentP2) * 0.5; wedgeP1 = currentP1; wedgeP2 = currentP2; @@ -461,12 +472,12 @@ void BrushStrokeCanvasComponent::create_triangles(const std::function(arcPoints.back())); } else if(d.hasRoundCaps) { // It's the last point, cap it off - Vector2f arcDirStart = (convert_vec2(vertArray[(newIndex + 1) % 3]) - points.back().pos).normalized(); - Vector2f arcDirEnd = (convert_vec2(vertArray[(newIndex + 2) % 3]) - points.back().pos).normalized(); - Vector2f arcDirCenter = -rectDir; - std::vector arcPoints = arc_vec(points.back().pos, arcDirStart, arcDirEnd, arcDirCenter, points.back().width * 0.5f, ARC_SMOOTHNESS); - arcPoints.emplace(arcPoints.begin(), convert_vec2(vertArray[(newIndex + 1) % 3])); - arcPoints.emplace_back(convert_vec2(vertArray[(newIndex + 2) % 3])); + Vector2d arcDirStart = (vertArray[(newIndex + 1) % 3] - points.back().pos).normalized(); + Vector2d arcDirEnd = (vertArray[(newIndex + 2) % 3] - points.back().pos).normalized(); + Vector2d arcDirCenter = -rectDir; + std::vector arcPoints = arc_vec(points.back().pos, arcDirStart, arcDirEnd, arcDirCenter, points.back().width * 0.5, ARC_SMOOTHNESS); + arcPoints.emplace(arcPoints.begin(), vertArray[(newIndex + 1) % 3]); + arcPoints.emplace_back(vertArray[(newIndex + 2) % 3]); for(size_t i = 0; i < arcPoints.size() - 1; i++) { if(passTriangleFunc(points.back().pos, arcPoints[i], arcPoints[i + 1])) return; topPoints.emplace_back(convert_vec2(arcPoints[i])); @@ -486,9 +497,9 @@ void BrushStrokeCanvasComponent::create_collider() { if(d.points->empty()) return; ColliderCollection strokeObjects; - std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); - create_triangles([&](Vector2f a, Vector2f b, Vector2f c) { - strokeObjects.triangle.emplace_back(a, b, c); + std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); + create_triangles([&](Vector2d a, Vector2d b, Vector2d c) { + strokeObjects.triangle.emplace_back(a.cast(), b.cast(), c.cast()); return false; }, points, 0, nullptr); strokeObjects.recalculate_bounds(); @@ -526,9 +537,9 @@ bool BrushStrokeCanvasComponent::collides_within_coords(const SCollision::Collid return false; bool toRet = false; - std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); - create_triangles([&](Vector2f a, Vector2f b, Vector2f c) { - toRet = SCollision::collide(checkAgainst, SCollision::Triangle{a, b, c}); + std::vector points = smooth_points(0, d.points->size() - 1, DEFAULT_SMOOTHNESS); + create_triangles([&](Vector2d a, Vector2d b, Vector2d c) { + toRet = SCollision::collide(checkAgainst, SCollision::Triangle{a.cast(), b.cast(), c.cast()}); return toRet; }, points, 0, nullptr); return toRet; diff --git a/src/CanvasComponents/BrushStrokeCanvasComponent.hpp b/src/CanvasComponents/BrushStrokeCanvasComponent.hpp index bceb305..15cfed8 100644 --- a/src/CanvasComponents/BrushStrokeCanvasComponent.hpp +++ b/src/CanvasComponents/BrushStrokeCanvasComponent.hpp @@ -13,6 +13,13 @@ struct BrushStrokeCanvasComponentPoint { } }; +struct BrushStrokeCanvasComponentPointDouble { + BrushStrokeCanvasComponentPointDouble(); + BrushStrokeCanvasComponentPointDouble(const BrushStrokeCanvasComponentPoint& p); + Vector2d pos; + double width; +}; + class BrushStrokeCanvasComponent : public CanvasComponent { public: constexpr static float DRAW_MINIMUM_LIMIT = 0.3; @@ -50,13 +57,13 @@ class BrushStrokeCanvasComponent : public CanvasComponent { std::array, 2> brushPathLOD; - std::vector every_nth_point_include_front_and_back(const std::vector& pts, size_t n) const; + std::vector every_nth_point_include_front_and_back(const std::vector& pts, size_t n) const; void add_precheck_aabb_level(size_t level, const std::vector>& levelArray); std::vector> precheckAABBLevels; - void create_triangles(const std::function& passTriangleFunc, const std::vector& smoothedPoints, size_t skipVertexCount, std::shared_ptr bPath) const; - std::vector get_wedge_indices(const std::vector& points) const; - std::vector smooth_points(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const; - std::vector smooth_points_avg(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const; + void create_triangles(const std::function& passTriangleFunc, const std::vector& smoothedPoints, size_t skipVertexCount, std::shared_ptr bPath) const; + std::vector get_wedge_indices(const std::vector& points) const; + std::vector smooth_points(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const; + //std::vector smooth_points_avg(size_t beginIndex, size_t endIndex, unsigned numOfDivisions) const; };