Curve Point Split Script — Curvature Resets to 0.39 After Splitting

Moho allows users to write new tools and plugins. Discuss scripting ideas and problems here.

Moderators: Víctor Paredes, Belgarath, slowtiger

Post Reply
ilvmgct
Posts: 7
Joined: Fri Dec 02, 2022 8:56 pm

Curve Point Split Script — Curvature Resets to 0.39 After Splitting

Post by ilvmgct »

Hi everyone,

I’ve written a Moho menu script that splits a selected point on a curve into two independent endpoints. The script runs correctly: the user selects a point and executes the split from the menu.

Current approach:

Read the selected point’s position and curvature data via M_Curve:GetCurvature, GetWeight, and GetOffset (for both front and back sides);
Use M_Mesh:AddPoint to create a coincident point at the same position;
Use M_Mesh:DeleteEdge to remove the segment between the original point and the new point, breaking the curve;
Attempt to restore curvature data before and after deleting the edge.
The problem:

Regardless of the original curvature value, after splitting, both the original and new points have their curvature reset to 0.39, and the curve visibly changes shape at the split location. I have tried the following without success:

Restoring curvature via M_Curve:SetCurvature / SetWeight / SetOffset before and after DeleteEdge;
Using M_Curve:GetControlHandle / SetControlHandle to manipulate handles directly;
Setting correctBezierHandles = false in M_Mesh:AddPoint;
Setting the last parameter of M_Mesh:DeleteEdge to false;
Directly manipulating the five sub-channels of the “Point Curvature” animation channel;
Changing the order of curvature restoration (before deletion, after deletion, and both).
My question:

Is there a correct way to split a point on a curve while preserving its curvature? Or is this a limitation of the current scripting API? If it is a limitation, is there an officially recommended alternative (e.g., duplicating the layer and deleting points from copies)?

The plugin code is available upon request. Any guidance or suggestions would be greatly appreciated.

Thank you!

Code: Select all

-- ss_split_point.lua
-- Implements a true break at a point on a curve in Moho, preserving the shape by saving/restoring the full curvature sub-channels.
-- Installation location: User Documents/Moho Pro/scripts/tool/
-- Usage: Select a point with the "Select Points" tool, then activate this tool and click once on the canvas.

ScriptName = "SS_SplitPoint"
SS_SplitPoint = {}

function SS_SplitPoint:Name()
    return "Split Point (Real)"
end

function SS_SplitPoint:Version()
    return "7.0"
end

function SS_SplitPoint:Description()
    return "Break a curve at the selected point, preserving the full curvature sub-channels."
end

function SS_SplitPoint:IsEnabled(moho)
    if (moho.layer:LayerType() ~= MOHO.LT_VECTOR) then return false end
    if (moho:CountSelectedPoints() < 1) then return false end
    return true
end

---------------------------------------------------------------------
-- Core logic
---------------------------------------------------------------------
function SS_SplitPoint:OnMouseDown(moho, mouseEvent)
    local layer = moho:LayerAsVector(moho.layer)
    if (layer == nil) then return end
    local mesh = layer:Mesh()
    if (mesh == nil) then return end
    local frame = moho.frame

    -- 1. Get the first selected point
    local selPtID = -1
    local selList = MOHO.SelectedPointList(mesh)
    for i, pt in ipairs(selList) do
        selPtID = mesh:ClosestPoint(pt.fPos)
        break
    end
    if (selPtID < 0) then return end

    local selPoint = mesh:Point(selPtID)
    local posA = LM.Vector2:new_local()
    posA:Set(selPoint.fPos.x, selPoint.fPos.y)

    -- 2. Find the curve and segment that this point belongs to
    local targetCurveID, targetSegID = -1, -1
    for curveID = 0, mesh:CountCurves() - 1 do
        local curve = mesh:Curve(curveID)
        if (curve ~= nil) then
            for segID = 0, curve:CountSegments() - 1 do
                local p1 = LM.Vector2:new_local()
                local p2 = LM.Vector2:new_local()
                local p3 = LM.Vector2:new_local()
                local p4 = LM.Vector2:new_local()
                curve:GetControlPoints(segID, p1, p2, p3, p4, false)
                if (self:VecEqual(p1, posA) or self:VecEqual(p4, posA)) then
                    targetCurveID = curveID
                    targetSegID = segID
                    break
                end
            end
        end
        if (targetCurveID >= 0) then break end
    end

    if (targetCurveID < 0 or targetSegID < 0) then
        moho:UpdateUI()
        return
    end

    local curve = mesh:Curve(targetCurveID)
    if (curve == nil) then return end

    -- 3. Check whether this point is a curve endpoint (connected to only one segment)
    if (curve:CountSegments() <= 1) then
        return
    end

    -- 4. * Save the full curvature data of the original point (five sub-channels)
    local curvePtID_A = curve:PointID(selPoint)
    if (curvePtID_A < 0) then
        moho:UpdateUI()
        return
    end

    -- Save the curvature itself
    local savedCurvature = curve:GetCurvature(curvePtID_A, frame)
    -- Save the front side (near point a) weight and offset
    local savedWeightIn  = curve:GetWeight(curvePtID_A, frame, true)
    local savedOffsetIn  = curve:GetOffset(curvePtID_A, frame, true)
    -- Save the back side (near point e) weight and offset
    local savedWeightOut = curve:GetWeight(curvePtID_A, frame, false)
    local savedOffsetOut = curve:GetOffset(curvePtID_A, frame, false)

    -- 5. Add a new point B at the original position, disabling Bezier handle auto-correction
    mesh:AddPoint(posA, selPtID, targetSegID, false, frame, false)

    -- 6. Find the mesh ID of the newly added point B
    local newPtID = -1
    for i = 0, mesh:CountPoints() - 1 do
        local pt = mesh:Point(i)
        if (pt ~= nil and self:VecEqual(pt.fPos, posA) and i ~= selPtID) then
            newPtID = i
            break
        end
    end
    if (newPtID < 0) then
        moho:UpdateUI()
        return
    end

    -- 7. Find the segment between A and B and delete it
    local foundSeg = false
    for curveID = 0, mesh:CountCurves() - 1 do
        local c = mesh:Curve(curveID)
        if (c ~= nil) then
            for segID = 0, c:CountSegments() - 1 do
                local p1 = LM.Vector2:new_local()
                local p2 = LM.Vector2:new_local()
                local p3 = LM.Vector2:new_local()
                local p4 = LM.Vector2:new_local()
                c:GetControlPoints(segID, p1, p2, p3, p4, false)
                if (self:VecEqual(p1, posA) and self:VecEqual(p4, posA)) then
                    mesh:DeleteEdge(curveID, segID, frame, false)
                    foundSeg = true
                    break
                end
            end
        end
        if (foundSeg) then break end
    end

    -- 8. * After deleting the segment, restore the saved curvature data to the new point B
    local finalCurve = mesh:Curve(targetCurveID)
    if (finalCurve == nil) then
        moho:UpdateUI()
        return
    end

    local newPoint = mesh:Point(newPtID)
    if (newPoint == nil) then
        moho:UpdateUI()
        return
    end
    local curvePtID_B = finalCurve:PointID(newPoint)
    if (curvePtID_B < 0) then
        moho:UpdateUI()
        return
    end

    -- Restore curvature
    finalCurve:SetCurvature(curvePtID_B, savedCurvature, frame)
    -- Restore the front side (near point a) weight and offset
    finalCurve:SetWeight(curvePtID_B, savedWeightIn, frame, true)
    finalCurve:SetOffset(curvePtID_B, savedOffsetIn, frame, true)
    -- Restore the back side (near point e) weight and offset
    finalCurve:SetWeight(curvePtID_B, savedWeightOut, frame, false)
    finalCurve:SetOffset(curvePtID_B, savedOffsetOut, frame, false)

    moho:UpdateUI()
end

---------------------------------------------------------------------
-- Helper function: compare two Vector2 values for equality (tolerance 1e-4)
---------------------------------------------------------------------
function SS_SplitPoint:VecEqual(a, b)
    if (a == nil or b == nil) then return false end
    local dx = a.x - b.x
    local dy = a.y - b.y
    return (dx * dx + dy * dy) < 1e-8
end

---------------------------------------------------------------------
-- Standard tool functions
---------------------------------------------------------------------
function SS_SplitPoint:Run(moho) end
function SS_SplitPoint:DoLayout(moho, layout) end
function SS_SplitPoint:OnMouseMove(moho, mouseEvent) end
function SS_SplitPoint:OnMouseUp(moho, mouseEvent) end
function SS_SplitPoint:OnKeyDown(moho, keyEvent) end
function SS_SplitPoint:OnKeyUp(moho, keyEvent) end
function SS_SplitPoint:OnSetCursor(moho, mouseEvent)
    mouseEvent:SetCursor(MOHO.CROSSHAIR)
end
function SS_SplitPoint:OnDelete(moho) end
function SS_SplitPoint:OnScriptError(moho) end

return SS_SplitPoint
User avatar
hayasidist
Posts: 4014
Joined: Wed Feb 16, 2011 11:12 am
Location: Kent, England

Re: Curve Point Split Script — Curvature Resets to 0.39 After Splitting

Post by hayasidist »

First a polite note: the SS_ prefix indicates that the author is SimplSam. Could you pick the script prefix that is unique and helps to identify you. It's possible (I haven't tried to investigate) that you have other SS_ scripts and there may be function name clashes. More about this here: https://mohoscripting.com/

Then take a look at the code below. This is the factory tool for splitting a curve into equal segments. Note that the guts of the split logic (commented out here) are now in a library call - but the original code is still there for your reference.

obviously, you'll want to add the inserted point at a different coordinate -- modify the line local v = curve:PointOnSegment(segID, i / (self.splitCount + 1)) before deleting the newly created segment.

Hope that helps.

Code: Select all

-- **************************************************
-- Provide Moho with the name of this script object
-- **************************************************

ScriptName = "LM_SplitCurve"

-- **************************************************
-- General information about this script
-- **************************************************

LM_SplitCurve = {}

LM_SplitCurve.BASE_STR = 2460

function LM_SplitCurve:Name()
	return "Split Curve"
end

function LM_SplitCurve:Version()
	return "6.0"
end

function LM_SplitCurve:Description()
	return MOHO.Localize("/Scripts/Menu/SplitCurve/Description=Splits up selected curve segments by adding extra points.")
end

function LM_SplitCurve:Creator()
	return "Lost Marble LLC"
end

function LM_SplitCurve:UILabel()
	return(MOHO.Localize("/Scripts/Menu/SplitCurve/SplitCurve=Split Curve..."))
end

-- **************************************************
-- Recurring values
-- **************************************************

LM_SplitCurve.splitCount = 2

-- **************************************************
-- Split Curve dialog
-- **************************************************

local LM_SplitCurveDialog = {}

function LM_SplitCurveDialog:new(moho)
	local d = LM.GUI.SimpleDialog(MOHO.Localize("/Scripts/Menu/SplitCurve/Title=Split Curve"), LM_SplitCurveDialog)
	local l = d:GetLayout()

	d.moho = moho

	l:AddChild(LM.GUI.StaticText(MOHO.Localize("/Scripts/Menu/SplitCurve/Note1=Insert points into selected curve segments:")), LM.GUI.ALIGN_CENTER)
	l:PushH(LM.GUI.ALIGN_CENTER)
		l:AddChild(LM.GUI.StaticText(MOHO.Localize("/Scripts/Menu/SplitCurve/PointCount=Point count")))
		d.splitCount = LM.GUI.TextControl(0, "0000", 0, LM.GUI.FIELD_UINT)
		l:AddChild(d.splitCount)
	l:Pop()

	return d
end

function LM_SplitCurveDialog:UpdateWidgets()
	self.splitCount:SetValue(LM_SplitCurve.splitCount)
end

function LM_SplitCurveDialog:OnValidate()
	local b = true
	if (not self:Validate(self.splitCount, 1, 10)) then
		b = false
	end
	return b
end

function LM_SplitCurveDialog:OnOK()
	LM_SplitCurve.splitCount = self.splitCount:IntValue()
end

-- **************************************************
-- The guts of this script
-- **************************************************

function LM_SplitCurve:IsEnabled(moho)
	if (moho.layer:LayerType() ~= MOHO.LT_VECTOR) then
		return false
	end
	if (moho.layer:CurrentAction() ~= "") then
		return false -- creating new objects in the middle of an action can lead to unexpected results
	end
	return true
end

function LM_SplitCurve:Run(moho)
	local dlog = LM_SplitCurveDialog:new(moho)
	if (dlog:DoModal() == LM.GUI.MSG_CANCEL) then
		return
	end

	local mesh = moho:Mesh()
	if (mesh == nil) then
		return
	end

	moho.document:PrepUndo(moho.layer)
	moho.document:SetDirty()

	MOHO:SplitSelectedSegments(mesh, self.splitCount, moho.layerFrame)
--[[
	for curveID = 0, mesh:CountCurves() - 1 do
		local curve = mesh:Curve(curveID)
		local ptCount = curve:CountSegments() * self.splitCount
		local pts = {}

		for segID = curve:CountSegments() - 1, 0, -1 do
			if (curve:IsSegmentSelected(segID)) then
				for i = self.splitCount, 1, -1 do
					local v = curve:PointOnSegment(segID, i / (self.splitCount + 1))
					table.insert(pts, v)
				end
			end
		end

		local ptID = 1

		for segID = curve:CountSegments() - 1, 0, -1 do
			if (curve:IsSegmentSelected(segID)) then
				for i = 0, self.splitCount - 1 do
					mesh:AddPoint(pts[ptID], curveID, segID, moho.layerFrame)
					ptID = ptID + 1
				end
			end
		end
	end
]]
end
Post Reply