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:
luigi-rosso
2025-11-15 17:24:32 +00:00
parent d182adede2
commit caefe3dfe3
8 changed files with 778 additions and 2 deletions

View File

@@ -1 +1 @@
c85665930cd09db16b0db2cb3d27405b64723aef
b63286db94fd8de300e452db63d454b5bebb092b

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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[] = {

View File

@@ -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();
}

View 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);
}