From ae1435300aad04c080a1ce50441c48b65acce304 Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Sun, 14 Jan 2024 19:07:31 +0100 Subject: [PATCH] feat: Add untested parts of the roughjs port These are not needed (yet) in our framebox use, but let's have them in an "untested" folder. This will help to get started if we later decide to extract the port as a separate Lua rocks. --- ptable.sile-dev-1.rockspec | 21 +- rough-lua/rough/generator.lua | 11 +- rough-lua/rough/renderer.lua | 10 +- .../untested/path-data-parser/absolutize.lua | 95 ++++++++ rough-lua/untested/path-data-parser/init.lua | 20 ++ .../untested/path-data-parser/normalize.lua | 204 ++++++++++++++++++ .../untested/path-data-parser/parser.lua | 152 +++++++++++++ .../points-on-curve/curve-to-bezier.lua | 56 +++++ rough-lua/untested/points-on-curve/init.lua | 148 +++++++++++++ 9 files changed, 701 insertions(+), 16 deletions(-) create mode 100644 rough-lua/untested/path-data-parser/absolutize.lua create mode 100644 rough-lua/untested/path-data-parser/init.lua create mode 100644 rough-lua/untested/path-data-parser/normalize.lua create mode 100644 rough-lua/untested/path-data-parser/parser.lua create mode 100644 rough-lua/untested/points-on-curve/curve-to-bezier.lua create mode 100644 rough-lua/untested/points-on-curve/init.lua diff --git a/ptable.sile-dev-1.rockspec b/ptable.sile-dev-1.rockspec index cf45ae3..705f866 100644 --- a/ptable.sile-dev-1.rockspec +++ b/ptable.sile-dev-1.rockspec @@ -22,8 +22,27 @@ build = { ["sile.packages.parbox"] = "packages/parbox/init.lua", ["sile.packages.ptable"] = "packages/ptable/init.lua", ["sile.packages.framebox"] = "packages/framebox/init.lua", - ["sile.packages.framebox.graphics.prng"] = "packages/framebox/graphics/prng.lua", ["sile.packages.framebox.graphics.renderer"] = "packages/framebox/graphics/renderer.lua", ["sile.packages.framebox.graphics.rough"] = "packages/framebox/graphics/rough.lua", + ["prng-prigarin"] = "prng-prigarin/init.lua", + ["rough-lua.rough.jsshims"] = "rough-lua/rough/jsshims.lua", + ["rough-lua.rough.generator"] = "rough-lua/rough/generator.lua", + ["rough-lua.rough.renderer"] = "rough-lua/rough/renderer.lua", + ["rough-lua.rough.fillers.hachure-filler"] = "rough-lua/rough/fillers/hachure-filler.lua", + ["rough-lua.rough.fillers.zigzag-filler"] = "rough-lua/rough/fillers/zigzag-filler.lua", + ["rough-lua.rough.fillers.zigzag-line-filler"] = "rough-lua/rough/fillers/zigzag-line-filler.lua", + ["rough-lua.rough.fillers.dot-filler"] = "rough-lua/rough/fillers/dot-filler.lua", + ["rough-lua.rough.fillers.dashed-filler"] = "rough-lua/rough/fillers/dashed-filler.lua", + ["rough-lua.rough.fillers.scan-line-hachure"] = "rough-lua/rough/fillers/scan-line-hachure.lua", + ["rough-lua.rough.fillers.hatch-filler"] = "rough-lua/rough/fillers/hatch-filler.lua", + ["rough-lua.rough.fillers.hachure-fill"] = "rough-lua/rough/fillers/hachure-fill.lua", + ["rough-lua.rough.fillers.filler"] = "rough-lua/rough/fillers/filler.lua", + ["rough-lua.rough.geometry"] = "rough-lua/rough/geometry.lua", + ["rough-lua.untested.path-data-parser"] = "rough-lua/untested/path-data-parser/init.lua", + ["rough-lua.untested.path-data-parser.parser"] = "rough-lua/untested/path-data-parser/parser.lua", + ["rough-lua.untested.path-data-parser.normalize"] = "rough-lua/untested/path-data-parser/normalize.lua", + ["rough-lua.untested.path-data-parser.absolutize"] = "rough-lua/untested/path-data-parser/absolutize.lua", + ["rough-lua.untested.points-on-curve"] = "rough-lua/untested/points-on-curve/init.lua", + ["rough-lua.untested.points-on-curve.curve-to-bezier"] = "rough-lua/untested/points-on-curve/curve-to-bezier.lua", } } diff --git a/rough-lua/rough/generator.lua b/rough-lua/rough/generator.lua index d442a2f..4873d94 100644 --- a/rough-lua/rough/generator.lua +++ b/rough-lua/rough/generator.lua @@ -18,14 +18,9 @@ local line, rectangle, renderer.arc, renderer.curve, renderer.linearPath, renderer.svgPath, renderer.patternFillArc, renderer.patternFillPolygons, renderer.solidFillPolygon --- PORTING NOTE: --- I ported the module but haven't tested it for now --- local curveToBezier = require("rough-lua.points-on-curve.curve-to-bezier").curveToBezier --- local pointsOnPath = require("rough-lua.points-on-curve").pointsOnPath --- local pointsOnBezierCurves = require("rough-lua.points-on-curve").pointsOnBezierCurves -local pointsOnPath = function () error("Not implemented") end -local curveToBezier = function () error("Not implemented") end -local pointsOnBezierCurves = function () error("Not implemented") end +local curveToBezier = require("rough-lua.untested.points-on-curve.curve-to-bezier").curveToBezier +local pointsOnPath = require("rough-lua.untested.points-on-curve").pointsOnPath +local pointsOnBezierCurves = require("rough-lua.untested.points-on-curve").pointsOnBezierCurves local RoughGenerator = pl.class({ diff --git a/rough-lua/rough/renderer.lua b/rough-lua/rough/renderer.lua index 176c0ab..e8760b6 100644 --- a/rough-lua/rough/renderer.lua +++ b/rough-lua/rough/renderer.lua @@ -12,13 +12,9 @@ local jsshims = require("rough-lua.rough.jsshims") local array_concat = jsshims.array_concat local PRNG = require("prng-prigarin") --- PORTING NOTE: --- I ported path-data-parser but haven't tested it for now --- local pathDataParser = require("rough-lua.path-data-parser") --- local parsePath, normalize, absolutize = pathDataParser.parsePath, pathDataParser.normalize, pathDataParser.absolutize -local normalize = function () error("Not yet implemented") end -local absolutize = function () error("Not yet implemented") end -local parsePath = function () error("Not yet implemented") end +local pathDataParser = require("rough-lua.untested.path-data-parser") +local parsePath, normalize, absolutize + = pathDataParser.parsePath, pathDataParser.normalize, pathDataParser.absolutize local getFiller = require("rough-lua.rough.fillers.filler").getFiller diff --git a/rough-lua/untested/path-data-parser/absolutize.lua b/rough-lua/untested/path-data-parser/absolutize.lua new file mode 100644 index 0000000..3f2d429 --- /dev/null +++ b/rough-lua/untested/path-data-parser/absolutize.lua @@ -0,0 +1,95 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local function absolutize(segments) + local cx, cy = 0, 0 + local subx, suby = 0, 0 + local out = {} + for _, segment in ipairs(segments) do + local key, data = segment.key, segment.data + if key == 'M' then + out[#out + 1] = { key = 'M', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + subx, suby = data[1], data[2] + elseif key == 'm' then + cx = cx + data[1] + cy = cy + data[2] + out[#out + 1] = { key = 'M', data = { cx, cy } } + subx, suby = cx, cy + elseif key == 'L' then + out[#out + 1] = { key = 'L', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + elseif key == 'l' then + cx = cx + data[1] + cy = cy + data[2] + out[#out + 1] = { key = 'L', data = { cx, cy } } + elseif key == 'C' then + out[#out + 1] = { key = 'C', data = pl.tablex.copy(data) } + cx, cy = data[5], data[6] + elseif key == 'c' then + local newdata = pl.tablex.map(data, function (d, i) + return (i % 2) == 0 and (d + cx) or (d + cy) + end) + out[#out + 1] = { key = 'C', data = newdata } + cx, cy = newdata[5], newdata[6] + elseif key == 'Q' then + out[#out + 1] = { key = 'Q', data = pl.tablex.copy(data) } + cx, cy = data[2], data[3] + elseif key == 'q' then + local newdata = pl.tablex.map(data, function (d, i) + return (i % 2) == 0 and (d + cx) or (d + cy) + end) + out[#out + 1] = { key = 'Q', data = newdata } + cx, cy = newdata[2], newdata[3] + elseif key == 'A' then + out[#out + 1] = { key = 'A', data = pl.tablex.copy(data) } + cx, cy = data[5], data[6] + elseif key == 'a' then + cx = cx + data[5] + cy = cy + data[6] + out[#out + 1] = { key = 'A', data = { data[1], data[2], data[3], data[4], data[5], cx, cy } } + elseif key == 'H' then + out[#out + 1] = { key = 'H', data = pl.tablex.copy(data) } + cx = data[1] + elseif key == 'h' then + cx = cx + data[1] + out[#out + 1] = { key = 'H', data = { cx } } + elseif key == 'V' then + out[#out + 1] = { key = 'V', data = pl.tablex.copy(data) } + cy = data[1] + elseif key == 'v' then + cy = cy + data[1] + out[#out + 1] = { key = 'V', data = { cy } } + elseif key == 'S' then + out[#out + 1] = { key = 'S', data = pl.tablex.copy(data) } + cx, cy = data[2], data[3] + elseif key == 's' then + local newdata = pl.tablex.map(data, function (d, i) + return (i % 2) == 0 and (d + cx) or (d + cy) + end) + out[#out + 1] = { key = 'S', data = newdata } + cx, cy = newdata[2], newdata[3] + elseif key == 'T' then + out[#out + 1] = { key = 'T', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + elseif key == 't' then + cx = cx + data[1] + cy = cy + data[2] + out[#out + 1] = { key = 'T', data = { cx, cy } } + elseif key == 'Z' or key == 'z' then + out[#out + 1] = { key = 'Z', data = {} } + cx, cy = subx, suby + end + end + return out +end + +return { + absolutize = absolutize, +} diff --git a/rough-lua/untested/path-data-parser/init.lua b/rough-lua/untested/path-data-parser/init.lua new file mode 100644 index 0000000..165b80c --- /dev/null +++ b/rough-lua/untested/path-data-parser/init.lua @@ -0,0 +1,20 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local normalize = require("rough-lua.untested.path-data-parser.normalize").normalize +local absolutize = require("rough-lua.untested.path-data-parser.absolutize").absolutize +local parsePath = require("rough-lua.untested.path-data-parser.parser").parsePath +local serialize = require("rough-lua.untested.path-data-parser.parser").serialize + +return { + parsePath = parsePath, + serialize = serialize, + absolutize = absolutize, + normalize = normalize, +} diff --git a/rough-lua/untested/path-data-parser/normalize.lua b/rough-lua/untested/path-data-parser/normalize.lua new file mode 100644 index 0000000..afa6b12 --- /dev/null +++ b/rough-lua/untested/path-data-parser/normalize.lua @@ -0,0 +1,204 @@ +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local jsshims = require("rough-lua.rough.jsshims") +local array_concat = jsshims.array_concat + +local function degToRad (degrees) + return (math.pi * degrees) / 180 +end + +local function rotate (x, y, angleRad) + local X = x * math.cos(angleRad) - y * math.sin(angleRad) + local Y = x * math.sin(angleRad) + y * math.cos(angleRad) + return { X, Y } +end + +local function arcToCubicCurves (x1, y1, x2, y2, r1, r2, angle, largeArcFlag, sweepFlag, recursive) + local angleRad = degToRad(angle) + local params = {} + local f1, f2, cx, cy + if recursive then + f1, f2, cx, cy = recursive[1], recursive[2], recursive[3], recursive[4] + else + x1, y1 = rotate(x1, y1, -angleRad) + x2, y2 = rotate(x2, y2, -angleRad) + local x = (x1 - x2) / 2 + local y = (y1 - y2) / 2 + local h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2) + if h > 1 then + h = math.sqrt(h) + r1 = h * r1 + r2 = h * r2 + end + local sign = (largeArcFlag == sweepFlag) and -1 or 1 + local r1Pow = r1 * r1 + local r2Pow = r2 * r2 + local left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x + local right = r1Pow * y * y + r2Pow * x * x + local k = sign * math.sqrt(math.abs(left / right)) + cx = k * r1 * y / r2 + (x1 + x2) / 2 + cy = k * -r2 * x / r1 + (y1 + y2) / 2 + f1 = math.asin(((y1 - cy) / r2)) + f2 = math.asin(((y2 - cy) / r2)) + if x1 < cx then + f1 = math.pi - f1 + end + if x2 < cx then + f2 = math.pi - f2 + end + if f1 < 0 then + f1 = math.pi * 2 + f1 + end + if f2 < 0 then + f2 = math.pi * 2 + f2 + end + if sweepFlag and f1 > f2 then + f1 = f1 - math.pi * 2 + end + if not sweepFlag and f2 > f1 then + f2 = f2 - math.pi * 2 + end + end + local df = f2 - f1 + if math.abs(df) > (math.pi * 120 / 180) then + local f2old = f2 + local x2old = x2 + local y2old = y2 + if sweepFlag and f2 > f1 then + f2 = f1 + (math.pi * 120 / 180) * (1) + else + f2 = f1 + (math.pi * 120 / 180) * (-1) + end + x2 = cx + r1 * math.cos(f2) + y2 = cy + r2 * math.sin(f2) + params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, { f2, f2old, cx, cy }) + end + df = f2 - f1 + local c1 = math.cos(f1) + local s1 = math.sin(f1) + local c2 = math.cos(f2) + local s2 = math.sin(f2) + local t = math.tan(df / 4) + local hx = 4 / 3 * r1 * t + local hy = 4 / 3 * r2 * t + local m1 = { x1, y1 } + local m2 = { x1 + hx * s1, y1 - hy * c1 } + local m3 = { x2 + hx * s2, y2 - hy * c2 } + local m4 = { x2, y2 } + m2[1] = 2 * m1[1] - m2[1] + m2[2] = 2 * m1[2] - m2[2] + if recursive then + return array_concat({ m2, m3, m4 }, params) + else + params = array_concat({ m2, m3, m4 }, params) + local curves = {} + for i = 1, #params, 3 do + local ro1 = rotate(params[i][1], params[i][2], angleRad) + local ro2 = rotate(params[i + 1][1], params[i + 1][2], angleRad) + local ro3 = rotate(params[i + 2][1], params[i + 2][2], angleRad) + curves[#curves + 1] = { ro1[1], ro1[2], ro2[1], ro2[2], ro3[1], ro3[2] } + end + return curves + end +end + +local function normalize(segments) + local out = {} + local lastType = '' + local cx, cy = 0, 0 + local subx, suby = 0, 0 + local lcx, lcy = 0, 0 + for _, segment in ipairs(segments) do + local key, data = segment.key, segment.data + if key == 'M' then + out[#out + 1] = { key = 'M', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + subx, suby = data[1], data[2] + elseif key == 'C' then + out[#out + 1] = { key = 'C', data = pl.tablex.copy(data) } + cx, cy = data[5], data[6] + lcx, lcy = data[3], data[4] + elseif key == 'L' then + out[#out + 1] = { key = 'L', data = pl.tablex.copy(data) } + cx, cy = data[1], data[2] + elseif key == 'H' then + cx = data[1] + out[#out + 1] = { key = 'L', data = { cx, cy } } + elseif key == 'V' then + cy = data[1] + out[#out + 1] = { key = 'L', data = { cx, cy } } + elseif key == 'S' then + local cx1, cy1 + if lastType == 'C' or lastType == 'S' then + cx1 = cx + (cx - lcx) + cy1 = cy + (cy - lcy) + else + cx1 = cx + cy1 = cy + end + out[#out + 1] = { key = 'C', data = { cx1, cy1, pl.tablex.copy(data) } } + lcx, lcy = data[1], data[2] + cx, cy = data[3], data[4] + elseif key == 'T' then + local x, y = data[1], data[2] + local x1, y1 + if lastType == 'Q' or lastType == 'T' then + x1 = cx + (cx - lcx) + y1 = cy + (cy - lcy) + else + x1 = cx + y1 = cy + end + local cx1 = cx + 2 * (x1 - cx) / 3 + local cy1 = cy + 2 * (y1 - cy) / 3 + local cx2 = x + 2 * (x1 - x) / 3 + local cy2 = y + 2 * (y1 - y) / 3 + out[#out + 1] = { key = 'C', data = { cx1, cy1, cx2, cy2, x, y } } + lcx, lcy = x1, y1 + cx, cy = x, y + elseif key == 'Q' then + local x1, y1, x, y = data[1], data[2], data[3], data[4] + local cx1 = cx + 2 * (x1 - cx) / 3 + local cy1 = cy + 2 * (y1 - cy) / 3 + local cx2 = x + 2 * (x1 - x) / 3 + local cy2 = y + 2 * (y1 - y) / 3 + out[#out + 1] = { key = 'C', data = { cx1, cy1, cx2, cy2, x, y } } + lcx, lcy = x1, y1 + cx, cy = x, y + elseif key == 'A' then + local r1, r2 = math.abs(data[1]), math.abs(data[2]) + local angle = data[3] + local largeArcFlag = data[4] + local sweepFlag = data[5] + local x, y = data[6], data[7] + if r1 == 0 or r2 == 0 then + out[#out + 1] = { key = 'C', data = { cx, cy, x, y, x, y } } + cx, cy = x, y + else + if cx ~= x or cy ~= y then + local curves = arcToCubicCurves(cx, cy, x, y, r1, r2, angle, largeArcFlag, sweepFlag) + for _, curve in ipairs(curves) do + out[#out + 1] = { key = 'C', data = curve } + end + cx, cy = x, y + end + end + elseif key == 'Z' then + out[#out + 1] = { key = 'Z', data = {} } + cx, cy = subx, suby + end + lastType = key + end + return out +end + +return { + normalize = normalize, + arcToCubicCurves = arcToCubicCurves, +} diff --git a/rough-lua/untested/path-data-parser/parser.lua b/rough-lua/untested/path-data-parser/parser.lua new file mode 100644 index 0000000..374552f --- /dev/null +++ b/rough-lua/untested/path-data-parser/parser.lua @@ -0,0 +1,152 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the path-data-parser JavaScript library. +-- (https://github.com/pshihn/path-data-parser/) +-- License: MIT +-- Copyright (c) 2019 Preet Shihn +-- +local COMMAND = 0 +local NUMBER = 1 +local EOD = 2 + +local PARAMS = { + A = 7, + a = 7, + C = 6, + c = 6, + H = 1, + h = 1, + L = 2, + l = 2, + M = 2, + m = 2, + Q = 4, + q = 4, + S = 4, + s = 4, + T = 2, + t = 2, + V = 1, + v = 1, + Z = 0, + z = 0, +} + +local function tokenize (d) + local tokens = {} + while d ~= '' do + local i, j = d:find("^[ \t\r\n,]+") + if i then + d = d:sub(j + 1) + else + i, j = d:find("^[aAcChHlLmMqQsStTvVzZ]") + if i then + tokens[#tokens + 1] = { type = COMMAND, text = d:sub(i, j) } + d = d:sub(j + 1) + else + i, j = d:find("^[+-]?%.?[0-9]+%.?[eE0-9]*") + -- PORTING NOTE: + -- JS has "^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?^.[0-9]+)([eE][-+]?[0-9]+)?)") + -- Lua does not support such complex regexps, so we use a simpler one + -- but it will not catch some malformed numbers. + if i then + tokens[#tokens + 1] = { type = NUMBER, text = tostring(tonumber(d:sub(i, j))) } + d = d:sub(j + 1) + else + return {} + end + end + end + end + tokens[#tokens + 1] = { type = EOD, text = '' } + return tokens +end + +local function isType (token, type) + return token.type == type +end + +local function parsePath (d) + local segments = {} + local tokens = tokenize(d) + local mode = 'BOD' + local index = 1 + local token = tokens[index] + while not isType(token, EOD) do + local paramsCount + local params = {} + if mode == 'BOD' then + if token.text == 'M' or token.text == 'm' then + index = index + 1 + paramsCount = PARAMS[token.text] + mode = token.text + else + return parsePath('M0,0' .. d) + end + elseif isType(token, NUMBER) then + paramsCount = PARAMS[mode] + else + index = index + 1 + paramsCount = PARAMS[token.text] + mode = token.text + end + if (index + paramsCount) <= #tokens then + for i = index, index + paramsCount - 1 do + local numbeToken = tokens[i] + if isType(numbeToken, NUMBER) then + params[#params + 1] = tonumber(numbeToken.text) + else + error('Param not a number: ' .. mode .. ',' .. numbeToken.text) + end + end + if type(PARAMS[mode]) == 'number' then + local segment = { key = mode, data = params } + segments[#segments + 1] = segment + index = index + paramsCount + token = tokens[index] + if mode == 'M' then + mode = 'L' + end + if mode == 'm' then + mode = 'l' + end + else + error('Bad segment: ' .. mode) + end + else + error('Path data ended short') + end + end + return segments +end + +local function serialize (segments) + local tokens = {} + for _, segment in ipairs(segments) do + tokens[#tokens + 1] = segment.key + if segment.key == 'C' or segment.key == 'c' then + tokens[#tokens + 1] = segment.data[1] + tokens[#tokens + 1] = tostring(segment.data[2]) .. ',' + tokens[#tokens + 1] = segment.data[3] + tokens[#tokens + 1] = tostring(segment.data[4]) .. ',' + tokens[#tokens + 1] = segment.data[5] + tokens[#tokens + 1] = segment.data[6] + elseif segment.key == 'S' or segment.key == 's' + or segment.key == 'Q' or segment.key == 'q' then + tokens[#tokens + 1] = segment.data[1] + tokens[#tokens + 1] = tostring(segment.data[2]) .. ',' + tokens[#tokens + 1] = segment.data[3] + tokens[#tokens + 1] = segment.data[4] + else + pl.tablex.insertvalues(tokens, segment.data) + end + end + return table.concat(tokens, ' ') +end + +return { + parsePath = parsePath, + serialize = serialize, +} \ No newline at end of file diff --git a/rough-lua/untested/points-on-curve/curve-to-bezier.lua b/rough-lua/untested/points-on-curve/curve-to-bezier.lua new file mode 100644 index 0000000..2ab8f22 --- /dev/null +++ b/rough-lua/untested/points-on-curve/curve-to-bezier.lua @@ -0,0 +1,56 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the bezier-points JavaScript library. +-- (https://github.com/pshihn/bezier-points) +-- License: MIT +-- Copyright (c) 2020 Preet Shihn +-- +local function clone (p) + return { p[1], p[2] } +end + +local function curveToBezier (pointsIn, curveTightness) + local len = #pointsIn + if len < 3 then + error('A curve must have at least three points.') + end + local out = {} + if len == 3 then + out[#out + 1] = clone(pointsIn[1]) + out[#out + 1] = clone(pointsIn[2]) + out[#out + 1] = clone(pointsIn[3]) + out[#out + 1] = clone(pointsIn[3]) + else + local points = {} + points[#points + 1] = pointsIn[1] + points[#points + 1] = pointsIn[1] + for i = 2, #pointsIn do + points[#points + 1] = pointsIn[i] + if i == (#pointsIn - 1) then + points[#points + 1] = pointsIn[i] + end + end + local b = {} + local s = 1 - curveTightness + out[#out + 1] = clone(points[1]) + for i = 2, (#points - 2) do + local cachedVertArray = points[i] + b[1] = { cachedVertArray[1], cachedVertArray[2] } + b[2] = { cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6, cachedVertArray[2] + (s * points[i + 1][2] - s * points[i - 1][2]) / 6 } + b[3] = { points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6, points[i + 1][2] + (s * points[i][2] - s * points[i + 2][2]) / 6 } + b[4] = { points[i + 1][1], points[i + 1][2] } + out[#out + 1] = b[2] + out[#out + 1] = b[3] + out[#out + 1] = b[4] + end + end + return out +end + +-- Exports + +return { + curveToBezier = curveToBezier, +} diff --git a/rough-lua/untested/points-on-curve/init.lua b/rough-lua/untested/points-on-curve/init.lua new file mode 100644 index 0000000..9085609 --- /dev/null +++ b/rough-lua/untested/points-on-curve/init.lua @@ -0,0 +1,148 @@ +-- +-- License: MIT +-- Copyright (c) 2023, Didier Willis +-- +-- This is a straightforward port of the bezier-points JavaScript library. +-- (https://github.com/pshihn/bezier-points) +-- License: MIT +-- Copyright (c) 2020 Preet Shihn +-- + +local function lerp (a, b, t) + return {a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t} +end + +-- Distance between 2 points squared +local function distanceSq (p1, p2) + return (p1[1] - p2[1]) ^ 2 + (p1[2] - p2[2]) ^ 2 +end + +-- Distance squared from a point p to the line segment vw +local function distanceToSegmentSq (p, v, w) + local l2 = distanceSq(v, w) + if l2 == 0 then + return distanceSq(p, v) + end + local t = ((p[1] - v[1]) * (w[1] - v[1]) + (p[2] - v[2]) * (w[2] - v[2])) / l2 + t = math.max(0, math.min(1, t)) + return distanceSq(p, lerp(v, w, t)) +end + +-- Adapted from https://seant23.wordpress.com/2010/11/12/offset-bezier-curves/ +local function flatness (points, offset) + local p1 = points[offset + 1] + local p2 = points[offset + 2] + local p3 = points[offset + 3] + local p4 = points[offset + 4] + + local ux = 3 * p2[1] - 2 * p1[1] - p4[1] + ux = ux * ux + local uy = 3 * p2[2] - 2 * p1[2] - p4[2] + uy = uy * uy + local vx = 3 * p3[1] - 2 * p4[1] - p1[1] + vx = vx * vx + local vy = 3 * p3[2] - 2 * p4[2] - p1[2] + vy = vy * vy + + if ux < vx then + ux = vx + end + if uy < vy then + uy = vy + end + return ux + uy +end + +local function getPointsOnBezierCurveWithSplitting (points, offset, tolerance, newPoints) + local outPoints = newPoints or {} + if flatness(points, offset) < tolerance then + local p0 = points[offset + 1] + if #outPoints > 0 then + local d = math.sqrt(distanceSq(outPoints[#outPoints], p0)) + if d > 1 then + table.insert(outPoints, p0) + end + else + table.insert(outPoints, p0) + end + table.insert(outPoints, points[offset + 4]) + else + -- subdivide + local t = .5 + local p1 = points[offset + 1] + local p2 = points[offset + 2] + local p3 = points[offset + 3] + local p4 = points[offset + 4] + + local q1 = lerp(p1, p2, t) + local q2 = lerp(p2, p3, t) + local q3 = lerp(p3, p4, t) + + local r1 = lerp(q1, q2, t) + local r2 = lerp(q2, q3, t) + + local red = lerp(r1, r2, t) + + getPointsOnBezierCurveWithSplitting({p1, q1, r1, red}, 0, tolerance, outPoints) + getPointsOnBezierCurveWithSplitting({red, r2, q3, p4}, 0, tolerance, outPoints) + end + return outPoints +end + +-- Ramer–Douglas–Peucker algorithm +-- https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm +local function simplifyPoints (points, start, finish, distance) + local outPoints = {} + local s = points[start] + local e = points[finish] + local maxDistSq = 0 + local maxNdx = 1 + for i = start + 1, finish - 1 do + local distSq = distanceToSegmentSq(points[i], s, e) + if distSq > maxDistSq then + maxDistSq = distSq + maxNdx = i + end + end + if math.sqrt(maxDistSq) > distance then + local t1 = simplifyPoints(points, start, maxNdx + 1, distance) + local t2 = simplifyPoints(points, maxNdx, finish, distance) + for _, v in ipairs(t1) do + table.insert(outPoints, v) + end + for _, v in ipairs(t2) do + table.insert(outPoints, v) + end + else + if #outPoints == 0 then + table.insert(outPoints, s) + end + table.insert(outPoints, e) + end + return outPoints +end + +local function simplify (points, distance) + return simplifyPoints(points, 1, #points, distance) +end + +local function pointsOnBezierCurves (points, tolerance, distance) + local newPoints = {} + local numSegments = (#points - 1) / 3 + for i = 0, numSegments - 1 do + local offset = i * 3 + getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints) + end + if distance and distance > 0 then + return simplifyPoints(newPoints, 1, #newPoints, distance) + end + return newPoints +end + +-- Exports + +return { + simplify = simplify, + simplifyPoints = simplifyPoints, + pointsOnBezierCurves = pointsOnBezierCurves, +}