mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 21:21:17 +01:00
feature: add contour measure to scripted path! (#11049) b63286db94
* feature: add contour measure to scripted path! * fix: failing tests Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
c85665930cd09db16b0db2cb3d27405b64723aef
|
||||
b63286db94fd8de300e452db63d454b5bebb092b
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "rive/math/raw_path.hpp"
|
||||
#include "rive/renderer.hpp"
|
||||
#include "rive/math/vec2d.hpp"
|
||||
#include "rive/math/contour_measure.hpp"
|
||||
#include "rive/math/path_measure.hpp"
|
||||
#include "rive/shapes/paint/image_sampler.hpp"
|
||||
#include "rive/viewmodel/viewmodel_instance_boolean.hpp"
|
||||
#include "rive/viewmodel/viewmodel_instance_color.hpp"
|
||||
@@ -58,6 +60,8 @@ enum class LuaAtoms : int16_t
|
||||
close,
|
||||
reset,
|
||||
add,
|
||||
contours,
|
||||
measure,
|
||||
|
||||
// Mat2D
|
||||
invert,
|
||||
@@ -166,7 +170,14 @@ enum class LuaAtoms : int16_t
|
||||
decompose,
|
||||
children,
|
||||
parent,
|
||||
node
|
||||
node,
|
||||
|
||||
// PathMeasure/ContourMeasure
|
||||
positionAndTangent,
|
||||
warp,
|
||||
extract,
|
||||
next,
|
||||
isClosed
|
||||
};
|
||||
|
||||
struct ScriptedMat2D
|
||||
@@ -806,6 +817,41 @@ private:
|
||||
TransformComponent* m_component;
|
||||
};
|
||||
|
||||
class ScriptedContourMeasure
|
||||
{
|
||||
public:
|
||||
ScriptedContourMeasure(rcp<ContourMeasure> measure,
|
||||
rcp<RefCntContourMeasureIter> iter) :
|
||||
m_measure(measure), m_iter(iter)
|
||||
{}
|
||||
|
||||
static constexpr uint8_t luaTag = LUA_T_COUNT + 26;
|
||||
static constexpr const char* luaName = "ContourMeasure";
|
||||
static constexpr bool hasMetatable = true;
|
||||
|
||||
ContourMeasure* measure() { return m_measure.get(); }
|
||||
rcp<RefCntContourMeasureIter> iter() { return m_iter; }
|
||||
|
||||
private:
|
||||
rcp<ContourMeasure> m_measure;
|
||||
rcp<RefCntContourMeasureIter> m_iter;
|
||||
};
|
||||
|
||||
class ScriptedPathMeasure
|
||||
{
|
||||
public:
|
||||
ScriptedPathMeasure(PathMeasure measure) : m_measure(std::move(measure)) {}
|
||||
|
||||
static constexpr uint8_t luaTag = LUA_T_COUNT + 27;
|
||||
static constexpr const char* luaName = "PathMeasure";
|
||||
static constexpr bool hasMetatable = true;
|
||||
|
||||
PathMeasure* measure() { return &m_measure; }
|
||||
|
||||
private:
|
||||
PathMeasure m_measure;
|
||||
};
|
||||
|
||||
static void interruptCPP(lua_State* L, int gc);
|
||||
|
||||
class CPPRuntimeScriptingContext : public ScriptingContext
|
||||
|
||||
@@ -132,6 +132,14 @@ public:
|
||||
this->rewind(path, tol);
|
||||
}
|
||||
|
||||
// Constructor that copies the path, allowing ContourMeasure objects to
|
||||
// outlive the original path.
|
||||
ContourMeasureIter(const RawPath& path, float tol = kDefaultTolerance)
|
||||
{
|
||||
m_optionalCopy = path;
|
||||
this->rewind(&m_optionalCopy, tol);
|
||||
}
|
||||
|
||||
void rewind(const RawPath*, float = kDefaultTolerance);
|
||||
|
||||
// Returns a measure object for each contour in the path
|
||||
@@ -154,6 +162,24 @@ public:
|
||||
std::vector<uint32_t> m_segmentCounts;
|
||||
};
|
||||
|
||||
// Ref-counted wrapper for ContourMeasureIter, used when the iterator needs
|
||||
// to outlive stack scope (e.g., in script bindings).
|
||||
class RefCntContourMeasureIter : public RefCnt<RefCntContourMeasureIter>
|
||||
{
|
||||
public:
|
||||
RefCntContourMeasureIter(
|
||||
const RawPath& path,
|
||||
float tol = ContourMeasureIter::kDefaultTolerance) :
|
||||
m_iter(path, tol)
|
||||
{}
|
||||
|
||||
ContourMeasureIter* get() { return &m_iter; }
|
||||
ContourMeasureIter* operator->() { return &m_iter; }
|
||||
|
||||
private:
|
||||
ContourMeasureIter m_iter;
|
||||
};
|
||||
|
||||
} // namespace rive
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,7 +15,13 @@ public:
|
||||
ContourMeasure::PosTanDistance atDistance(float distance) const;
|
||||
ContourMeasure::PosTanDistance atPercentage(float percentageDistance) const;
|
||||
|
||||
void getSegment(float startDistance,
|
||||
float endDistance,
|
||||
RawPath* dst,
|
||||
bool startWithMove = true) const;
|
||||
|
||||
float length() const { return m_length; }
|
||||
bool isClosed() const;
|
||||
|
||||
private:
|
||||
float m_length;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "rive/math/vec2d.hpp"
|
||||
#include "rive/lua/rive_lua_libs.hpp"
|
||||
#include "rive/math/raw_path.hpp"
|
||||
#include "rive/math/contour_measure.hpp"
|
||||
#include "rive/math/path_measure.hpp"
|
||||
#include "rive/factory.hpp"
|
||||
|
||||
#include <math.h>
|
||||
@@ -107,6 +109,29 @@ static int path_add(lua_State* L)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int path_contours(lua_State* L)
|
||||
{
|
||||
auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
|
||||
// Use the copy constructor to ensure ContourMeasure outlives the path
|
||||
auto iter = make_rcp<RefCntContourMeasureIter>(scriptedPath->rawPath);
|
||||
auto firstContour = iter->get()->next();
|
||||
if (firstContour)
|
||||
{
|
||||
lua_newrive<ScriptedContourMeasure>(L, firstContour, iter);
|
||||
return 1;
|
||||
}
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int path_measure(lua_State* L)
|
||||
{
|
||||
auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
|
||||
PathMeasure pathMeasure(&scriptedPath->rawPath);
|
||||
lua_newrive<ScriptedPathMeasure>(L, std::move(pathMeasure));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int path_namecall(lua_State* L)
|
||||
{
|
||||
int atom;
|
||||
@@ -129,6 +154,10 @@ static int path_namecall(lua_State* L)
|
||||
return path_reset(L);
|
||||
case (int)LuaAtoms::add:
|
||||
return path_add(L);
|
||||
case (int)LuaAtoms::contours:
|
||||
return path_contours(L);
|
||||
case (int)LuaAtoms::measure:
|
||||
return path_measure(L);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +165,220 @@ static int path_namecall(lua_State* L)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ContourMeasure methods
|
||||
static int contour_measure_length(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
|
||||
lua_pushnumber(L, scripted->measure()->length());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int contour_measure_isClosed(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
|
||||
lua_pushboolean(L, scripted->measure()->isClosed());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int contour_measure_positionAndTangent(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
|
||||
float distance = (float)luaL_checknumber(L, 2);
|
||||
auto posTan = scripted->measure()->getPosTan(distance);
|
||||
lua_pushvec2d(L, posTan.pos);
|
||||
lua_pushvec2d(L, posTan.tan);
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int contour_measure_warp(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
|
||||
auto src = lua_checkvec2d(L, 2);
|
||||
Vec2D result = scripted->measure()->warp(*src);
|
||||
lua_pushvec2d(L, result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int contour_measure_extract(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
|
||||
float startDistance = (float)luaL_checknumber(L, 2);
|
||||
float endDistance = (float)luaL_checknumber(L, 3);
|
||||
auto destPath = lua_torive<ScriptedPath>(L, 4);
|
||||
bool startWithMove = lua_isboolean(L, 5) ? lua_toboolean(L, 5) : true;
|
||||
scripted->measure()->getSegment(startDistance,
|
||||
endDistance,
|
||||
&destPath->rawPath,
|
||||
startWithMove);
|
||||
destPath->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int contour_measure_next(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
|
||||
auto iter = scripted->iter();
|
||||
if (iter)
|
||||
{
|
||||
auto nextContour = iter->get()->next();
|
||||
if (nextContour)
|
||||
{
|
||||
// Create new ScriptedContourMeasure with the same rcp iter
|
||||
// The iter is already advanced, so we can reuse it
|
||||
lua_newrive<ScriptedContourMeasure>(L, nextContour, iter);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int contour_measure_index(lua_State* L)
|
||||
{
|
||||
int atom;
|
||||
const char* key = lua_tostringatom(L, 2, &atom);
|
||||
if (!key)
|
||||
{
|
||||
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (atom)
|
||||
{
|
||||
case (int)LuaAtoms::length:
|
||||
return contour_measure_length(L);
|
||||
case (int)LuaAtoms::isClosed:
|
||||
return contour_measure_isClosed(L);
|
||||
case (int)LuaAtoms::next:
|
||||
return contour_measure_next(L);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int contour_measure_namecall(lua_State* L)
|
||||
{
|
||||
int atom;
|
||||
const char* str = lua_namecallatom(L, &atom);
|
||||
if (str != nullptr)
|
||||
{
|
||||
switch (atom)
|
||||
{
|
||||
case (int)LuaAtoms::positionAndTangent:
|
||||
return contour_measure_positionAndTangent(L);
|
||||
case (int)LuaAtoms::warp:
|
||||
return contour_measure_warp(L);
|
||||
case (int)LuaAtoms::extract:
|
||||
return contour_measure_extract(L);
|
||||
}
|
||||
}
|
||||
|
||||
luaL_error(L,
|
||||
"%s is not a valid method of %s",
|
||||
str,
|
||||
ScriptedContourMeasure::luaName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// PathMeasure methods
|
||||
static int path_measure_length(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
|
||||
lua_pushnumber(L, scripted->measure()->length());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int path_measure_isClosed(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
|
||||
lua_pushboolean(L, scripted->measure()->isClosed());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int path_measure_positionAndTangent(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
|
||||
float distance = (float)luaL_checknumber(L, 2);
|
||||
auto posTanDist = scripted->measure()->atDistance(distance);
|
||||
lua_pushvec2d(L, posTanDist.pos);
|
||||
lua_pushvec2d(L, posTanDist.tan);
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int path_measure_warp(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
|
||||
auto src = lua_checkvec2d(L, 2);
|
||||
// Use atDistance to get position and tangent, then apply warp formula
|
||||
auto posTanDist = scripted->measure()->atDistance(src->x);
|
||||
Vec2D result = {
|
||||
posTanDist.pos.x - posTanDist.tan.y * src->y,
|
||||
posTanDist.pos.y + posTanDist.tan.x * src->y,
|
||||
};
|
||||
lua_pushvec2d(L, result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int path_measure_extract(lua_State* L)
|
||||
{
|
||||
auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
|
||||
float startDistance = (float)luaL_checknumber(L, 2);
|
||||
float endDistance = (float)luaL_checknumber(L, 3);
|
||||
auto destPath = lua_torive<ScriptedPath>(L, 4);
|
||||
bool startWithMove = lua_isboolean(L, 5) ? lua_toboolean(L, 5) : true;
|
||||
scripted->measure()->getSegment(startDistance,
|
||||
endDistance,
|
||||
&destPath->rawPath,
|
||||
startWithMove);
|
||||
destPath->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int path_measure_index(lua_State* L)
|
||||
{
|
||||
int atom;
|
||||
const char* key = lua_tostringatom(L, 2, &atom);
|
||||
if (!key)
|
||||
{
|
||||
luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (atom)
|
||||
{
|
||||
case (int)LuaAtoms::length:
|
||||
return path_measure_length(L);
|
||||
case (int)LuaAtoms::isClosed:
|
||||
return path_measure_isClosed(L);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int path_measure_namecall(lua_State* L)
|
||||
{
|
||||
int atom;
|
||||
const char* str = lua_namecallatom(L, &atom);
|
||||
if (str != nullptr)
|
||||
{
|
||||
switch (atom)
|
||||
{
|
||||
case (int)LuaAtoms::positionAndTangent:
|
||||
return path_measure_positionAndTangent(L);
|
||||
case (int)LuaAtoms::warp:
|
||||
return path_measure_warp(L);
|
||||
case (int)LuaAtoms::extract:
|
||||
return path_measure_extract(L);
|
||||
}
|
||||
}
|
||||
|
||||
luaL_error(L,
|
||||
"%s is not a valid method of %s",
|
||||
str,
|
||||
ScriptedPathMeasure::luaName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const luaL_Reg pathStaticMethods[] = {
|
||||
{"new", path_new},
|
||||
{NULL, NULL},
|
||||
@@ -152,6 +395,24 @@ int luaopen_rive_path(lua_State* L)
|
||||
lua_setreadonly(L, -1, true);
|
||||
lua_pop(L, 1); // pop the metatable
|
||||
|
||||
// Register ContourMeasure
|
||||
lua_register_rive<ScriptedContourMeasure>(L);
|
||||
lua_pushcfunction(L, contour_measure_index, nullptr);
|
||||
lua_setfield(L, -2, "__index");
|
||||
lua_pushcfunction(L, contour_measure_namecall, nullptr);
|
||||
lua_setfield(L, -2, "__namecall");
|
||||
lua_setreadonly(L, -1, true);
|
||||
lua_pop(L, 1); // pop the metatable
|
||||
|
||||
// Register PathMeasure
|
||||
lua_register_rive<ScriptedPathMeasure>(L);
|
||||
lua_pushcfunction(L, path_measure_index, nullptr);
|
||||
lua_setfield(L, -2, "__index");
|
||||
lua_pushcfunction(L, path_measure_namecall, nullptr);
|
||||
lua_setfield(L, -2, "__namecall");
|
||||
lua_setreadonly(L, -1, true);
|
||||
lua_pop(L, 1); // pop the metatable
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ std::unordered_map<std::string, int16_t> atoms = {
|
||||
{"close", (int16_t)LuaAtoms::close},
|
||||
{"reset", (int16_t)LuaAtoms::reset},
|
||||
{"add", (int16_t)LuaAtoms::add},
|
||||
{"contours", (int16_t)LuaAtoms::contours},
|
||||
{"measure", (int16_t)LuaAtoms::measure},
|
||||
{"invert", (int16_t)LuaAtoms::invert},
|
||||
{"isIdentity", (int16_t)LuaAtoms::isIdentity},
|
||||
{"width", (int16_t)LuaAtoms::width},
|
||||
@@ -117,6 +119,11 @@ std::unordered_map<std::string, int16_t> atoms = {
|
||||
{"parent", (int16_t)LuaAtoms::parent},
|
||||
{"node", (int16_t)LuaAtoms::node},
|
||||
{"addToPath", (int16_t)LuaAtoms::addToPath},
|
||||
{"positionAndTangent", (int16_t)LuaAtoms::positionAndTangent},
|
||||
{"warp", (int16_t)LuaAtoms::warp},
|
||||
{"extract", (int16_t)LuaAtoms::extract},
|
||||
{"next", (int16_t)LuaAtoms::next},
|
||||
{"isClosed", (int16_t)LuaAtoms::isClosed},
|
||||
};
|
||||
|
||||
static const luaL_Reg lualibs[] = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "rive/math/path_measure.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
using namespace rive;
|
||||
|
||||
@@ -50,3 +51,63 @@ ContourMeasure::PosTanDistance PathMeasure::atPercentage(
|
||||
|
||||
return atDistance(m_length * inRangePercentage);
|
||||
}
|
||||
|
||||
void PathMeasure::getSegment(float startDistance,
|
||||
float endDistance,
|
||||
RawPath* dst,
|
||||
bool startWithMove) const
|
||||
{
|
||||
if (dst == nullptr || m_contours.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp distances to valid range
|
||||
startDistance = std::max(0.0f, std::min(startDistance, m_length));
|
||||
endDistance = std::max(0.0f, std::min(endDistance, m_length));
|
||||
|
||||
if (startDistance >= endDistance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float currentDistance = 0.0f;
|
||||
bool isFirstSegment = true;
|
||||
|
||||
for (auto contour : m_contours)
|
||||
{
|
||||
float contourLength = contour->length();
|
||||
float contourStart = currentDistance;
|
||||
float contourEnd = currentDistance + contourLength;
|
||||
|
||||
// Check if this contour intersects with the requested range
|
||||
if (contourEnd > startDistance && contourStart < endDistance)
|
||||
{
|
||||
// Calculate the local distances within this contour
|
||||
float localStart = std::max(0.0f, startDistance - contourStart);
|
||||
float localEnd =
|
||||
std::min(contourLength, endDistance - contourStart);
|
||||
|
||||
// Extract from this contour
|
||||
contour->getSegment(localStart,
|
||||
localEnd,
|
||||
dst,
|
||||
isFirstSegment && startWithMove);
|
||||
isFirstSegment = false;
|
||||
}
|
||||
|
||||
currentDistance += contourLength;
|
||||
|
||||
// If we've passed the end distance, we're done
|
||||
if (currentDistance >= endDistance)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PathMeasure::isClosed() const
|
||||
{
|
||||
// Return true only if there is exactly one contour and it is closed
|
||||
return m_contours.size() == 1 && m_contours[0]->isClosed();
|
||||
}
|
||||
|
||||
369
tests/unit_tests/runtime/scripting/scripted_path_test.cpp
Normal file
369
tests/unit_tests/runtime/scripting/scripted_path_test.cpp
Normal file
@@ -0,0 +1,369 @@
|
||||
|
||||
#include "catch.hpp"
|
||||
#include "scripting_test_utilities.hpp"
|
||||
#include "rive/lua/rive_lua_libs.hpp"
|
||||
#include "rive/math/path_types.hpp"
|
||||
|
||||
using namespace rive;
|
||||
|
||||
TEST_CASE("path contours returns first contour", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"path:close()\n"
|
||||
"local contour = path:contours()\n"
|
||||
"return contour ~= nil\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure has length", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"path:close()\n"
|
||||
"local contour = path:contours()\n"
|
||||
"return contour.length\n");
|
||||
lua_State* L = vm.state();
|
||||
float length = (float)lua_tonumber(L, -1);
|
||||
CHECK(length > 0.0f);
|
||||
CHECK(length < 100.0f); // Should be around 30 for a 10x10 rectangle
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure isClosed", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"path:close()\n"
|
||||
"local contour = path:contours()\n"
|
||||
"return contour.isClosed\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure isClosed false for open path", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local contour = path:contours()\n"
|
||||
"return contour.isClosed\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(!lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure positionAndTangent", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"local contour = path:contours()\n"
|
||||
"local pos, tan = contour:positionAndTangent(0)\n"
|
||||
"return pos.x, pos.y, tan.x, tan.y\n",
|
||||
4);
|
||||
lua_State* L = vm.state();
|
||||
float posX = (float)lua_tonumber(L, -4);
|
||||
float posY = (float)lua_tonumber(L, -3);
|
||||
float tanX = (float)lua_tonumber(L, -2);
|
||||
float tanY = (float)lua_tonumber(L, -1);
|
||||
CHECK(posX == Approx(0.0f));
|
||||
CHECK(posY == Approx(0.0f));
|
||||
CHECK(tanX > 0.0f); // Tangent should point along the line
|
||||
CHECK(tanY == Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure warp", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"local contour = path:contours()\n"
|
||||
"local result = contour:warp(Vec2D.xy(5, 2))\n"
|
||||
"return result.x, result.y\n",
|
||||
2);
|
||||
lua_State* L = vm.state();
|
||||
float x = (float)lua_tonumber(L, -2);
|
||||
float y = (float)lua_tonumber(L, -1);
|
||||
CHECK(x == Approx(5.0f));
|
||||
CHECK(y == Approx(2.0f)); // Offset perpendicular to path
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure extract", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local contour = path:contours()\n"
|
||||
"local destPath: Path = Path.new()\n"
|
||||
"contour:extract(0, 10, destPath, true)\n"
|
||||
"return destPath\n");
|
||||
lua_State* L = vm.state();
|
||||
auto destPath = lua_torive<ScriptedPath>(L, -1);
|
||||
CHECK(destPath != nullptr);
|
||||
// The extracted path should have some content
|
||||
CHECK(destPath->rawPath.verbs().count() > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure extract defaults to startWithMove true",
|
||||
"[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local contour = path:contours()\n"
|
||||
"local destPath: Path = Path.new()\n"
|
||||
"contour:extract(0, 10, destPath)\n"
|
||||
"return destPath\n");
|
||||
lua_State* L = vm.state();
|
||||
auto destPath = lua_torive<ScriptedPath>(L, -1);
|
||||
CHECK(destPath != nullptr);
|
||||
CHECK(destPath->rawPath.verbs().count() > 0);
|
||||
// When startWithMove is not provided, it should default to true
|
||||
// So the first verb should be a move
|
||||
CHECK(destPath->rawPath.verbs()[0] == PathVerb::move);
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure extract with startWithMove false", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local contour = path:contours()\n"
|
||||
"local destPath: Path = Path.new()\n"
|
||||
"destPath:moveTo(Vec2D.xy(100, 100))\n"
|
||||
"contour:extract(0, 10, destPath, false)\n"
|
||||
"return destPath\n");
|
||||
lua_State* L = vm.state();
|
||||
auto destPath = lua_torive<ScriptedPath>(L, -1);
|
||||
CHECK(destPath != nullptr);
|
||||
CHECK(destPath->rawPath.verbs().count() > 1);
|
||||
// When startWithMove is false and path already has a move,
|
||||
// the extracted segment should continue without adding another move
|
||||
// So we should have the initial move, then the extracted geometry
|
||||
CHECK(destPath->rawPath.verbs()[0] == PathVerb::move);
|
||||
// The second verb should be a line (from the extracted segment)
|
||||
CHECK(destPath->rawPath.verbs()[1] == PathVerb::line);
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure next iterates contours", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:close()\n"
|
||||
"path:moveTo(Vec2D.xy(20, 20))\n"
|
||||
"path:lineTo(Vec2D.xy(30, 20))\n"
|
||||
"path:close()\n"
|
||||
"local contour1 = path:contours()\n"
|
||||
"local contour2 = contour1.next\n"
|
||||
"return contour1 ~= nil, contour2 ~= nil\n",
|
||||
2);
|
||||
lua_State* L = vm.state();
|
||||
CHECK(lua_toboolean(L, -2)); // First contour exists
|
||||
CHECK(lua_toboolean(L, -1)); // Second contour exists
|
||||
}
|
||||
|
||||
TEST_CASE("contour measure next returns nil when done", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:close()\n"
|
||||
"local contour1 = path:contours()\n"
|
||||
"local contour2 = contour1.next\n"
|
||||
"return contour2 == nil\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("path measure returns PathMeasure", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"path:close()\n"
|
||||
"local measure = path:measure()\n"
|
||||
"return measure ~= nil\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("path measure has length", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"path:close()\n"
|
||||
"local measure = path:measure()\n"
|
||||
"return measure.length\n");
|
||||
lua_State* L = vm.state();
|
||||
float length = (float)lua_tonumber(L, -1);
|
||||
CHECK(length > 0.0f);
|
||||
CHECK(length < 100.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("path measure isClosed for single closed contour", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"path:close()\n"
|
||||
"local measure = path:measure()\n"
|
||||
"return measure.isClosed\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("path measure isClosed false for multiple contours", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:close()\n"
|
||||
"path:moveTo(Vec2D.xy(20, 20))\n"
|
||||
"path:lineTo(Vec2D.xy(30, 20))\n"
|
||||
"path:close()\n"
|
||||
"local measure = path:measure()\n"
|
||||
"return measure.isClosed\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(!lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("path measure isClosed false for open path", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local measure = path:measure()\n"
|
||||
"return measure.isClosed\n");
|
||||
lua_State* L = vm.state();
|
||||
CHECK(!lua_toboolean(L, -1));
|
||||
}
|
||||
|
||||
TEST_CASE("path measure positionAndTangent", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"local measure = path:measure()\n"
|
||||
"local pos, tan = measure:positionAndTangent(0)\n"
|
||||
"return pos.x, pos.y, tan.x, tan.y\n",
|
||||
4);
|
||||
lua_State* L = vm.state();
|
||||
float posX = (float)lua_tonumber(L, -4);
|
||||
float posY = (float)lua_tonumber(L, -3);
|
||||
float tanX = (float)lua_tonumber(L, -2);
|
||||
float tanY = (float)lua_tonumber(L, -1);
|
||||
CHECK(posX == Approx(0.0f));
|
||||
CHECK(posY == Approx(0.0f));
|
||||
CHECK(tanX > 0.0f);
|
||||
CHECK(tanY == Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("path measure warp", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"local measure = path:measure()\n"
|
||||
"local result = measure:warp(Vec2D.xy(5, 2))\n"
|
||||
"return result.x, result.y\n",
|
||||
2);
|
||||
lua_State* L = vm.state();
|
||||
float x = (float)lua_tonumber(L, -2);
|
||||
float y = (float)lua_tonumber(L, -1);
|
||||
CHECK(x == Approx(5.0f));
|
||||
CHECK(y == Approx(2.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("path measure extract", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local measure = path:measure()\n"
|
||||
"local destPath: Path = Path.new()\n"
|
||||
"measure:extract(0, 10, destPath, true)\n"
|
||||
"return destPath\n");
|
||||
lua_State* L = vm.state();
|
||||
auto destPath = lua_torive<ScriptedPath>(L, -1);
|
||||
CHECK(destPath != nullptr);
|
||||
CHECK(destPath->rawPath.verbs().count() > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("path measure extract defaults to startWithMove true", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local measure = path:measure()\n"
|
||||
"local destPath: Path = Path.new()\n"
|
||||
"measure:extract(0, 10, destPath)\n"
|
||||
"return destPath\n");
|
||||
lua_State* L = vm.state();
|
||||
auto destPath = lua_torive<ScriptedPath>(L, -1);
|
||||
CHECK(destPath != nullptr);
|
||||
CHECK(destPath->rawPath.verbs().count() > 0);
|
||||
// When startWithMove is not provided, it should default to true
|
||||
// So the first verb should be a move
|
||||
CHECK(destPath->rawPath.verbs()[0] == PathVerb::move);
|
||||
}
|
||||
|
||||
TEST_CASE("path measure extract with startWithMove false", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 10))\n"
|
||||
"local measure = path:measure()\n"
|
||||
"local destPath: Path = Path.new()\n"
|
||||
"destPath:moveTo(Vec2D.xy(100, 100))\n"
|
||||
"measure:extract(0, 10, destPath, false)\n"
|
||||
"return destPath\n");
|
||||
lua_State* L = vm.state();
|
||||
auto destPath = lua_torive<ScriptedPath>(L, -1);
|
||||
CHECK(destPath != nullptr);
|
||||
CHECK(destPath->rawPath.verbs().count() > 1);
|
||||
// When startWithMove is false and path already has a move,
|
||||
// the extracted segment should continue without adding another move
|
||||
// So we should have the initial move, then the extracted geometry
|
||||
CHECK(destPath->rawPath.verbs()[0] == PathVerb::move);
|
||||
// The second verb should be a line (from the extracted segment)
|
||||
CHECK(destPath->rawPath.verbs()[1] == PathVerb::line);
|
||||
}
|
||||
|
||||
TEST_CASE("path measure extract across multiple contours", "[scripting]")
|
||||
{
|
||||
ScriptingTest vm("local path: Path = Path.new()\n"
|
||||
"path:moveTo(Vec2D.xy(0, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(10, 0))\n"
|
||||
"path:close()\n"
|
||||
"path:moveTo(Vec2D.xy(20, 0))\n"
|
||||
"path:lineTo(Vec2D.xy(30, 0))\n"
|
||||
"path:close()\n"
|
||||
"local measure = path:measure()\n"
|
||||
"local destPath: Path = Path.new()\n"
|
||||
"measure:extract(5, 25, destPath, true)\n"
|
||||
"return destPath\n");
|
||||
lua_State* L = vm.state();
|
||||
auto destPath = lua_torive<ScriptedPath>(L, -1);
|
||||
CHECK(destPath != nullptr);
|
||||
// Should extract from both contours
|
||||
CHECK(destPath->rawPath.verbs().count() > 0);
|
||||
}
|
||||
Reference in New Issue
Block a user