Skip to main content
  • Home
  • Development
  • Documentation
  • Donate
  • Operational login
  • Browse the archive

swh logo
SoftwareHeritage
Software
Heritage
Archive
Features
  • Search

  • Downloads

  • Save code now

  • Add forge now

  • Help

https://github.com/nithin1527/onion-soup
03 June 2025, 15:20:27 UTC
  • Code
  • Branches (1)
  • Releases (0)
  • Visits
    • Branches
    • Releases
    • HEAD
    • refs/heads/main
    • 698a000737b45477597e7ffc8f17e59072bfe7f3
    No releases to show
  • 8d9d496
  • /
  • floatingBodies.lua
Raw File Download Save again
Take a new snapshot of a software origin

If the archived software origin currently browsed is not synchronized with its upstream version (for instance when new commits have been issued), you can explicitly request Software Heritage to take a new snapshot of it.

Use the form below to proceed. Once a request has been submitted and accepted, it will be processed as soon as possible. You can then check its processing state by visiting this dedicated page.
swh spinner

Processing "take a new snapshot" request ...

To reference or cite the objects present in the Software Heritage archive, permalinks based on SoftWare Hash IDentifiers (SWHIDs) must be used.
Select below a type of object currently browsed in order to display its associated SWHID and permalink.

  • content
  • directory
  • revision
  • snapshot
origin badgecontent badge
swh:1:cnt:7fde6c95454818f4877ffd6567a7aaa6bd44847f
origin badgedirectory badge
swh:1:dir:8d9d49631e2688049de55f2c874e85e56cb50620
origin badgerevision badge
swh:1:rev:698a000737b45477597e7ffc8f17e59072bfe7f3
origin badgesnapshot badge
swh:1:snp:c055d63f5a6fe8fb1d9092a820512b5f1eea62fa

This interface enables to generate software citations, provided that the root directory of browsed objects contains a citation.cff or codemeta.json file.
Select below a type of object currently browsed in order to generate citations for them.

  • content
  • directory
  • revision
  • snapshot
(requires biblatex-software package)
Generating citation ...
(requires biblatex-software package)
Generating citation ...
(requires biblatex-software package)
Generating citation ...
(requires biblatex-software package)
Generating citation ...
Tip revision: 698a000737b45477597e7ffc8f17e59072bfe7f3 authored by nithin1527 on 12 April 2025, 02:19:45 UTC
Update README.md
Tip revision: 698a000
floatingBodies.lua
label = "Points and Polygons"
methods = {{ label = "Floating Body", run = runFloatingBody }}

-- Returns a table of the vertecies in all selected polygons
-- In the event that multiple polygons are selected, only one is chosen
function get_pts_and_segment_selection(model)
    local p = model:page()
    if not p:hasSelection() then
        return
    end

    local tab = {}

    for _, obj, sel, _ in p:objects() do
        local transform = obj:matrix()
        if sel then
            if obj:type() == "path" then 
                local shape = obj:shape()
                for _, subpath in ipairs(shape) do
                    if subpath.type == "curve" then
                        local max = 0
                        for j, vertices in ipairs(subpath) do
                            table.insert(tab, transform*vertices[1])
                            max = max + 1
                        end
                        table.insert(tab, transform*subpath[max][2])
                    end
                end
                -- Only finds the first selected path object
                break
            end
        end
    end

    return tab
end

-- Reorders a table of sequential points starting at a provided value
-- Courtesy of GPT
function reorderTable(t, startElement)
    -- Find the index of the start element
    local startIndex = nil
    for i, v in ipairs(t) do
        if v == startElement then
            startIndex = i
            break
        end
    end

    -- If the element is not found, return the original table
    if startIndex == nil then
        print("Element not found in the table")
        return t
    end

    -- Create a new table to store the reordered result
    local reordered = {}

    -- Add the elements from the start index to the end
    for i = startIndex, #t do
        table.insert(reordered, t[i])
    end

    -- Add the elements from the beginning to the start index - 1
    for i = 1, startIndex - 1 do
        table.insert(reordered, t[i])
    end

    return reordered
end

-- Givena table of adjacent points of a polygon, returns true if it is convex
function isConvex(vertices)
    function orient(p, q, r) return p.x * (q.y - r.y) + q.x * (r.y - p.y) + r.x * (p.y - q.y) end
    local side = nil
    local i = 1
    while i < #vertices do
        local temp = orient(vertices[i], vertices[(i % #vertices)+1], vertices[((i+1)%#vertices)+1])
        if side == nil then
            if temp > 0 then
                side = 1
            end
            if temp < 0 then
                side = -1
            end
        else
            if side*temp < 0 then
                return false
            end
        end
        i = i + 1

    end
    return true
end

-- Calculate the area of a polygon given its vertices (Shoelace algorithm)
-- Code generated via Copilot
function calculate_polygon_area(vertices)
    local area = 0
    local num_vertices = #vertices

    for i = 1, num_vertices do
        local j = (i % num_vertices) + 1
        area = area + (vertices[i].x * vertices[j].y) - (vertices[j].x * vertices[i].y)
    end

    return math.abs(area) / 2
end

-- Function to compare points based on dot product, then x value, then y
-- (Allows a certain chain to always be on a certain side of another)
function compare(a, b)
    if a[1] < b[1] then
        return true
    end

    if a[1] > b[1] then
        return false
    end

    if a[2].x < b[2].x then
        return true
    end

    if a[2].x > b[2].x then
        return false
    end

    if a[2].y < b[2].y then
        return true
    end

    return false
end

-- Orders points based on dot product
function get_closest(points, dir, model)
    -- Stores the dot product to sort by
    local temp = {}
    for _, pt in ipairs(points) do
        table.insert(temp, {dir^pt, pt})
    end

    --table.sort(temp, compare) Just trying it, shouldn't matter though
    table.sort(temp, function(a,b) return a[1]<b[1] end)

    -- Convert it back into a table of vertices
    local newPoints = {}
    for _, pt in ipairs(temp) do
        table.insert(newPoints, pt[2])
    end

    return newPoints
end


--[=[
Given:
 - vertices, segments of polygon A: () -> {Vector}, () -> {Segment} 
 - vertices, segments of polygon B: () -> {Vector}, () -> {Segment} 
Return:
 - table of interection points: () -> {Vector}
--]=]
function get_intersection_points(s1,s2, model)
	local intersections = {}
     
    local end_point = s1[#s1]
    for i=1,#s1 do
        local intersection = ipe.Segment(s1[i], end_point):intersects(s2)
        if intersection then
            table.insert(intersections, intersection)
        end
        end_point = s1[i]
    end
	return intersections
end



-- euclidean distance between two points
function euclidean_distance(p1, p2)
    return math.sqrt(math.pow((p1.x-p2.x), 2) + math.pow((p1.y-p2.y), 2))
end


-- Use the first and last point in sorted to construct the chains
function get_segments_sorted(coordinates, sorted, model)
    local chain1 = {}
    local chain2 = {}
    local first = sorted[1]
    local last = sorted[#sorted]
    local index

    for i, x in ipairs(coordinates) do
        if first == x then
            index = i
            break
        end
    end

    if index == nil then
        model:warning("Issue finding segment in get_segments_sorted: ")
    end

    local i = index - 1
    local chainFlag = true

    local cur

    local flag = true
    while flag or i ~= index-1 do
        flag = false
        cur = coordinates[i+1]
        if cur == last then
            chainFlag = false
        end

        if chainFlag then
            table.insert(chain1, cur)
        else
            table.insert(chain2, 1, cur)
        end

        i = (i + 1) % #coordinates
    end

    -- In order to make sure they have access to the segments, we add the first and last node to both
    -- It is easy enough to check to see if it is the first or the last node. 
    -- Requires a check for othogonality
    table.insert(chain1, last)
    table.insert(chain2, 1, first)
    return {chain1, chain2}

end

-- Used for get_angle
function get_magnitude(p1)
    return math.sqrt(math.pow(p1.x, 2) + math.pow(p1.y, 2))
end

-- get the difference between two vertices
function get_slope(a,b)
    return ipe.Vector(a.x-b.x, a.y-b.y)
end

-- Given two angle vectors, get the angle between them (Used in get_angle_between_segments)
function get_angle(a, b)
    return math.acos(a^b/(get_magnitude(a) * get_magnitude(b)))
end

-- Given a segment and direction vector, get the angle between
function get_angle_between_segments(a1, a2, dir)
    return get_angle(get_slope(a1, a2), dir)
end

-- Find the area using a given h value
function get_area(gamma, h, theta1, theta2, model)
    if h < 0.001 then
        model:warning("Tiny h")
        return 0
    end
    local temp = gamma * h + h^2/2 * (math.tan(theta1) + math.tan(theta2))
    
    if temp < 0 then
        model:warning("Negative area")
        model:warning("Gamma = " .. gamma .. "H = " .. h .. ", theta1 = " .. theta1 .. " Theta2 = " .. theta2)
        model:warning(temp)
    end
    return gamma * h + h^2/2 * (math.tan(theta1) + math.tan(theta2))
end

-- Make a line at a given point in the direction dir
function get_line(point, dir, model)
    local temp_point = ipe.Vector(point.x + dir.x*20, point.y + dir.y*20)
    if point.x == nil then
        model:warning("Breaky1")
    end
    
    local temp = ipe.LineThrough(point, temp_point)
    return temp
end

-- Returns the inverse of the given vertex direction
function get_inverse(dir, model)
    return ipe.Vector(-1 * dir.y, 1 * dir.x)
end

-- Make a line perpendicular to the given vertex direction at point (used for the lines at each vertex)
function get_perp_line(point, dir, model)
    local temp = get_line(point, get_inverse(dir, model), model)
  return temp
end


-- Pass in 2 line segments and a line for the direction vector, returns the min distance between the two vectors
-- While you could solve the direction yourself as it is orthogonal to the segments, to reduce unneeded calculations
-- I just pass it in
-- Will break if the lines are parallel to dir, but not if they overlap


function get_dist_between_lines(seg1, seg2, dir, model)
    local p1 = seg1:intersects(get_line(ipe.Vector(0,0), dir, model))
    local p2 = seg2:intersects(get_line(ipe.Vector(0,0), dir, model))

    return euclidean_distance(p1, p2)
end


function get_dist_for_points(p1, p2, dir, model)
    --model:warning("1" .. dir.x .. dir.y)
    return get_dist_between_lines(get_perp_line(p1, dir, model), get_perp_line(p2, dir, model), dir, model)
end




-- Issue lies in thetas, with figuring out whether to make it positive or negative
-- Given a location (1) and a point for direction (2), get the area of that step
function calc_area(a1, a2, b1, b2, dir, gamma, h, model)
    local seg1 = ipe.Segment(a1, a2)
    local seg2 = ipe.Segment(b1, b2)

    local temp1 = seg1:line():dir()
    local temp2 = seg2:line():dir()


    local theta1 = get_angle(temp1, dir)
    local theta2 = get_angle(temp2, dir)

    -- Needs to adjust the angle depending on if it should increase or decrease the area
    if temp1.x*dir.y - temp1.y*dir.x < 0 then
        theta1 = -theta1
    end
    if temp2.x*dir.y - temp2.y*dir.x > 0 then
        theta2 = -theta2
    end

    return math.abs(get_area(gamma, h, theta1, theta2, model))
end

function reverseTable(t)
    local n = #t
    for i = 1, math.floor(n / 2) do
        t[i], t[n - i + 1] = t[n - i + 1], t[i]
    end
end

-- Issue lies in thetas, with figuring out whether to make it positive or negative
-- Given a location (1) and a point for direction (2), get the area of that step
function find_h(a1, a2, b1, b2, dir, gamma, area, model)
    if area < 0.001 then
        return 0
    end


    local seg1 = ipe.Segment(a1, a2)
    local seg2 = ipe.Segment(b1, b2)

    local temp1 = seg1:line():dir()
    local temp2 = seg2:line():dir()


    local theta1 = get_angle(temp1, dir)
    local theta2 = get_angle(temp2, dir)


    -- How do i make this work? Seems like which chain is which doesn't tend to be consistent, so it
    -- Needs to rely on orientation or be modified before the function to be consistent
    if temp1.x*dir.y - temp1.y*dir.x < 0 then
        theta1 = -theta1
    end
    if temp2.x*dir.y - temp2.y*dir.x > 0 then
        theta2 = -theta2
    end

    if math.abs(math.tan(theta1) + math.tan(theta2)) < 0.001 then
        if math.abs(gamma) < 0.001 then
            return 0
        end
        return area/gamma
    end

    local t1 = (-gamma + math.sqrt(gamma*gamma+2*(math.tan(theta1)+math.tan(theta2))*area))/(math.tan(theta1)+math.tan(theta2))
    local t2 = (-gamma - math.sqrt(gamma*gamma+2*(math.tan(theta1)+math.tan(theta2))*area))/(math.tan(theta1)+math.tan(theta2))

    if t1 < 0 then
        return t2
    else
        if t2 < 0 then
            return t1
        else
            return math.min(t1,t2)
        end
    end
end

-- Used for intersections
function removeDuplicates(tbl)
    local seen = {}
    local result = {}
    
    for _, value in ipairs(tbl) do
        if not seen[value] then
            seen[value] = true
            table.insert(result, value)
        end
    end
    
    return result
end

--Prints out a vector
function print_vector(vector, name, model)
    model:warning(name.." = (" .. vector.x .. ", " .. vector.y .. ")")
end


function runFloatingBodies(model)

    -- Stores the unmodified coordinates in ncoordinates
    local ncoordinates = get_pts_and_segment_selection(model)
    if ncoordinates == nil or #ncoordinates == 0 then
        model:warning("No/Not enough coordinates found, exiting")
        return
    end

    -- The polygon must be convex, if not, we exit
    if not isConvex(ncoordinates) then
        model:warning("The provided shape is not convex, exiting ipelet (If three adjacent points are on the same line, it breaks)")
        return
    end

    -- Takes in the desired amount of area, doesn't accept 0 or 100 since those would do nothing
    local delta = model:getString("Enter delta value (1-99, where x means x% of the total area)")
    delta = tonumber(delta)
    if delta == nil or delta < 1 or delta > 99 then
        model:warning("Invalid delta input")
        return
    end


    -- We don't check this one since we have a default value
    local showType = model:getString("Would you like the halfspace lines (Default) or a polygon of midpoints (1)")


    local midpoints = {}
    local target_area = calculate_polygon_area(ncoordinates) * delta/100


    -- Iterates over 1 degree, 2 degrees ... 359 degrees
    for i = 0, 359 do
        -- Creates a directional vector
        local dir = ipe.Vector(math.cos(i/180*math.pi), math.sin(i/180*math.pi))

        -- Sorts the coordinates based on dot product value to the direction vector, then puts the 1st node first in the table
        local points = get_closest(ncoordinates, dir, model)
        local coordinates = reorderTable(ncoordinates, points[1])


        -- Reorders it so that chain 1 is always clockwise of dir
        if (coordinates[1].x-coordinates[2].x)*dir.y - (coordinates[1].y-coordinates[2].y)*dir.x > (coordinates[1].x-coordinates[#coordinates].x)*dir.y - (coordinates[1].y-coordinates[#coordinates].y)*dir.x then
            reverseTable(coordinates)
            coordinates = reorderTable(coordinates, points[1])
        end

        -- Gets the chains, stored as {chain1, chain2}
        local chains = get_segments_sorted(coordinates, points,  model)

        -- Indexes for chain 1 and chain 2
        local c1 = 1
        local c2 = 1

        local chain1 = chains[1]
        local chain2 = chains[2]

        -- IMPORTANT
        -- Gamma updates at the end of each segment for the following segment
        local gamma = 0
        local total_area = 0

        -- They both start on the initial point, so we just skip that useless step (Breaks index - 1 otherwise)
        if chain1[2]^dir < chain2[2]^dir then
            c1 = c1 + 1
        else
            c2 = c2 + 1
        end

        -- While neither have gone too far (This really doesn't matter, just acts as a failsafe to prevent)
        -- Infinite looping if something breaks
        while c1 <= #chain1 and c2 <= #chain2 do

            --Vectors that show the direction of a segment from a1 or b1
            local a2
            local b2

            -- Gets the length of the next segment
            local h = get_dist_for_points(points[c1+c2-2], points[c1+c2-1], dir, model)

            -- If they are just about on the same line, we don't need to account for it. Issue with == due to floating point error
            if h > 0.00001 then

                -- We need to know which point is first, so we have two cases
                -- We modify the second point using temp slope instead of just taking the other value in order to flip it around
                -- This ensures angle calculation works
                if chain1[c1]^dir < chain2[c2]^dir then   
                    a2 = chain1[c1+1]
                    b2 = chain2[c2]
                    local temp_slope = get_slope(chain2[c2-1], chain2[c2])
                    b2 = ipe.Vector(b2.x + temp_slope.x, b2.y + temp_slope.y)
                else
                    b2 = chain2[c2+1]
                    a2 = chain1[c1]
                    local temp_slope = get_slope(chain1[c1-1], chain1[c1])
                    a2 = ipe.Vector(a2.x + temp_slope.x, a2.y + temp_slope.y)
                end

                -- Calculates the max area of the segment
                local temp_area = calc_area(chain1[c1], a2, chain2[c2], b2, dir, gamma, h, model)

                -- If it overshoots, we need to find the h that works
                if temp_area + total_area > target_area then

                    local h = find_h(chain1[c1], a2, chain2[c2], b2, dir, gamma, target_area-total_area, model)
                    
                    -- Takes the first point and advances along dir by h, so we can find the orthogonal line to it
                    local last_point
                    if chain1[c1]^dir < chain2[c2]^dir then
                        last_point = ipe.Vector(chain1[c1].x + dir.x*h, chain1[c1].y + dir.y*h)
                    else
                        last_point = ipe.Vector(chain2[c2].x + dir.x*h, chain2[c2].y + dir.y*h)
                    end

                    -- Finds the intersection points in the main shape
                    local temp_perp_line = get_perp_line(last_point, dir, model)
                    local intersect_points = get_intersection_points(coordinates, temp_perp_line, model)


                    -- This one is possible when a line goes through a vertex
                    if #intersect_points > 2 then
                        intersect_points = removeDuplicates(intersect_points)
                    end

                    -- Shouldn't trigger, but this prevents a crash
                    if #intersect_points < 2 then
                        model:warning("Didn't intersect. You shouldn't see this message")
                        break
                    end

                    -- If half space lines, draw them
                    if showType ~= "1" then
                        local start = intersect_points[1]
                        local finish = intersect_points[2]

                        -- Create the path between the two vectors
                        local segment = {type="segment", start, finish}
                        local shape = { type="curve", closed=false, segment}
                        local pathObj = ipe.Path(model.attributes, { shape })
                        
                        -- Draw the path
                        model:creation("create basic path", pathObj)
                    end

                    -- Adds the midpoint to a table
                    table.insert(midpoints, ipe.Vector((intersect_points[1].x + intersect_points[2].x)/2,(intersect_points[1].y + intersect_points[2].y)/2))
                    
                    break

                end

                -- Update gamma. I wish it was simpler, but if the next point is on the same line as the first point, we need
                -- To update the gamma there instead of at the other chain
                if chain1[c1]^dir < chain2[c2]^dir then   
                    local temp_point
                    if chain1[c1+1]^dir < chain2[c2]^dir then
                        temp_point = get_perp_line(chain1[c1+1], dir, model):intersects(ipe.Segment(chain2[c2], chain2[c2-1]):line())
                        gamma = euclidean_distance(temp_point, chain1[c1+1])
                    else
                        temp_point = get_perp_line(chain2[c2], dir, model):intersects(ipe.Segment(chain1[c1], chain1[c1+1]):line())
                        gamma = euclidean_distance(temp_point, chain2[c2])
                    end
                else
                    local temp_point
                    if chain2[c2+1]^dir < chain1[c1]^dir then
                        temp_point = get_perp_line(chain2[c2+1], dir, model):intersects(ipe.Segment(chain1[c1], chain1[c1-1]):line())
                        gamma = euclidean_distance(temp_point, chain2[c2+1])
                    else
                        temp_point = get_perp_line(chain1[c1], dir, model):intersects(ipe.Segment(chain2[c2], chain2[c2+1]):line())
                        gamma = euclidean_distance(temp_point, chain1[c1])
                    end

                end

                total_area = total_area + temp_area
            else
                -- If h is basically 0, we can just find the distance between those two points
                gamma = euclidean_distance(chain1[c1], chain2[c2])
            end

            


            -- Advancing on the chain (Basically, whichever segment is next we update)
            if c1 == #chain1 then
                c2 = c2 + 1
            else
                if c2 == #chain2 then
                    c1 = c1 + 1
                else
                    -- Floating point issues
                    if math.abs(chain1[c1]^dir - chain2[c2]^dir) < 0.001 then
                        if chain1[c1+1]^dir < chain2[c2+1]^dir then
                            c1 = c1 + 1
                        else 
                            c2 = c2+1
                        end
                    else
                        if chain1[c1]^dir < chain2[c2]^dir then
                            c1 = c1 + 1
                        else 
                            c2 = c2+1
                        end
                    end
                end
            end
            
        end
    end

    -- Midpoint polygon
    if showType == "1" then
        local closest_points = midpoints
        local start = closest_points[#closest_points]
        local finish
        local lines = {}
        for _, point in ipairs(closest_points) do
            finish = ipe.Vector(point.x, point.y)
            local segment = {type="segment", start, finish}
            local shape = { type="curve", closed=false, segment}
            table.insert(lines, shape)
            start = finish
        end
        local pathObj = ipe.Path(model.attributes, lines)
        model:creation("create basic path", pathObj)
    end

end

back to top

Software Heritage — Copyright (C) 2015–2026, The Software Heritage developers. License: GNU AGPLv3+.
The source code of Software Heritage itself is available on our development forge.
The source code files archived by Software Heritage are available under their own copyright and licenses.
Terms of use: Archive access, API— Content policy— Contact— JavaScript license information— Web API