-- ************************************************** -- Provide Moho with the name of this script object -- ************************************************** ScriptName = "HS_Shape" -- ************************************************** -- General information about this script -- ************************************************** HS_Shape = {} -- HS_Shape.BASE_STR ---- No localisation is this tool (yet?!) -->> Live run **** vvv local HS_Test_harness = {} HS_Test_harness.tracePrint = function () end HS_Test_harness.diagnosePrint = function () end HS_Test_harness.diagnoseModifierKeys = function () end --<< ***** ^^^^^^ --[[]] function HS_Shape:Name() return "Draw shapes and curves" end function HS_Shape:Version() return "10.01" end function HS_Shape:IsBeginnerScript() return false end function HS_Shape:Description() return "Draw a shape (hold <shift> to constrain, <alt> and <ctrl> for other options)" end function HS_Shape:Creator() return "Hayasidist" end function HS_Shape:UILabel() return HS_Shape:Name() .. " Vers: " .. HS_Shape:Version() end function HS_Shape:ColorizeIcon() return true end --****************************** -- local utility subroutines -- ***************************** local thisUUT = "B" -- for the load customised tools portion ... local function getFunction(t, a) -- look for function t.a in the global table -- HS_Test_harness:diagnosePrint (thisUUT, t, a, type(_G[t][a])) return type(_G[t][a]) == "function" end local function getArg (n, ...) local arg = {...} return arg[n] end local HS_MaxPt = 3000 -- this is the maximum number of points that a subtool can draw -- in practice, much more than 1000 gives serious performance issues local function validatePtCt (n) local rtx = false if (type (n) == "number") and (n >= 2) and (n <= HS_MaxPt) and math.ceil (n) == n then rtx = true end return (rtx) end local function validateInt (val) if type(val) == "number" then val = math.floor (val) else val = 0 end return val end local function validateFloat (val) if type(val) ~= "number" then val = 0 end return val end local function validateBool (val) if type(val) ~= "boolean" then val = false end return val end local function validateString (val) if type(val) == "string" then val:gsub ("%C", "") -- strip out control characters else val = false end return val end local shapeFncs = { {"load", "LoadPrefs"}, {"save", "SavePrefs"}, {"reset", "ResetPrefs"}, {"mdn", "OnMouseDown"}, {"mmov", "OnMouseMoved"}, -- from here down, optional routine names are allowed {"about", "About"}, {"alt", {"Alt", "Alternative"}}, {"onSel", {"OnSelection", "OnStart"}}, {"onExit", {"OnMouseUp", "OnExit"}}, {"LabelUI", "Labels"}, {"bigN", {"BigN", "bigN", "Int1", "Int"}}, {"lilN", {"LittleN", "littleN", "Int2"}}, {"rat", {"Scale", "Float1", "Float"}}, {"but", {"Button", "Butt"}}, {"box", {"Box", "CheckBox"}} } --[=[ a fully populated template ident table local t = {ident="Ident", self=HS_ShapeIdent, icon="hs_filename", gKey="HS_ShapeIdent", mdn=self.OnMouseDown, mmov=self.OnMouseMoved, tip="any string", about=self.About, alt=self.Alt, onSel=self.OnSelection, onExit=self.OnExit, load=self.LoadPrefs, save=self.SavePrefs, reset=self.ResetPrefs, LabelUI=self.Labels, bigN=self.BigN, lilN=self.LittleN, rat=self.Scale, but=self.Button, box=self.CheckBox} ]=] local function populateFunctions (t) -- -- ** this takes a partial Identify table and fills in the functions from within the designated "self" -- for i = 1, #shapeFncs do t[shapeFncs[i][1]] = nil -- assume we won't find it if type(shapeFncs[i][2]) == "table" then for j = 1, #shapeFncs[i][2] do t[shapeFncs[i][1]] = _G[t.gKey][shapeFncs[i][2][j]] if t[shapeFncs[i][1]] and type(t[shapeFncs[i][1]]) == "function" then -- -->> possible that a later option is actually the required function?!?!? break end end else t[shapeFncs[i][1]] = _G[t.gKey][shapeFncs[i][2]] end end return t end local function buildIdentifyTable (name) -- -- ** this uses name:Shape() function to populate an Identify table -- local t = {ident=nil, self=_G[name], icon=nil, gKey=name, mdn=nil, mmov=nil, -- ** from here down, the functions are optional tip=nil, about=nil, alt= nil, onSel=nil, load=nil, save=nil, reset=nil, LabelUI=nil, bigN=nil, lilN=nil, rat=nil, but=nil, box=nil} local i, j local param = _G[name]:Shape() if type(param) == "table" then t.ident = param[1] t.tip = param[2] t.icon = param[3] else -- we have separate basic items t.ident = getArg(1, _G[name]:Shape()) t.tip = getArg(2, _G[name]:Shape()) t.icon = getArg(3, _G[name]:Shape()) end t = populateFunctions (t) return (t) end local function validateTool (t) -- -- ** check the contents of an Identify table for correct data types and self-consistency as far as possible -- local i, j local val, msg -- for validation / status messages local valMsg = {} local fatal = false local scan = true -- scan for functions needed local doInfoMsg = true -- for "info / warning" messages about a subtool (if false the only errors are reported -- useful for testing) -- ** ident val = validateString (t.ident) -- is the identity a string? if val then if #val < 3 then val = false end end if not val then HS_Test_harness:diagnosePrint (thisUUT, "bad ident", type(t.ident), t.ident) msg = "FATAL -- Bad identifier. Found data type " .. type(t.ident) .. " with value " .. tostring(t.ident) table.insert(valMsg, msg) fatal = true t.ident = "" else t.ident = val end -- ** tool tip local val = validateString (t.tip) -- and tooltip if not val then HS_Test_harness:diagnosePrint (thisUUT, "bad tip", type(t.tip), t.tip) if doInfoMsg then msg = "WARNING -- Tip is not of type " .. [["string". Type is ]] .. type(t.tip) .. " with value " .. tostring(t.tip) table.insert(valMsg, msg) end t.tip = "" else t.tip = val end -- ** icon file name local val = validateString (t.icon) if val then local invalFile = [==[[%\%/%:%?%"%<%>%|%.]]==] val = val:gsub ("^%s+", "") -- strip leading spaces if val:find (invalFile) then val = false end end if not val then HS_Test_harness:diagnosePrint (thisUUT, "bad icon", t.icon) msg = "FATAL -- Bad icon filename. Found data type " .. type(t.icon) .. " with value " .. tostring(t.icon) table.insert(valMsg, msg) t.icon = nil fatal = true else t.icon = val end -- ** global table entry local val = validateString (t.gKey) -- the string that is "self" if not val then HS_Test_harness:diagnosePrint (thisUUT, "bad tool table name", type(t.gKey), t.gKey) msg = "FATAL -- Bad tool table name. Found data type " .. type(t.gKey) .. " with value " .. tostring(t.gKey) table.insert(valMsg, msg) t.gKey = "Bad Tool Id" fatal = true else if t.self ~= _G[val] then HS_Test_harness:diagnosePrint (thisUUT, "tool table name inconsistent with self", t.gKey) msg = "FATAL -- Textual tool table name inconsistemt with internal name " .. t.gKey table.insert(valMsg, msg) fatal = true end t.gKey = val end -- ** functions are actually functions for i = 1, #shapeFncs do if t[shapeFncs[i][1]] and type(t[shapeFncs[i][1]]) ~= "function" then HS_Test_harness:diagnosePrint (thisUUT, "Function expected, got", type(t[shapeFncs[i][1]]), "for", t[shapeFncs[i][1]]) msg = "ERROR -- " .. t.gKey .. "." .. t[shapeFncs[i][1]] .. " is of type " .. type(t[shapeFncs[i][1]]) .. ". Function expected." table.insert(valMsg, msg) t[shapeFncs[i][1]] = nil end end -- ** consistent set of prefs local prefFncs = {"reset", "save", "load"} local prefCt = 0 local fnc = "" msg = "; but not " for i = 1, #prefFncs do fnc = "*error*" for j = 1, #shapeFncs do if prefFncs[i] == shapeFncs[j][1] then fnc = shapeFncs[j][2] break end end if t[prefFncs[i]] then prefCt = prefCt + 1 msg = fnc .. " " .. msg else msg = msg .. " " .. fnc end end if not (prefCt == 0 or prefCt == #prefFncs) then msg = "WARNING -- " .. t.ident .. " Preferences has " .. msg .. "." HS_Test_harness:diagnosePrint (thisUUT, msg) if doInfoMsg then table.insert(valMsg, msg) end end -- ** consistent set of mouse local mouseFncs = {"mmov", "mdn"} local mouseCt = 0 msg = " but not " for i = 1, #mouseFncs do fnc = "*error*" for j = 1, #shapeFncs do if mouseFncs[i] == shapeFncs[j][1] then fnc = shapeFncs[j][2] break end end if t[mouseFncs[i]] then mouseCt = mouseCt + 1 msg = fnc .. " " .. msg else msg = msg .. " " .. fnc end end if not (mouseCt == 0 or mouseCt == #mouseFncs) then msg = "FATAL -- " .. t.ident .. " Has " .. msg .. "." table.insert(valMsg, msg) fatal = true end -- ** help / about? if not t.about then msg = "Information. " .. t.ident .. " Has no " .. [["About"]] .. " function." HS_Test_harness:diagnosePrint (thisUUT, msg) t.about = HS_Shape.About_Void if doInfoMsg then table.insert(valMsg, msg) end end --[=[ -- -- vvvv DIAGNOSTIC =========== HS_Test_harness:tracePrint (thisUUT, "Validation finished. Fatal?", fatal, "Messages", #valMsg) for i = 1, #valMsg do HS_Test_harness:diagnosePrint (thisUUT, valMsg[i]) end -- -- ^^^ ========================== ]=] if #valMsg > 0 then msg = "" for i = 1, #valMsg do msg = msg .. valMsg[i] .. "\n" end HS_Shape:ToolError (t.gKey, "Validation error(s)", msg) end if fatal then return nil else return t end end --****************************** -- prefs and load custom subtools -- ***************************** function HS_Shape:LoadPrefs(prefs) local i, j local key, value, name, goodTool if HS_Test_harness then HS_Test_harness:tracePrint (thisUUT, self:Name(), self:Version(), "Load prefs") end -- -- ** check the internal tools -- for i = 1, #HS_Shape.subToolDispatch - 1 do goodTool = validateTool (populateFunctions (HS_Shape.subToolDispatch[i].self:Identify())) if not goodTool then HS_Test_harness:diagnosePrint (thisUUT, name, "validation errors") HS_Shape.subToolDispatch[i] = HS_Shape.subToolDispatch[#HS_Shape.subToolDispatch] -- Void else HS_Shape.subToolDispatch[i] = goodTool end end -- -- ** look for inserts. -- -- A user-shape unit is structured similarly to a tool script but must also have the script:Shape() function to provide basic info -- -- local key, value, name, goodTool -- name = nil for key,value in pairs(_G) do if type(_G[key]) == "table" then if getFunction(key, "Shape") then -- HS_Test_harness:diagnosePrint (thisUUT, key, "candidate plug-in") goodTool = validateTool (buildIdentifyTable (key), key) if goodTool then table.insert (HS_Shape.subToolDispatch, #HS_Shape.subToolDispatch, goodTool) end end end end -- -- ** Actually load preferences -- -- ** System self.autoFill = prefs:GetBool("HS_Shape.autoFill", true) self.autoOutline = prefs:GetBool("HS_Shape.autoOutline", true) self.drawOn0 = prefs:GetBool("HS_Shape.drawOn0", true) -- self.extenderOpt = prefs:GetInt("HS_Shape.extenderOpt", 0) self.extenderOpt = 0 -- always start with basic options self.shapeType = prefs:GetString("HS_Shape.shapeType", "") -- will default to "first" (see below) because "" won't be found... self.toolSeqA = prefs:GetString("HS_Shape.toolSeqA", "") -- default is "" which wil lead to "dispatch table order" -- HS_Test_harness:diagnosePrint (thisUUT, "toolSeqA is", self.toolSeqA, #self.toolSeqA) if #self.toolSeqA < 3 then -- -- if no stored SeqA set up default mapping of tools in the toolbar = order in the dispatch table (but not the "void") and set SeqA accordingly -- -- HS_Test_harness:diagnosePrint (thisUUT, "subTool dispatch entries", #self.subToolDispatch) local s = "" for i = 1, #self.subToolDispatch - 1 do HS_Shape.toolSeq[i] = i s = s .. "[" .. self.subToolDispatch[i].ident .. "], " end self.toolSeqA = "{" .. s:sub(1, #s-2) .. "}" -- HS_Test_harness:diagnosePrint (thisUUT, "toolSeqA default is", self.toolSeqA) else -- -- map the tools to the list in seqA -- local sTrim, key, ix, toolNum, found, dup sTrim = self.toolSeqA:match("%b{}") local toolIgnored = -1 local toolUsed = 1 local toolNew = 0 usedTool = {} -- keep check on what we've used for i = 1, #self.subToolDispatch - 1 do usedTool[i] = toolNew -- assume they're new end toolNum = 0 for key in sTrim:gmatch("%b[]") do key = key:sub(2, #key-1) found = false dup = false -- HS_Test_harness:diagnosePrint (thisUUT, "seeking", key) for j = 1, #self.subToolDispatch - 1 do -- ignore the "Void" end if self.subToolDispatch[j].ident == key then if usedTool[j] == toolUsed then -- HS_Test_harness:diagnosePrint (thisUUT, key, "(tool number".. j ..") alreday in use") dup = true else toolNum = toolNum + 1 self.toolSeq[toolNum] = j usedTool[j] = toolUsed found = true -- HS_Test_harness:diagnosePrint (thisUUT, key, "is tool number", j) end break end end if not found and not dup then -- HS_Test_harness:diagnosePrint (thisUUT, key, "is not a known tool") -- and remove from seqA HS_Shape:ToolChangeAlert(key, "no longer installed.") elseif dup then -- HS_Test_harness:diagnosePrint (thisUUT, key, "duplicated") -- and remove from seqA HS_Shape:ToolChangeAlert(key, "duplicated in tool list", "Duplicate removed.") end end -- HS_Test_harness:diagnosePrint (thisUUT, "Found", toolNum, "tools", "max is", #self.subToolDispatch - 1) -- -- now look for new tools -->> if tool not in SeqA but in Dispatch >> will happen with newly recognised custom subtool?! -- sTrim = self.toolSeqA:match("%b{}", #sTrim) -- look for a list of ignored tools if not sTrim then sTrim = "{}" end -- HS_Test_harness:diagnosePrint (thisUUT, "unused tools", sTrim) for key in sTrim:gmatch("%b[]") do key = key:sub(2, #key-1) found = false dup = false -->>>what about this????? -- HS_Test_harness:diagnosePrint (thisUUT, "seeking", key, "unused") for j = 1, #self.subToolDispatch - 1 do -- ignore the "Void" end if self.subToolDispatch[j].ident == key then found = true -- HS_Test_harness:diagnosePrint (thisUUT, key, "is tool number", j, "and is in the ignored list") usedTool[j] = toolIgnored break end end end -- -- here we've found everything in the "used" and "ignored" lists, so anything still unused is new -- for j = 1, #usedTool do if usedTool[j] == toolNew then usedTool[j] = toolUsed toolNum = toolNum + 1 self.toolSeq[toolNum] = j -- HS_Test_harness:diagnosePrint (thisUUT, self.subToolDispatch[j].ident, "is new tool") -- add to UI HS_Shape:ToolChangeAlert(self.subToolDispatch[j].ident, "installed as a new subtool.") end end -- HS_Test_harness:diagnosePrint (thisUUT, "Now found", toolNum, "tools", "max is", #self.subToolDispatch - 1) -- -- rebuild SeqA -- -- installed tools local s = "" for i = 1, #HS_Shape.toolSeq do s = s .. "[" .. self.subToolDispatch[HS_Shape.toolSeq[i]].ident .. "], " end self.toolSeqA = "{" .. s:sub(1, #s-2) .. "}" -- used tools -- ignored tools s = "" for i = 1, #usedTool do if usedTool[i] == toolIgnored then s = s .. "[" .. self.subToolDispatch[i].ident .. "], " end end if #s < 3 then s = ", {}" else s = ", {" .. s:sub(1, #s-2) .. "}" end -- concatenate self.toolSeqA = self.toolSeqA .. s -- HS_Test_harness:diagnosePrint (thisUUT, "toolSeqA reset to", self.toolSeqA) end -- ** sub tools -- load all prefs irrespective of the button order or presence in the UI. for i = 1, #self.subToolDispatch do if self.subToolDispatch[i].load then self.subToolDispatch[i].load(self.subToolDispatch[i].self, prefs) end end -- find "last used tool" in tool seq and reset self.buttonNum = self.First -- default if not found for i = 1, #HS_Shape.toolSeq do if self.subToolDispatch[HS_Shape.toolSeq[i]].ident == self.shapeType then self.buttonNum = i + self.First - 1 -- HS_Test_harness:diagnosePrint (thisUUT, "Last used was", self.shapeType, "now button number", i, "message number", self.buttonNum) break end end end -- -- ** Save preferences -- function HS_Shape:SavePrefs(prefs) -- ** System prefs:SetBool("HS_Shape.autoFill", self.autoFill) prefs:SetBool("HS_Shape.autoOutline", self.autoOutline) prefs:SetBool("HS_Shape.drawOn0", self.drawOn0) -- prefs:SetInt("HS_Shape.extenderOpt", self.extenderOpt) prefs:SetString("HS_Shape.shapeType", self.shapeType) prefs:SetString("HS_Shape.toolSeqA", self.toolSeqA) -- ** sub tools -- save all irrespective ... for i = 1, #self.subToolDispatch do if self.subToolDispatch[i].save then self.subToolDispatch[i].save(self.subToolDispatch[i].self, prefs) end end end -- -- ** Reset prefs -- function HS_Shape:ResetPrefs() -- HS_Test_harness:tracePrint (thisUUT, "Reset Prefs") -- ** System self.autoFill = true self.autoOutline = true self.drawOn0 = true self.extenderOpt = 0 self.shapeType = self.subToolDispatch[1].ident self.toolSeqA = "" -- ** sub tools -- reset all irrespetive ... for i = 1, #self.subToolDispatch do if self.subToolDispatch[i].reset then self.subToolDispatch[i].reset(self.subToolDispatch[i].self) end end end -- ************************************************** -- Recurring values -- -- ************************************************** local HS_curvC = 4/(3*math.sqrt(2)) -- factor to calc curvatures local HS_roundcorner = 0.15 local HS_circleCorner = 0.390524 -- recalculated (for a 4-point circle) -- was local HS_circleCorner = 0.391379 local HS_maxCurv = 0.666667 local HS_segment = math.rad(180) -- semicircle local MohoVers = 0 -- for mouseup shape creation local shapePoints = {} -- for diagnosis local thisUUT = "D" -- for when (timeline) a shape is created local drawingFrame = 0 -- local keyingFrame = 0 -- for UI data input / validation local bigNmax = 72 -- any number bigger than min!! local wheelMax = 10 -- maximum that a scroll wheel can add local gridMax = 48 -- limiter rows and cols in grid local bigNmed = 24 -- limiter for sides on polygon and concentrics local bigNmin = 4 -- can increase but not decrease from 4 for ovals... local bigNText = "" local littleNText = "" local floatText = "" local buttText = "" local checkText = "" local bigN, littleN, rat, box -- from the UI HS_Shape.toolSeq = {} -- mappping of buttons to tool dispatch List -- if this changes, set active subtool to 1 (unless it's in the new list??) HS_Shape.toolSeqA = "{}" -- the alpha string of button sequence in the tool bar -- ************************************************************ -- Glaobals for use in the UI -- the values here are irrelevant -- -- ************************************************************ HS_Shape.autoFill = true HS_Shape.autoOutline = true HS_Shape.extenderOpt = 0 HS_Shape.shapeType = "" HS_Shape.buttonNum = HS_Shape.First HS_Shape.drawOn0 = true -- ************************************************** -- Drawing controls for the current shape -- -- ************************************************** HS_Shape.startVec = LM.Vector2:new_local() HS_Shape.endVec = LM.Vector2:new_local() HS_Shape.drawnPoints = 0 local FakeMouse = {drawingVec = LM.Vector2:new_local(), drawingStartVec = LM.Vector2:new_local(), ctrlKey = false, shiftKey = false, altKey = false} -- for Edit mode checks: HS_Shape.lastItem = "" HS_Shape.lastUUID = "" HS_Shape.pointCt = 0 HS_Shape.editMode = false --****************************** -- global utility subroutines -- ***************************** -- -- ** default "no help" -- function HS_Shape:About_Void() return [==[ No "help" / "about" function for this tool. ]==] end -- -- ** disable all UI data fields -- function HS_Shape:Labels_Void() HS_Shape.bigNIn:Enable(false) -- "big N" input ... HS_Shape.bigNIn:SetValue(nil) HS_Shape.bigNIn:SetWheelInc(1) bigNText = "" HS_Shape.littleNIn:Enable(false) -- "little N" input ... HS_Shape.littleNIn:SetValue(nil) HS_Shape.littleNIn:SetWheelInc(1) littleNText = "" HS_Shape.scaleIn:Enable(false) -- "scale factor" input ... HS_Shape.scaleIn:SetValue(nil) HS_Shape.scaleIn:SetWheelInc(0.01) floatText = "" HS_Shape.optionButton:Enable(false) -- "options" input ... buttText = "" HS_Shape.checkIn:Enable(false) -- checkbox input ... HS_Shape.checkIn:SetValue(nil) checkText = "" end -- -- ** enable data inputs -- function HS_Shape:SetInt1 (label, value, inc) bigNText = validateString(label) or "Integer 1" value = validateInt(value) HS_Shape.bigNIn:SetValue(value) if inc then inc = LM.Clamp(validateInt(inc), 1, wheelMax) else inc = 1 end HS_Shape.bigNIn:SetWheelInc(inc) HS_Shape.bigNIn:Enable(true) end function HS_Shape:SetInt2 (label, value, inc) littleNText = validateString(label) or "Integer 2" value = validateInt(value) HS_Shape.littleNIn:SetValue(value) if inc then inc = LM.Clamp(validateInt(inc), 1, wheelMax) else inc = 1 end HS_Shape.littleNIn:SetWheelInc(inc) HS_Shape.littleNIn:Enable(true) end function HS_Shape:SetFloat (label, value, inc) floatText = validateString(label) or "Float 1" if inc then inc = LM.Clamp(validateFloat(inc), 0.01, wheelMax) else inc = 0.01 end value = validateFloat(value) --?? make multiple of inc? HS_Shape.scaleIn:SetValue(value) HS_Shape.scaleIn:SetWheelInc(inc) HS_Shape.scaleIn:Enable(true) end function HS_Shape:SetButton (label) buttText = validateString(label) or "Button" HS_Shape.optionButton:Enable(true) end function HS_Shape:SetCheckBox (label, value) checkText = validateString(label) or "CheckBox" value = validateBool(value) HS_Shape.checkIn:SetValue(value) HS_Shape.checkIn:Enable(true) end -- --** Set the drawing vector to snap to grid or not -- function HS_Shape:DrawingPos (moho, mouseEvent) vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end return vec end -- --** Set the drawing bounds -- function HS_Shape:DrawingBounds (moho, mouseEvent, shift, alt) -- shift / alt = false or true to override the mouseEvent values vec1 = LM.Vector2:new_local() vec1:Set(mouseEvent.drawingStartVec) if (moho.gridOn) then moho:SnapToGrid(vec) end vec2 = LM.Vector2:new_local() vec2:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec2) end local doSquare if shift == nil then doSquare = mouseEvent.shiftKey else doSquare = shift end if doSquare then -- constrain width = height if (vec2.x > vec1.x) then if (vec2.y > vec1.y) then vec2.y = vec1.y + (vec2.x - vec1.x) else vec2.y = vec1.y - (vec2.x - vec1.x) end else if (vec2.y > vec1.y) then vec2.y = vec1.y - (vec2.x - vec1.x) else vec2.y = vec1.y + (vec2.x - vec1.x) end end end local doCentre if alt == nil then doCentre = mouseEvent.altKey else doCentre = alt end if doCentre then -- draw from center point vec1.x = vec1.x - (vec2.x - vec1.x) vec1.y = vec1.y - (vec2.y - vec1.y) end return vec1, vec2 end -- -- ** make a closed path of points -- function HS_Shape:MakeLoop (moho, vec, drawingFrame, points, curv, doShape) local shapeID = -1 local shape local mesh = moho:DrawingMesh() local n = mesh:CountPoints() local i mesh:AddLonePoint(vec, drawingFrame) for i = 0, points-1 do mesh:AppendPoint(vec, drawingFrame) end -- add the points for the curve plus "one more" because welding loses the last point mesh:WeldPoints(n + points, n, drawingFrame) for i = 0, points-1 do mesh:Point(n+i):SetCurvature(curv, drawingFrame) end if doShape == nil or doShape then mesh:SelectConnected() if (HS_Shape.autoFill) then shapeID = moho:CreateShape(true, false, drawingFrame) if (shapeID >= 0) then if (not HS_Shape.autoOutline) then mesh:Shape(shapeID).fHasOutline = false end end elseif (HS_Shape.autoOutline) then shapeID = moho:CreateShape(false, false, drawingFrame) end end -- shape = mesh:Shape(shapeID) if shapeID > -1 then shape = mesh:Shape(shapeID) else shape = nil end if shape and doShape then shape.fSelected = true end return n, shape end -- -- ** make an open path of points -- -- startPt = HS_Shape:MakeLine(moho, startVec, drawingFrame, self.Points, curv) -- startPt = HS_Shape:MakeLine (moho, vec, drawingFrame, drawnPoints, HS_circleCorner) -->> improve curvature in MouseMoved function HS_Shape:MakeLine (moho, vec, drawingFrame, points, curv, doShape) local mesh = moho:DrawingMesh() local n = mesh:CountPoints() local shapeID = -1 mesh:AddLonePoint(vec, drawingFrame) for i = 0, points - 2 do mesh:AppendPoint(vec, drawingFrame) end for i = 0, points - 1 do mesh:Point(n + i):SetCurvature(curv, drawingFrame) end if doShape == nil or doShape then mesh:SelectConnected() if (HS_Shape.autoOutline) then shapeID = moho:CreateShape(false, false, drawingFrame) end end if shapeID > -1 then shape = mesh:Shape(shapeID) else shape = nil end if shape and doShape then shape.fSelected = true end return n, shape end -- -- ** Alerts -- function HS_Shape:BadToolAlert(name, toolId, text, data) -- >> extend to have the desired data type? (this is only for bad numbers) local line1 = "Subtool " .. name .. "\nCritical error." local t = type(data) local subText = "" if t == "nil" then subText = "Expected data, got nil" elseif t == "number" then subText = "Invalid number " .. tostring(data) else subText = "Expected number, got type " .. t .. " (" .. tostring(data) .. ")" end -- HS_Test_harness:tracePrint (thisUUT, "Alert", name, text, subText) subText = "\n" .. subText LM.GUI.Alert(LM.GUI.ALERT_WARNING, line1, text, subText, "OK") self.shapeButtons[toolId]:Enable(false) end function HS_Shape:ToolChangeAlert(name, text, sub) local line1 = "Subtool " .. name .. " " .. text local subText = sub or "" -- HS_Test_harness:tracePrint (thisUUT, "Alert", name, text, subText) LM.GUI.Alert(LM.GUI.ALERT_INFO, line1, subText, "", "OK") end function HS_Shape:AboutTool (name, text) text = text or HS_Shape:About_Void() local line1 = "About " .. name .. string.rep (" ", 10) -- lose all \n at the front text = text:gsub ("^%c+", "") LM.GUI.Alert(LM.GUI.ALERT_INFO, line1, text, "", "OK", nil, nil) end function HS_Shape:ToolError (name, reason, text) local line1 = "Subtool " .. name .. string.rep (" ", 10) .. "\n" .. reason LM.GUI.Alert(LM.GUI.ALERT_WARNING, line1, text, "", "OK") end -- ************************************************** -- The guts of this script -- ************************************************** -- -- relevant / enabled?? -- function HS_Shape:IsEnabled(moho) local mesh = moho:DrawingMesh() if (mesh == nil) then return false end if (moho.drawingLayer:CurrentAction() ~= "") then return false -- creating new objects in the middle of an action can lead to unexpected results end return true end function HS_Shape:IsRelevant(moho) local rtx = false local vectorLayer = moho:LayerAsVector(moho.drawingLayer) if type (MOHO.ScriptInterface.AppVersion) == "function" then MohoVers = tonumber((string.gsub(moho:AppVersion(), "^(%d+)(%.%d+)(%..+)", "%1%2"))) else MohoVers = 0 end if MohoVers < 12 then rtx = false elseif (moho:DisableDrawingTools()) then rtx = false elseif (vectorLayer) then rtx = true end return rtx end --********************************************************************************************************* MOUSE: MAIN SWITCH -- mouse actions --***************************** function HS_Shape:OnMouseDown(moho, mouseEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return end moho.document:PrepUndo(moho.drawingLayer) moho.document:SetDirty() -- -- clear the mouse up shape table -- local i, j, k for j in pairs (shapePoints) do shapePoints[j] = nil end -- -- check if edit mode (when skip to "mouse move") or create mode (when dispatch) -- -- HS_Test_harness:diagnosePrint (thisUUT, "Last UUID / now", self.lastUUID, moho.drawingLayer:UUID()) -- HS_Test_harness:diagnosePrint (thisUUT, "point counts then / now", self.pointCt, mesh:CountPoints()) if mouseEvent.ctrlKey and (self.drawnPoints > 0) and (self.pointCt == mesh:CountPoints()) and (moho:CountSelectedPoints() == self.drawnPoints) and (self.startVec.x ~= nil) and (self.lastItem == self.shapeType) and (self.lastUUID == moho.drawingLayer:UUID()) then -- -- if control key and the right number of points etc, then edit the active selection -- self.editMode = true else -- -- if not control on initial mouse down then draw new -- self.editMode = false if self.drawOn0 then drawingFrame = 0 -- keyingFrame = 0 else drawingFrame = moho.drawingLayerFrame -- keyingFrame = moho.drawingFrame end mesh:SelectNone() self.drawnPoints = 0 i = 0 if (self.buttonNum >= self.First) and (self.buttonNum <= self.Last) then i = self.buttonNum - self.First + 1 if self.subToolDispatch[HS_Shape.toolSeq[i]].mdn then -- HS_Test_harness:tracePrint (thisUUT, "Mouse down with shape type", i, type(self.subToolDispatch[HS_Shape.toolSeq[i]].mdn)) j = self.subToolDispatch[HS_Shape.toolSeq[i]].mdn(self.subToolDispatch[HS_Shape.toolSeq[i]].self, moho, mouseEvent, mesh, drawingFrame) if validatePtCt (j) then -- save stuff for edit / mouse moved validation self.lastUUID = moho.drawingLayer:UUID() -- this layer drawn on ... self.pointCt = mesh:CountPoints() -- how many points now self.drawnPoints = j -- and how many of those we just drew for k = 1, self.drawnPoints do mesh:Point(self.pointCt - k).fSelected = true -- select everything we just drew end -- elseif j == nil then -- not using mouse to draw?! else -- HS_Test_harness:diagnosePrint (thisUUT, self.subToolDispatch[HS_Shape.toolSeq[i]].ident, "not supplied a valid drawn point count") self:BadToolAlert(self.subToolDispatch[HS_Shape.toolSeq[i]].ident, i, "Bad drawn point count", j) end end end mouseEvent.view:DrawMe() end end function HS_Shape:OnMouseMoved(moho, mouseEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return end if self.editMode then mouseEvent.drawingStartVec:Set(FakeMouse.drawingStartVec) -- keep the original initial mouse down vec --[[ -- >> is it possible to determine what the mouse start vec should be if the selection has been moved / transformed???? local min = LM.Vector2:new_local() local max = LM.Vector2:new_local() mesh:SelectedBounds(min, max) ]] mouseEvent.ctrlKey = FakeMouse.ctrlKey -- and the last modifer keys mouseEvent.shiftKey = FakeMouse.shiftKey mouseEvent.altKey = FakeMouse.altKey end local i = 0 if (self.buttonNum >= self.First) and (self.buttonNum <= self.Last) then i = self.buttonNum - self.First + 1 if self.subToolDispatch[HS_Shape.toolSeq[i]].mmov then -- HS_Test_harness:tracePrint (thisUUT, "Mouse moved with shape type", i, type(self.subToolDispatch[HS_Shape.toolSeq[i]].mmov)) self.subToolDispatch[HS_Shape.toolSeq[i]].mmov(self.subToolDispatch[HS_Shape.toolSeq[i]].self, moho, mouseEvent, mesh, drawingFrame, self.drawnPoints) end end -- save the modifier keys for edit FakeMouse.ctrlKey = mouseEvent.ctrlKey FakeMouse.shiftKey = mouseEvent.shiftKey FakeMouse.altKey = mouseEvent.altKey if self.drawOn0 then moho:AddPointKeyframe(drawingFrame) else moho:AddPointKeyframe(moho.drawingFrame) end mouseEvent.view:DrawMe() end function HS_Shape:OnMouseUp(moho, mouseEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return end -- -- create shapes if required - routines can place lists of point numbers in a table ... -- local i, j, k for j in pairs (shapePoints) do mesh:SelectNone() for k = 1, #shapePoints[j] do mesh:Point(shapePoints[j][k]).fSelected = true end if (self.autoFill) then local shapeID = moho:CreateShape(true, false, drawingFrame) if (shapeID >= 0) then if (not self.autoOutline) then mesh:Shape(shapeID).fHasOutline = false end end elseif (self.autoOutline) then moho:CreateShape(false, false, drawingFrame) end end -- mesh:SelectConnected() -- << may need to review this and save/restore selections if non-connected shapes are to be created -- << ?? concentric circs are not connected but edit works??? mesh:SelectNone() for i = 1, self.drawnPoints do mesh:Point(self.pointCt - i).fSelected = true -- select everything we just drew end -- HS_Test_harness:diagnosePrint (thisUUT, "Mouse up -- Draw on 0", self.drawOn0, "Drawing frame", drawingFrame, "Moho DF", moho.drawingFrame) local i = 0 if (self.buttonNum >= self.First) and (self.buttonNum <= self.Last) then i = self.buttonNum - self.First + 1 if self.subToolDispatch[HS_Shape.toolSeq[i]].onExit then -- HS_Test_harness:tracePrint (thisUUT, "Mouse up with shape type", i, type(self.subToolDispatch[HS_Shape.toolSeq[i]].onExit)) self.subToolDispatch[HS_Shape.toolSeq[i]].onExit(self.subToolDispatch[HS_Shape.toolSeq[i]].self, moho, mouseEvent, mesh, drawingFrame, self.drawnPoints) end end if self.drawOn0 then moho:AddPointKeyframe(drawingFrame) moho:NewKeyframe(CHANNEL_POINT) moho:NewKeyframe(CHANNEL_CURVE) else moho:AddPointKeyframe(moho.drawingFrame) moho:NewKeyframe(CHANNEL_POINT) moho:NewKeyframe(CHANNEL_CURVE) end moho:UpdateSelectedChannels() mouseEvent.view:DrawMe() moho:UpdateUI() -- -- ** set up for possible edit -- -- self.drawnPoints is set in mouse down self.startVec, self.endVec = HS_Shape:DrawingBounds (moho, mouseEvent, false, false) self.lastItem = self.shapeType FakeMouse.drawingVec:Set(self.endVec) FakeMouse.drawingStartVec:Set(self.startVec) -- HS_Test_harness:diagnosePrint (thisUUT, "Mouse up - fake mouse", {"start vec", FakeMouse.drawingStartVec}, {"end vec", FakeMouse.drawingVec}) -- HS_Test_harness:diagnosePrint (thisUUT,HS_Test_harness:diagnoseModifierKeys (FakeMouse.ctrlKey, FakeMouse.shiftKey, FakeMouse.altKey)) end local function OnKeyDownDelete (moho, keyEvent) local mesh = moho:DrawingMesh() if (mesh == nil) then return false end local deleted = false if (HS_Shape.drawnPoints > 0) -- make as sure as we can, it's one of ours... and (HS_Shape.pointCt == mesh:CountPoints()) and (HS_Shape.drawnPoints == moho:CountSelectedPoints()) and (HS_Shape.lastUUID == moho.drawingLayer:UUID()) then -- HS_Test_harness:diagnosePrint (thisUUT, "Delete accepted") moho.document:PrepUndo(moho.drawingLayer) moho.document:SetDirty() mesh:DeleteSelectedPoints() deleted = true end return deleted end function HS_Shape:OnKeyDown(moho, keyEvent) local keyDispatch = { {LM.GUI.KEY_DELETE, OnKeyDownDelete}, {LM.GUI.KEY_BACKSPACE, OnKeyDownDelete} } local updateView = false if keyEvent.ctrlKey then -- HS_Test_harness:diagnosePrint (thisUUT, "using factory point tool") LM_SelectPoints:OnKeyDown(moho, keyEvent) return end local i for i = 1, #keyDispatch do if keyEvent.keyCode == keyDispatch[i][1] then -- HS_Test_harness:diagnosePrint (thisUUT, "found key entry", i) updateView = keyDispatch[i][2] (moho, keyEvent) break end end if updateView then -- HS_Test_harness:diagnosePrint (thisUUT, "updating view") moho:UpdateSelectedChannels() keyEvent.view:DrawMe() end end -- ************************************************** -- **************************************************************************************************************************** SECTOR -- ************************************************** HS_ShapeSector = {} HS_ShapeSector.Points = 21 local startVec = LM.Vector2:new_local() local startPt function HS_ShapeSector:Identify() local t = {ident="Sector", tip="Sector: Ctrl: make semi-circle or less. Shift: make exact quadrant or semi-circle. Alt: start horizontal.", icon="hs_sector", self=HS_ShapeSector, gKey="HS_ShapeSector"} return t end function HS_ShapeSector:About() local body = [==[ This tool creates a sector of up to (but excluding) a full circle. Ctrl constrains to a semi-circle. Shift constrains the sector to quadrant or semi-circle; Alt moves the initial radius from vertical to horizontal. One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeSector:LoadPrefs(prefs) self.Points = prefs:GetInt("HS_ShapeSector.Points", 21) end function HS_ShapeSector:SavePrefs(prefs) prefs:SetInt("HS_ShapeSector.Points", self.Points) end function HS_ShapeSector:ResetPrefs() self.Points = 21 end function HS_ShapeSector:Labels () HS_Shape:SetInt1 ("Points in Sector", self.Points) end function HS_ShapeSector:BigN (moho, view, value) self.Points = LM.Clamp(value, bigNmin, bigNmax) return (self.Points) end -- === function HS_ShapeSector:OnMouseDown (moho, mouseEvent, mesh, drawingFrame) local points = HS_ShapeSector.Points local curv = HS_circleCorner startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLoop(moho, startVec, drawingFrame, points, curv) mesh:Point(startPt):SetCurvature(MOHO.PEAKED, drawingFrame) mesh:Point(startPt + 1):SetCurvature(MOHO.PEAKED, drawingFrame) mesh:Point(startPt + points-1):SetCurvature(MOHO.PEAKED, drawingFrame) return points end function HS_ShapeSector:OnMouseMoved (moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) local x1 = startVec.x local x2 = vec.x local y1 = startVec.y local y2 = vec.y -- which quadrant are we in? local q1 = (x2>=x1) and (y2>=y1) local q2 = (x2>=x1) and (y2<y1) local q3 = (x2<x1) and (y2<y1) local q4 = (x2<x1) and (y2>=y1) local n = mesh:CountPoints() local centre = n - drawnPoints -- in edit mode, self.Points may have changed local pt = mesh:Point(centre) pt.fPos.x = x1 pt.fPos.y = y1 local radius = (vec - startVec):Mag() local maxAngle = math.atan2(x2-x1, y2-y1) -- is between -180 0 +180 -- allow greater than semi circ unless Ctrl is down if (not mouseEvent.ctrlKey) then if q3 or q4 then -- maxAngle will be negative so ... maxAngle = math.rad(360) + maxAngle end end -- semicircle if Shift is down and >90 ; quadrant if <90 if (mouseEvent.shiftKey) then maxAngle = HS_segment if q3 or q4 then maxAngle = - maxAngle end if q1 or q4 then maxAngle = maxAngle / 2 end end -->>> consider resetting curvatures to give better roundness -- rotate through 90 if Alt local offsetAngle = math.rad(0) if mouseEvent.altKey then offsetAngle = math.rad(90) maxAngle = maxAngle + math.rad(90) end local segAngle = (maxAngle - offsetAngle) / (self.Points - 2) local circPoints= 0 for circPoints = 0, drawnPoints - 2 do local pt = mesh:Point(centre + circPoints+1) pt.fPos.x = x1 + radius * math.sin(offsetAngle + segAngle*circPoints) pt.fPos.y = y1 + radius * math.cos(offsetAngle + segAngle*circPoints) end end -- ************************************************** -- ********************************************************************************************************************************** OVAL -- ************************************************** HS_ShapeOval = {} HS_ShapeOval.Points = 4 local startVec = LM.Vector2:new_local() local startPt function HS_ShapeOval:Identify() local t = {ident="Oval", tip="Oval: Ctrl: all corners rounded. Shift: height=width. Ctrl+Shift: Circle. Alt: draw from centre.", icon="hs_oval", self=HS_ShapeOval, gKey="HS_ShapeOval"} return t end function HS_ShapeOval:About() local body = [==[ This tool creates an ellipse. Point curvature the curvature is set to give the correct ellipsoidal shape – from a straight line where one dimension is very small, to near circular where the dimensions are similar. Ctrl forces the point curvatures to “circular”. Shift distributes the points equidistant from the centre. Ctrl-Shift together create a circle. One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive. If the number of points chosen is not divisible by 4 the input is reduced to the nearest lower multiple of 4. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeOval:LoadPrefs(prefs) self.Points = prefs:GetInt("HS_ShapeOval.Points", 4) end function HS_ShapeOval:SavePrefs(prefs) prefs:SetInt("HS_ShapeOval.Points", self.Points) end function HS_ShapeOval:ResetPrefs() self.Points = 4 end function HS_ShapeOval:Labels () HS_Shape:SetInt1 ("Points on Oval", HS_ShapeOval.Points, 4) end function HS_ShapeOval:BigN (moho, view, value) self.Points = LM.Clamp(value, bigNmin, bigNmax) self.Points = 4 * math.floor(self.Points/4) -- make sure we're divisble by 4 as we need pts at 0,90,180,270 return (self.Points) end -- ==================== function HS_ShapeOval:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local points = HS_ShapeOval.Points local curv = HS_roundcorner startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLoop(moho, startVec, drawingFrame, points, curv) return points end function HS_ShapeOval:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent) local radFloor = 1e-6 -- the smallest radius we can handle local eccCeil = 1e3 -- the biggest ratio of radii local xRad = math.max(math.abs(vec2.x-vec1.x)/2, radFloor) local yRad = math.max(math.abs(vec2.y-vec1.y)/2, radFloor) local points = drawnPoints local i, j, ratio local n = mesh:CountPoints() local ptStart = n - points local angleStart = math.rad(0) local angleStep = math.rad(360) / points local radius, theta, offsetX, offsetY, firstStep, incrStep, XisBiggest local step = {} local eccentric -- how round is it?? -- generate the angle table if xRad > yRad then -- cluster at 90 / 270 XisBiggest = true eccentric = xRad/yRad incrStep = -1 firstStep = points/4 else -- cluster at 0 / 180 XisBiggest = false eccentric = yRad/xRad firstStep = 1 incrStep = 1 end -- spread out the steps -- ratio = math.min(eccentric, 10) ratio = 1 + 0.5*math.log(eccentric) step[1] = 1 j = 1 for i = 1, points/4 - 1 do -- so if we have 40 points we'll have 10 table entries or 8 pts just 2 if (step[i] / ratio) > radFloor then step[i+1] = step[i] / ratio else step[i+1] = 0 -- if it's very tiny then treat as zero to cluster points at the peak end j = j + step[i+1] -- cumulative sum of steps to make 90 degrees end angleStep = math.rad(90) / j -- radians per unit step theta = angleStart -- set the shape for i = 0, points - 1 do -- calc the angle theta = theta + step[firstStep] * angleStep radius = 2 * (xRad * yRad) / math.sqrt (xRad*math.sin(theta) * xRad*math.sin(theta) + yRad*math.cos(theta) * yRad*math.cos(theta)) offsetY = radius * math.sin(theta) offsetX = radius * math.cos(theta) local pt = mesh:Point(ptStart+i) pt.fPos.x = vec1.x + offsetX pt.fPos.y = vec1.y + offsetY -- check if we need to switch round in the table firstStep = firstStep + incrStep if firstStep > points / 4 or firstStep < 1 then -- hit the end going up or down - switch direction ... incrStep = - incrStep firstStep = firstStep + incrStep end end local curvature = HS_roundcorner local curvatureStartPt local curvatureMidPt -- if ctrl then circular if mouseEvent.ctrlKey then curvature = HS_circleCorner for i = 0, points - 1 do mesh:Point(ptStart+i):SetCurvature(curvature, drawingFrame) end else eccentric = math.min(eccentric, eccCeil) if XisBiggest then -- ecc is X/Y if yRad < radFloor*10 then curvatureStartPt = MOHO.PEAKED curvatureMidPt = MOHO.PEAKED curvature = MOHO.PEAKED else curvatureStartPt = LM.Clamp(eccentric, 0, HS_maxCurv / HS_roundcorner) * HS_roundcorner curvatureMidPt = LM.Clamp(1/eccentric, 0, HS_maxCurv / HS_roundcorner) * HS_roundcorner curvature = LM.Slerp ( 1/eccentric, curvatureMidPt,curvatureStartPt) end else -- ecc is Y/X if xRad < radFloor*10 then curvatureStartPt = MOHO.PEAKED curvatureMidPt = MOHO.PEAKED curvature = MOHO.PEAKED else curvatureStartPt = LM.Clamp(1/eccentric, 0, HS_maxCurv / HS_roundcorner) * HS_roundcorner curvatureMidPt = LM.Clamp(eccentric, 0, HS_maxCurv / HS_roundcorner) * HS_roundcorner curvature = LM.Slerp ( 1/eccentric,curvatureStartPt, curvatureMidPt) end end local limit = points/4 mesh:Point(ptStart):SetCurvature(curvatureStartPt, drawingFrame) mesh:Point(ptStart+2*limit):SetCurvature(curvatureStartPt, drawingFrame) mesh:Point(ptStart+limit):SetCurvature(curvatureMidPt, drawingFrame) mesh:Point(ptStart+3*limit):SetCurvature(curvatureMidPt, drawingFrame) for i = 1, limit - 1 do mesh:Point(ptStart+i):SetCurvature(curvature , drawingFrame) mesh:Point(ptStart+2*limit-i):SetCurvature(curvature , drawingFrame) mesh:Point(ptStart+2*limit+i):SetCurvature(curvature , drawingFrame) mesh:Point(ptStart+4*limit-i):SetCurvature(curvature , drawingFrame) end end end -- ************************************************** -- ****************************************************************************************************************************** ROUND CORNER RECTANGLE -- ************************************************** HS_ShapeRectangle = {} local startVec = LM.Vector2:new_local() local startPt HS_ShapeRectangle.Roundness = 0.2 -- corner roundness local cvMax = 0.4 -- absolute max is .5 which makes it "bone" with coincident points local cvMin = 0.1 local points function HS_ShapeRectangle:Identify() local t = {ident="Rectangle", tip="Round Cornered Rectangle: Shift: height=width. Alt: draw from centre.", icon="hs_rc_rect", self=HS_ShapeRectangle, gKey="HS_ShapeRectangle"} return t end function HS_ShapeRectangle:About() local body = [==[ This tool creates a round-cornered rectangle. One data entry field is enabled: the radius of the semicircle that forms the corner as a ratio of the length of the smallest side. It is constrained to be between 0.01 and 0.4. There is also a checkbox for "smoother corners". If checked, additional points are created at each corner. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeRectangle:LoadPrefs(prefs) self.Roundness = prefs:GetFloat("HS_ShapeRectangle.Roundness", 0.2) -- round corner radius as fraction of shortest side self.Smoother = prefs:GetBool("HS_ShapeRectangle.Smoother", false) end function HS_ShapeRectangle:SavePrefs(prefs) prefs:SetFloat("HS_ShapeRectangle.Roundness", self.Roundness) prefs:SetBool("HS_ShapeRectangle.Smoother", self.Smoother) end function HS_ShapeRectangle:ResetPrefs() self.Roundness = 0.2 self.Smoother = false end function HS_ShapeRectangle:Labels () HS_Shape:SetFloat ("corner roundness", HS_ShapeRectangle.Roundness) HS_Shape:SetCheckBox ("Smoother corners?", HS_ShapeRectangle.Smoother) end function HS_ShapeRectangle:Scale (moho, view, value) self.Roundness = LM.Clamp(value, cvMin, cvMax) return (self.Roundness) end function HS_ShapeRectangle:CheckBox (moho, view, value) self.Smoother = value return self.Smoother end -- ====================================== function HS_ShapeRectangle:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) if self.Smoother then points = 20 else points = 12 end local curv = MOHO.PEAKED startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLoop(moho, startVec, drawingFrame, points, curv) local i, j -- four C shapes with 5 points each (smoother) or 3 -- points S+0 (peak); S+1,2,3 (Curve) S+4 (peak) for "smoother" -- S+0 peak, S+1 Corve, S+2 Peak for i = 0, 3 do if self.Smoother then for j = 1, 3 do -- 3 points to curve mesh:Point((startPt + i*5)+j):SetCurvature(HS_circleCorner, drawingFrame) end else mesh:Point(startPt + 1 + (i*3)):SetCurvature(HS_circleCorner, drawingFrame) end end return points end function HS_ShapeRectangle:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent) -- local n = mesh:CountPoints() local pt local xLen = math.abs(vec1.x-vec2.x) local yLen = math.abs(vec1.y-vec2.y) local radius = self.Roundness * math.min(xLen, yLen) local xDir local yDir local smallDeg if (vec2.x>vec1.x) then xDir = -1 else xDir = 1 end if (vec2.y>vec1.y) then yDir = -1 else yDir = 1 end smallDeg = .3 local sin45 = math.sqrt(2)/2 local Deg01 = math.rad (smallDeg) local Deg89 = math.rad (90-smallDeg) local rDelta01 = radius * (1-math.sin(Deg01)) -- also = cos(90-small) local rDelta45 = radius * (1-sin45) -- also = cos45 local rDelta89 = radius * (1-math.sin(Deg89)) -- also = cos(small) --[[ Interesting "other" shape -- smallDeg = 0.1 -- local rDelta01 = radius * (1-smallDeg) -- also = cos(90-small) -- this produces a "scolloped" edge! -- local rDelta89 = radius * (smallDeg) -- also = cos(small) ]] local p = {{x=0, y=radius}, {x=rDelta89, y=rDelta01}, {x=rDelta45, y=rDelta45}, {x=rDelta01, y=rDelta89}, {x=radius, y=0}} local c = {{x=vec1.x, y=vec1.y}, {x=vec2.x, y=vec1.y}, {x=vec2.x, y=vec2.y}, {x=vec1.x, y=vec2.y}} local m = {{x=-1, y=-1}, {x=1, y=-1}, {x=1, y=1}, {x=-1, y=1}} local jStep, jPts local i, j if self.Smoother then jStep = 1 jPts = 5 else jStep = 2 jPts = 3 end for i = 0, 3, 2 do -- four C shapes -- do the L-R ones for j = 0, jPts-1 do -- 5 points use all j's; 3 points use 1,3,5 pt = mesh:Point((startPt + i*jPts)+j) pt.fPos.x = c[i+1].x + (m[i+1].x * xDir * (p[(j*jStep)+1].x)) pt.fPos.y = c[i+1].y + (m[i+1].y * yDir * (p[(j*jStep)+1].y)) end end for i = 1, 3, 2 do -- four C shapes -- do the R-L ones for j = 0, jPts-1 do -- 3 or 5 points pt = mesh:Point((startPt + i*jPts)+j) pt.fPos.x = c[i+1].x + (m[i+1].x * xDir * (p[5-(j*jStep)].x)) pt.fPos.y = c[i+1].y + (m[i+1].y * yDir * (p[5-(j*jStep)].y)) end end end -- ************************************************** -- ****************************************************************************************************************************** ROUND END RECTANGLE (BONE INFLUENCE) -- ************************************************** HS_ShapeBone = {} local BonePoints = 10 local startPoint = 0 local startVec = LM.Vector2:new_local() HS_ShapeBone.Opt = doVector HS_ShapeBone.Wide = 0.1 -- half width (radius of curved end) of the bone shape -- for bone lozenge shape local doBoundingBox = 0 local doVector = 1 local ctBoneOpts = 2 function HS_ShapeBone:Identify() local t = {ident="Bone", tip="Bone: Use Options Button to select draw by Bounding box or Vector.", icon="hs_bone", self=HS_ShapeBone, gKey="HS_ShapeBone"} return t end function HS_ShapeBone:About() local body = [==[ This tool creates a "bone influence" shape. It has two parallel sides with semicircles for the shorter edges. The style of creation can be selected by a button which, when pressed, cycles through the options: >> use a bounding box created by the mouse click-drag to define the shape. The shape will be aligned horizontally or vertically and the dimensions will be set from the bounding box. Alt draws from the centre, but shift is not used. >> use the mouse drag to define the vector along which the shape is created. In this case the “radius of the end semicircle” needs to be manually entered. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeBone:LoadPrefs(prefs) self.Opt = prefs:GetInt("HS_ShapeBone.Opt", 0) self.Wide = prefs:GetFloat("HS_ShapeBone.Wide", 0.1) -- half width (radius of curved end) of the bone shape end function HS_ShapeBone:SavePrefs(prefs) prefs:SetInt("HS_ShapeBone.Opt", self.Opt) prefs:SetFloat("HS_ShapeBone.Wide", self.Wide) end function HS_ShapeBone:ResetPrefs() self.Opt = 0 self.Wide = 0.1 end function HS_ShapeBone:Labels () local buttText if HS_ShapeBone.Opt == doVector then HS_Shape:SetFloat ("radius of end", HS_ShapeBone.Wide) buttText = "Vector" elseif HS_ShapeBone.Opt == doBoundingBox then buttText = "B/Box" end HS_Shape:SetButton (buttText) end function HS_ShapeBone:Scale (moho, view, value) self.Wide = LM.Clamp(value, .001, 1) return (self.Wide) end function HS_ShapeBone:Button (moho, view, value) self.Opt = math.fmod(self.Opt + value + ctBoneOpts, ctBoneOpts) end -- ====================================== function HS_ShapeBone:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPoint = HS_Shape:MakeLoop(moho, startVec, drawingFrame, BonePoints, HS_circleCorner) local corners = {2,3,7,8} local i, v for i, v in ipairs(corners) do mesh:Point(startPoint+v):SetCurvature(MOHO.PEAKED, drawingFrame) end return BonePoints end function HS_ShapeBone:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) if self.Opt == doBoundingBox then self:OnMouseMoved_BB(moho, mouseEvent, mesh, drawingFrame, drawnPoints) elseif self.Opt == doVector then self:OnMouseMoved_Vec(moho, mouseEvent, mesh, drawingFrame, drawnPoints) end end function HS_ShapeBone:OnMouseMoved_BB(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent, false) -- no "shift" local x1 = vec1.x local x2 = vec2.x local y1 = vec1.y local y2 = vec2.y local xLen = math.abs(x1-x2) local yLen = math.abs(y1-y2) local radius = math.min(xLen, yLen) / 2 local sin45 = math.sqrt(2)/2 local rDelta = radius * (1-sin45) local xDir local yDir if (x2>x1) then xDir = -1 else xDir = 1 end if (y2>y1) then yDir = -1 else yDir = 1 end local pt if xLen > yLen then pt = mesh:Point(startPoint+0) pt.fPos.x = x1 pt.fPos.y = y1 - (radius * yDir) pt = mesh:Point(startPoint+1) pt.fPos.x = x1 - (rDelta * xDir) pt.fPos.y = y1 - (rDelta * yDir) pt = mesh:Point(startPoint+2) pt.fPos.x = x1 - (radius * xDir) pt.fPos.y = y1 pt = mesh:Point(startPoint+3) pt.fPos.x = x2 + (radius * xDir) pt.fPos.y = y1 pt = mesh:Point(startPoint+4) pt.fPos.x = x2 + (rDelta * xDir) pt.fPos.y = y1 - (rDelta * yDir) pt = mesh:Point(startPoint+5) pt.fPos.x = x2 pt.fPos.y = y1 - (radius * yDir) pt = mesh:Point(startPoint+6) pt.fPos.x = x2 + (rDelta * xDir) pt.fPos.y = y2 + (rDelta * yDir) pt = mesh:Point(startPoint+7) pt.fPos.x = x2 + (radius * xDir) pt.fPos.y = y2 pt = mesh:Point(startPoint+8) pt.fPos.x = x1 - (radius * xDir) pt.fPos.y = y2 pt = mesh:Point(startPoint+9) pt.fPos.x = x1 - (rDelta * xDir) pt.fPos.y = y2 + (rDelta * yDir) else pt = mesh:Point(startPoint+0) pt.fPos.x = x1 - (radius * xDir) pt.fPos.y = y1 pt = mesh:Point(startPoint+1) pt.fPos.x = x1 - (rDelta * xDir) pt.fPos.y = y1 - (rDelta * yDir) pt = mesh:Point(startPoint+2) pt.fPos.x = x1 pt.fPos.y = y1 - (radius * yDir) pt = mesh:Point(startPoint+3) pt.fPos.x = x1 pt.fPos.y = y2 + (radius * yDir) pt = mesh:Point(startPoint+4) pt.fPos.x = x1 - (rDelta * xDir) pt.fPos.y = y2 + (rDelta * yDir) pt = mesh:Point(startPoint+5) pt.fPos.x = x1 - (radius * xDir) pt.fPos.y = y2 pt = mesh:Point(startPoint+6) pt.fPos.x = x2 + (rDelta * xDir) pt.fPos.y = y2 + (rDelta * yDir) pt = mesh:Point(startPoint+7) pt.fPos.x = x2 pt.fPos.y = y2 + (radius * yDir) pt = mesh:Point(startPoint+8) pt.fPos.x = x2 pt.fPos.y = y1 - (radius * yDir) pt = mesh:Point(startPoint+9) pt.fPos.x = x2 + (rDelta * xDir) pt.fPos.y = y1 - (rDelta * yDir) end end function HS_ShapeBone:OnMouseMoved_Vec(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent, false, false) -- do not process shift / alt local x1 = vec1.x local x2 = vec2.x local y1 = vec1.y local y2 = vec2.y local xLen = x2-x1 local yLen = y2-y1 local theta = math.atan2 (yLen, xLen) local oLen = math.sqrt (xLen*xLen + yLen*yLen) local radius = self.Wide local sin45 = math.sqrt(2)/2 local rDelta = radius * sin45 local pt pt = mesh:Point(startPoint+0) pt.fTempPos.x = -radius pt.fTempPos.y = 0 pt = mesh:Point(startPoint+1) pt.fTempPos.x = -rDelta pt.fTempPos.y = rDelta pt = mesh:Point(startPoint+2) pt.fTempPos.x = 0 pt.fTempPos.y = radius pt = mesh:Point(startPoint+3) pt.fTempPos.x = oLen pt.fTempPos.y = radius pt = mesh:Point(startPoint+4) pt.fTempPos.x = oLen + rDelta pt.fTempPos.y = rDelta pt = mesh:Point(startPoint+5) pt.fTempPos.x = oLen + radius pt.fTempPos.y = 0 pt = mesh:Point(startPoint+6) pt.fTempPos.x = oLen + rDelta pt.fTempPos.y = -rDelta pt = mesh:Point(startPoint+7) pt.fTempPos.x = oLen pt.fTempPos.y = -radius pt = mesh:Point(startPoint+8) pt.fTempPos.x = 0 pt.fTempPos.y = -radius pt = mesh:Point(startPoint+9) pt.fTempPos.x = -rDelta pt.fTempPos.y = -rDelta for i = 0, 9 do pt = mesh:Point(startPoint + i) pt.fTempPos:Rotate(theta) pt.fTempPos.x = pt.fTempPos.x + x1 pt.fTempPos.y = pt.fTempPos.y + y1 pt.fPos:Set(pt.fTempPos) end end -- ************************************************** -- ******************************************************************************************************************************** ANNULUS -- ************************************************** HS_ShapeAnnulus = {} HS_ShapeAnnulus.Points = 8 HS_ShapeAnnulus.startVec = LM.Vector2:new_local() HS_ShapeAnnulus.scale = 0.85 -- annulus inner diameter as a fraction of outer function HS_ShapeAnnulus:Identify() local t = {ident="Annulus", tip="Annulus: Shift: height=width. Alt: draw from centre. (Ctrl not used).", icon="hs_annulus", self=HS_ShapeAnnulus, gKey="HS_ShapeAnnulus"} return t end function HS_ShapeAnnulus:About() local body = [==[ This tool creates a single shape composed of two concentric ovals. Shift creates concentric circles. One data entry field is enabled: the ratio of the width of the centre to outer radius; it is constrained to be between 0.001 and 0.9950 inclusive (A large number creates a large hole in the centre) ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeAnnulus:LoadPrefs(prefs) self.scale = prefs:GetFloat("HS_ShapeAnnulus.scale", 0.85) -- annulus inner as a fraction of outer end function HS_ShapeAnnulus:SavePrefs(prefs) prefs:SetFloat("HS_ShapeAnnulus.scale", self.scale) end function HS_ShapeAnnulus:ResetPrefs() self.scale = 0.85 end function HS_ShapeAnnulus:Labels () HS_Shape:SetFloat ("inner ring ratio", self.scale) end function HS_ShapeAnnulus:Scale (moho, view, value) self.scale = LM.Clamp(value, .001, .995) return (self.scale) end -- ====================================== function HS_ShapeAnnulus:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end -- -- ring 1 -- mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n + 4, n, drawingFrame) -- set all their curvatures appropriately (0.391379 is the magic number for a 4 point circle) mesh:Point(n):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n + 1):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n + 2):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n + 3):SetCurvature(HS_circleCorner, drawingFrame) -- -- ring 2 -- mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n + 8, n + 4, drawingFrame) mesh:Point(n + 4):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n + 5):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n + 6):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n + 7):SetCurvature(HS_circleCorner, drawingFrame) mesh:SelectNone() mesh:Point(n).fSelected = true mesh:Point(n + 4).fSelected = true mesh:SelectConnected() if (HS_Shape.autoFill) then local shapeID = moho:CreateShape(true, false, drawingFrame) if (shapeID >= 0) then if (not HS_Shape.autoOutline) then mesh:Shape(shapeID).fHasOutline = false end end elseif (HS_Shape.autoOutline) then moho:CreateShape(false, false, drawingFrame) end return 8 end function HS_ShapeAnnulus:OnMouseMoved (moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end if (mouseEvent.altKey) then -- draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) end local n = mesh:CountPoints() -- -- radius lengths and of "oval" origin -- local yLen = y2 - y1 -- can be -ve local xLen = x2 - x1 local x0 = (x1 + x2) / 2 local y0 = (y1 + y2) / 2 -- -- ours are the last 8 -- do in 2 batches of 4: last 4 are "outside" -- local pt = mesh:Point(n - 4) pt.fPos.x = x0 pt.fPos.y = y1 local pt = mesh:Point(n - 3) pt.fPos.x = x2 pt.fPos.y = y0 local pt = mesh:Point(n - 2) pt.fPos.x = x0 pt.fPos.y = y2 local pt = mesh:Point(n - 1) pt.fPos.x = x1 pt.fPos.y = y0 -- now set the proper curvature local f = math.abs(x2 - x1) - math.abs(y2 - y1) if (math.abs(f) > 1e-6) then if (math.abs(y2 - y1) > 1e-6) then f = (x2 - x1) / (y2 - y1) else f = 1000 end f = math.abs(f) f = LM.Clamp(f, 0, HS_maxCurv / HS_circleCorner) * HS_circleCorner mesh:Point(n - 4):SetCurvature(f, drawingFrame) mesh:Point(n - 2):SetCurvature(f, drawingFrame) if (math.abs(x2 - x1) > 1e-6) then f = (y2 - y1) / (x2 - x1) else f = 1000 end f = math.abs(f) f = LM.Clamp(f, 0, HS_maxCurv / HS_circleCorner) * HS_circleCorner mesh:Point(n - 3):SetCurvature(f, drawingFrame) mesh:Point(n - 1):SetCurvature(f, drawingFrame) else mesh:Point(n - 4):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n - 3):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n - 2):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n - 1):SetCurvature(HS_circleCorner, drawingFrame) end -- -- first 4 are inside -- n = n-4 -- skip back .... local dx = (1 - self.scale) * xLen / 2 local dy = (1 - self.scale) * yLen / 2 local pt = mesh:Point(n - 4) pt.fPos.x = x0 pt.fPos.y = y1 + dy local pt = mesh:Point(n - 3) pt.fPos.x = x2 - dx pt.fPos.y = y0 local pt = mesh:Point(n - 2) pt.fPos.x = x0 pt.fPos.y = y2 - dy local pt = mesh:Point(n - 1) pt.fPos.x = x1 + dx pt.fPos.y = y0 -- now set the proper curvature local f = math.abs(x2 - x1) - math.abs(y2 - y1) if (math.abs(f) > 1e-6) then if (math.abs(y2 - y1) > 1e-6) then f = (x2 - x1) / (y2 - y1) else f = 1000 end f = math.abs(f) f = LM.Clamp(f, 0, HS_maxCurv / HS_circleCorner) * HS_circleCorner mesh:Point(n - 4):SetCurvature(f, drawingFrame) mesh:Point(n - 2):SetCurvature(f, drawingFrame) if (math.abs(x2 - x1) > 1e-6) then f = (y2 - y1) / (x2 - x1) else f = 1000 end f = math.abs(f) f = LM.Clamp(f, 0, HS_maxCurv / HS_circleCorner) * HS_circleCorner mesh:Point(n - 3):SetCurvature(f, drawingFrame) mesh:Point(n - 1):SetCurvature(f, drawingFrame) else mesh:Point(n - 4):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n - 3):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n - 2):SetCurvature(HS_circleCorner, drawingFrame) mesh:Point(n - 1):SetCurvature(HS_circleCorner, drawingFrame) end end -- ************************************************** -- *************************************************************************************************************************** RT TRIANGLE -- ************************************************** HS_ShapeRtTriangle = {} local startVec = LM.Vector2:new_local() local startPt function HS_ShapeRtTriangle:Identify() local t = {ident="RtTriangle", tip="Right Triangle: Shift: height=width. Alt: draw from centre", icon="hs_rt_tri", self=HS_ShapeRtTriangle, gKey="HS_ShapeRtTriangle"} return t end function HS_ShapeRtTriangle:About() local body = [==[ This tool creates a right angled triangle. No data entry fields are enabled. ]==] return body .. "\n" .. HS_Shape:UILabel() end -- no prefs -- no data -- ============== function HS_ShapeRtTriangle:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local curv = MOHO.PEAKED local drawPts = 3 startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLoop(moho, startVec, drawingFrame, drawPts, curv) return drawPts end function HS_ShapeRtTriangle:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now local pt vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent) pt = mesh:Point(startPt + 0) pt.fPos.x = vec1.x pt.fPos.y = vec1.y pt = mesh:Point(startPt + 1) pt.fPos.x = vec1.x pt.fPos.y = vec2.y pt = mesh:Point(startPt + 2) pt.fPos.x = vec2.x pt.fPos.y = vec1.y end -- ************************************************** -- ************************************************************************************************************************************************ GRID -- ************************************************** HS_ShapeGrid = {} HS_ShapeGrid.startVec = LM.Vector2:new_local() local doOneFill = 0 local doSepShapes = 1 local ctQuadOpts = 2 HS_ShapeGrid.Opt = doOneFill -- options for grid HS_ShapeGrid.Wide = 6 -- Columns in a quad grid HS_ShapeGrid.Deep = 6 -- Rows function HS_ShapeGrid:Identify() local t = {ident="Grid", tip="Rectangular Grid: Use Options Button to create one fill and one stroke or individual shapes for each cell. Shift: height=width. Alt: draw from centre", icon="hs_grid", self=HS_ShapeGrid, gKey="HS_ShapeGrid"} return t end function HS_ShapeGrid:About() local body = [==[ This tool creates a grid of the chosen number of rows and columns. Two data entry fields are enabled: these are the number of rows and columns. Both are constrained to be between 1 and 48 inclusive. There is an options button to select the type of fill. Clicking on the button cycles through the options. There are two options: >> create just one shape for the grid lines and one for the background fill (if the relevant autostroke / autofill options are set) >> create separate shapes for each cell Note that where the choice of input values will result in a large number of shapes being created (e.g., a square grid with 48*48 cells) the elapsed time taken to create all these shapes will be high. (Where the potential for creating a large numbers of individual shapes exists, shapes are created on mouse up and not on mouse down.) ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeGrid:LoadPrefs(prefs) self.Opt = prefs:GetInt("HS_ShapeGrid.Opt", 0) self.Deep = prefs:GetInt("HS_ShapeGrid.Deep", 6) self.Wide = prefs:GetInt("HS_ShapeGrid.Wide", 6) end function HS_ShapeGrid:SavePrefs(prefs) prefs:SetInt("HS_ShapeGrid.Deep", self.Deep) prefs:SetInt("HS_ShapeGrid.Wide", self.Wide) prefs:SetInt("HS_ShapeGrid.Opt", self.quadGridOpt) end function HS_ShapeGrid:ResetPrefs() self.Opt = 0 self.Deep = 6 self.Wide = 6 end function HS_ShapeGrid:Labels () HS_Shape:SetInt1 ("Grid Rows", HS_ShapeGrid.Deep) HS_Shape:SetInt2 ("Columns", HS_ShapeGrid.Wide) local buttText if HS_ShapeGrid.Opt == doOneFill then buttText = "One Shape" elseif HS_ShapeGrid.Opt == doSepShapes then buttText = "Cell Shapes" end HS_Shape:SetButton (buttText) end function HS_ShapeGrid:BigN (moho, view, value) self.Deep = LM.Clamp(value, 1, gridMax) return self.Deep end function HS_ShapeGrid:LittleN (moho, view, value) self.Wide = LM.Clamp(value, 1, gridMax) return self.Wide end function HS_ShapeGrid:Button (moho, view, value) self.Opt = math.fmod(self.Opt + value + ctQuadOpts, ctQuadOpts) end -- ====================================== function HS_ShapeGrid:OnMouseDown (moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end local i, j, m -- set up the dimensions as total Size local gridWide = self.Wide +1 -- points not lines local gridDeep = self.Deep +1 -- the UI gives rows not rows of pts local points = {} local gridSize = gridWide*gridDeep -- -- first create the outside box and fill as necessary -- j= 2*gridWide + 2*gridDeep - 4 mesh:AddLonePoint(self.startVec, drawingFrame) for i=0, j-1 do mesh:AppendPoint(self.startVec, drawingFrame) end -- last one (n+j) moves to weld mesh:WeldPoints(n+j, n, drawingFrame) -- -- fill only (if selected) for the outer box -- mesh:SelectConnected() if (HS_Shape.autoFill) and (self.Opt == doOneFill) then local shapeID = moho:CreateShape(true, false, drawingFrame) if (shapeID >= 0) then mesh:Shape(shapeID).fHasOutline = false end end -- create the remaining "Deep" lines of "Wide" points if there are any (i.e. not a 1*1 grid) if gridSize > 4 then local topLeftNext = n + 2*gridWide + 2*gridDeep - 5 -- point number for weld local topRightNext = n + gridWide local leftPtNew = topLeftNext + 1 -- pt number in the new lines local leftPt = {} -- keep a list of the left most points for subsequent bindings leftPt[1] = 1 + n -- top row leftPt[gridDeep] = n + 2*gridWide + gridDeep-4 -- bottom row for j=0, gridDeep-3 do mesh:AddLonePoint(self.startVec, drawingFrame) for i=0, gridWide-2 do mesh:AppendPoint(self.startVec, drawingFrame) end mesh:WeldPoints(leftPtNew, topLeftNext, drawingFrame) mesh:WeldPoints(leftPtNew + gridWide-2, topRightNext, drawingFrame) leftPt[j+2] = leftPtNew topLeftNext = topLeftNext - 1 topRightNext = topRightNext + 1 leftPtNew = leftPtNew + gridWide - 2 end -- now add pairs of points to link and weld -- as you weld points so they disappear, so the next to weld is always the same reference -- bottom row bottom pts are "special" for j=0, gridDeep-3 do for i= 0, gridWide-3 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, leftPt[j+1]+i, drawingFrame) mesh:WeldPoints(n+gridSize, leftPt[j+2]+i, drawingFrame) end end j = gridDeep-2 for i= 0, gridWide-3 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, leftPt[j+1]+i, drawingFrame) mesh:WeldPoints(n+gridSize, leftPt[j+2]-i, drawingFrame) end end -- now create the grid shape mesh:SelectConnected() if (HS_Shape.autoOutline) and (self.Opt == doOneFill) then local shapeID = moho:CreateShape(false, false, drawingFrame) end -- and finally set peaked points for i = n, n + gridSize-1 do mesh:Point(i):SetCurvature(MOHO.PEAKED, drawingFrame) end -- if we're deferring shape creation until mouse up ... local row, col = 0, 0 if self.Opt == doSepShapes then -- populate points table -- start with the outer ring for i = 0, gridWide - 1 do --top row points[i+1] = n + i end for i = 1, gridDeep-1 do -- right col points[gridWide*(i+1)] = n + gridWide + i - 1 end for i = 1, gridWide - 1 do -- bottom row points[(gridWide * (gridDeep-1)) + i] = n + (gridWide*2) + (gridDeep-2) - i end for i = 1, gridDeep-2 do -- left col points[(gridWide*i)+1] = n + 2*gridWide + 2*gridDeep - 4 - i end -- now the middle bit ... m = n + 2*gridWide + 2*gridDeep - 4 for i = 1, gridDeep-2 do for j = 1, gridWide-2 do points[(gridWide*i) + (j+1)] = m m = m + 1 end end -- finally, populate the shapes to make table local p = {} for i = 0, gridWide-2 do for j = 0, gridDeep - 2 do p[1] = points[gridWide*j + i + 1] p[2] = points[gridWide*j + i + 2] p[3] = points[gridWide*(j+1) + i + 1] p[4] = points[gridWide*(j+1) + i + 2] table.insert (shapePoints, {p[1], p[2], p[3], p[4]}) end end end return gridSize end function HS_ShapeGrid:OnMouseMoved (moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end if (mouseEvent.altKey) then -- draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) end local n = mesh:CountPoints() local gridWide = self.Wide +1 -- points not lines local gridDeep = self.Deep +1 local gridSize = gridWide*gridDeep -- the last gridsize points are ours local deltaX = (x2-x1) / (gridWide-1) local deltaY = (y2-y1) / (gridDeep-1) local i local j -- move the outside -- pts +0 to +Wide are top row; the next d-2 are right side; the next w are bottom in rev order the next d-2 are lft side rev -- base points number for the perimter = n - gridSize local basePt = n-gridSize j=0 for i=0, gridWide-1 do local pt = mesh:Point(basePt + j) pt.fPos.x = deltaX*i + x1 pt.fPos.y = y1 j=j+1 end for i=1, gridDeep-2 do local pt = mesh:Point(n-gridSize + j) pt.fPos.x = deltaX*(gridWide-1) + x1 pt.fPos.y = deltaY*i + y1 j=j+1 end for i=0, gridWide-1 do local pt = mesh:Point(n-gridSize + j) pt.fPos.x = deltaX*(gridWide-i-1) + x1 pt.fPos.y = deltaY*(gridDeep-1) + y1 j=j+1 end for i=1, gridDeep-2 do local pt = mesh:Point(n-gridSize + j) pt.fPos.x = x1 pt.fPos.y = deltaY*(gridDeep-i-1) + y1 j=j+1 end -- now the remaining points are inside.. we've done the edges so just the insides.. -- base points number for the insides local basePt = basePt + j for j=1, gridDeep-2 do for i= 1, gridWide-2 do local pt = mesh:Point(basePt + i-1 + (gridWide-2)*(j-1)) pt.fPos.x = deltaX*i + x1 pt.fPos.y = deltaY*j + y1 end end end -- ************************************************** -- *********************************************************************************************************************************** TRI GRID COMMON / DISPATCH -- ************************************************** HS_ShapeTri_Grid = {} HS_ShapeTri_Grid.startVec = LM.Vector2:new_local() -- for triGridOpt local doHex = 2 -- do hexagons - ragged edge local doSqAlt = 0 -- do squares with alternate diagonals local doSqHex = 1 -- do squares with diagnoals all in the same orientation local ctGridOpts = 3 -- how many options? HS_ShapeTri_Grid.Opt = doSqHex -- options for grid HS_ShapeTri_Grid.hexWide = 6 -- Columns of triangles in a hexagonal patterned grid HS_ShapeTri_Grid.hexDeep = 4 -- Rows HS_ShapeTri_Grid.triWide = 5 -- Columns of squares in an alternating diagonals triangular grid HS_ShapeTri_Grid.triDeep = 5 -- Rows HS_ShapeTri_Grid.sqhWide = 6 -- Columns of squares in a square hex-based triangular grid HS_ShapeTri_Grid.sqhDeep = 6 -- Rows function HS_ShapeTri_Grid:Identify() local t = {ident="TriangularGrid", tip="Triangulated Grid: Use Options Button for style of triangulation. Shift: height=width. Alt: draw from centre.", icon="hs_tri_grid", self=HS_ShapeTri_Grid, gKey="HS_ShapeTri_Grid"} return t end function HS_ShapeTri_Grid:About() local body = [==[ The options button is used to select the style of the grid of triangles. Clicking on the button cycles through the options. There are three grid variants: >> a grid based on hexagons ("Hexagonal") Two data entry fields are enabled: these are the number of rows and the number of triangles per row. The number of rows is constrained to be between 1 and 48 inclusive; the number of triangles per row is constrained to be between 2 and 48 inclusive. This produces a “ragged edge” grid of similar triangles. >> a grid based on squares with alternate diagonals ("Squares/alt") >> a grid based on squares with the same diagonals ("Hex Square") In these last two options two data entry fields are enabled: these are the number of rows and the number of squares per row. Both are constrained to be between 1 and 48 inclusive. Note that where the choice of input values will result in a large number of shapes being created (e.g., a triangular grid with 48*96 cells) the elapsed time taken to create all these shapes will be high. (Where the potential for creating a large numbers of individual shapes exists, shapes are created on mouse up and not on mouse down.) ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeTri_Grid:SavePrefs(prefs) prefs:SetInt("HS_ShapeTri_Grid.Opt", self.Opt) prefs:SetInt("HS_ShapeTri_Grid.hexDeep", self.hexDeep) prefs:SetInt("HS_ShapeTri_Grid.hexWide", self.hexWide) prefs:SetInt("HS_ShapeTri_Grid.triDeep", self.triDeep) prefs:SetInt("HS_ShapeTri_Grid.triWide", self.triWide) prefs:SetInt("HS_ShapeTri_Grid.sqhDeep", self.sqhDeep) prefs:SetInt("HS_ShapeTri_Grid.sqhWide", self.sqhWide) end function HS_ShapeTri_Grid:LoadPrefs(prefs) self.Opt = prefs:GetInt("HS_ShapeTri_Grid.Opt", 0) self.hexDeep = prefs:GetInt("HS_ShapeTri_Grid.hexDeep", 4) self.hexWide = prefs:GetInt("HS_ShapeTri_Grid.hexWide", 6) self.triDeep = prefs:GetInt("HS_ShapeTri_Grid.triDeep", 5) self.triWide = prefs:GetInt("HS_ShapeTri_Grid.triWide", 5) self.sqhDeep = prefs:GetInt("HS_ShapeTri_Grid.sqhDeep", 6) self.sqhWide = prefs:GetInt("HS_ShapeTri_Grid.sqhWide", 6) end function HS_ShapeTri_Grid:ResetPrefs() self.Opt = 0 self.hexDeep = 4 self.hexWide = 6 self.triDeep = 5 self.triWide = 5 self.sqhDeep = 6 self.sqhWide = 6 end function HS_ShapeTri_Grid:Labels () local bigNText, bigNVal local LittleNText, LittleNVal local buttText if self.Opt == doHex then bigNText = "Rows" bigNVal = self.hexDeep littleNText = "Triangles per row" LittleNVal = self.hexWide buttText = "Hexagonal" elseif self.Opt == doSqAlt then bigNText = "Rows" bigNVal = self.triDeep littleNText = "Columns of squares" LittleNVal = self.triWide buttText = "Squares/alt" elseif self.Opt == doSqHex then bigNText = "Rows" bigNVal = self.sqhDeep littleNText = "Columns of squares" LittleNVal = self.sqhWide buttText = "Hex Square" end HS_Shape:SetInt1 (bigNText, bigNVal) HS_Shape:SetInt2 (littleNText, LittleNVal) HS_Shape:SetButton (buttText) end function HS_ShapeTri_Grid:BigN (moho, view, value) local rtx if self.Opt == doHex then self.hexDeep = LM.Clamp(value, 1, gridMax) rtx = self.hexDeep elseif self.Opt == doSqAlt then self.triDeep = LM.Clamp(value, 1, gridMax) rtx = self.triDeep elseif self.Opt == doSqHex then self.sqhDeep = LM.Clamp(value, 1, gridMax) rtx = self.sqhDeep end return rtx end function HS_ShapeTri_Grid:LittleN (moho, view, value) local rtx if self.Opt == doHex then self.hexWide = LM.Clamp(value, 2, gridMax) rtx = self.hexWide elseif self.Opt == doSqAlt then self.triWide = LM.Clamp(value, 1, gridMax) rtx = self.triWide elseif self.Opt == doSqHex then self.sqhWide = LM.Clamp(value, 1, gridMax) rtx = self.sqhWide end return rtx end function HS_ShapeTri_Grid:Button (moho, view, value) self.Opt = math.fmod(self.Opt + value + ctGridOpts, ctGridOpts) end -- ====================================== function HS_ShapeTri_Grid:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local j if self.Opt == doHex then j = self:OnMouseDown_HexGrid(moho, mouseEvent, mesh, drawingFrame) elseif self.Opt == doSqAlt then j = self:OnMouseDown_Rt_TriGrid(moho, mouseEvent, mesh, drawingFrame) elseif self.Opt == doSqHex then j = self:OnMouseDown_Sq_HexGrid(moho, mouseEvent, mesh, drawingFrame) end return j end function HS_ShapeTri_Grid:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) if self.Opt == doHex then self:OnMouseMoved_HexGrid(moho, mouseEvent, mesh, drawingFrame, drawnPoints) elseif self.Opt == doSqAlt then self:OnMouseMoved_Rt_TriGrid(moho, mouseEvent, mesh, drawingFrame, drawnPoints) elseif self.Opt == doSqHex then self:OnMouseMoved_Sq_HexGrid(moho, mouseEvent, mesh, drawingFrame, drawnPoints) end end -- ************************************************** -- *********************************************************************************************************************************** HEX TRIANGLE GRID -- ************************************************** function HS_ShapeTri_Grid:OnMouseDown_HexGrid(moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end local i, j, k -- set up the dimensions as total Size local gridDeep = self.hexDeep + 1 -- the UI gives rows not rows of pts - so this many lines local gridSize = 0 -- -- first create the lines -- local rows = {} local rowPts for i = 1, gridDeep do rows[i] = gridSize rowPts = math.ceil ((self.hexWide-math.fmod(i+1,2))/2)+1 gridSize = gridSize + rowPts mesh:AddLonePoint(self.startVec, drawingFrame) for j= 2, rowPts do mesh:AppendPoint(self.startVec, drawingFrame) -- >>>logical, but what if only one colums - IOW 1 point? end end rows[gridDeep+1] = gridSize -- now add pairs of points to link and weld -- as you weld points so they disappear, so the next to weld is always the same reference for i = 1, gridDeep-1 do local thisPts = rows[i+1] - rows[i] local nextPts = rows[i+2] - rows[i+1] local oddRow = math.fmod(i,2) local limit = math.min(thisPts, nextPts) for j = 0, limit-1 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, n+j+rows[i], drawingFrame) mesh:WeldPoints(n+gridSize, n+j+rows[i+1], drawingFrame) end if thisPts < nextPts then -- if fewer in "top" row then weld all top points n:n+1 for j = 0, thisPts-1 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, n+j+rows[i], drawingFrame) mesh:WeldPoints(n+gridSize, n+j+1+rows[i+1], drawingFrame) end elseif thisPts == nextPts and oddRow == 0 then -- if same number in top row and an eve row then weld n:n+1 from 1st for j = 0, thisPts-2 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, n+j+rows[i], drawingFrame) mesh:WeldPoints(n+gridSize, n+j+1+rows[i+1], drawingFrame) end else -- if more in top row then weld n:n-1 from 2nd or -- if same number in top row and an odd row then weld n:n-1 from 2nd for j = 1, thisPts-1 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, n+j+rows[i], drawingFrame) mesh:WeldPoints(n+gridSize, n+j-1+rows[i+1], drawingFrame) end end end -- create shapes for i = 1, self.hexDeep do local ttb local nextTop, nextBtm = n, n local p = {} ttb = (math.fmod(i,2) == 1) -- i.e.if it is rows 1, 3... start with a top-top-bottom for j = 0, self.hexWide-1 do if ttb then p[1] = rows[i]+nextTop p[2] = rows[i]+nextTop+1 p[3] = rows[i+1]+nextBtm nextTop = nextTop + 1 else p[1] = rows[i]+nextTop p[2] = rows[i+1]+nextBtm p[3] = rows[i+1]+nextBtm+1 nextBtm = nextBtm + 1 end ttb = not ttb --flip table.insert (shapePoints, {p[1], p[2], p[3]}) end end -- and finally set peaked points for i = n, n + gridSize-1 do mesh:Point(i):SetCurvature(MOHO.PEAKED, drawingFrame) end mesh:SelectConnected() return gridSize end function HS_ShapeTri_Grid:OnMouseMoved_HexGrid(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end if (mouseEvent.altKey) then -- draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) end local n = mesh:CountPoints() local gridDeep = self.hexDeep + 1 -- the UI gives rows not rows of pts - so this many lines local gridWide = 1+math.floor (self.hexWide/2) -- calculate base number of points for the # of triangles local gridSize = 0 local rows = {} local rowPts local deltaX = (x2-x1) / gridWide local deltaY = (y2-y1) / (gridDeep-1) local i local j for i = 1, gridDeep+1 do rows[i] = gridSize rowPts = math.ceil ((self.hexWide-math.fmod(i+1,2))/2)+1 gridSize = gridSize + rowPts end local basePt = n-drawnPoints for i=1, gridDeep do local rem = math.fmod (i+1, 2) -- are we an even # row -- if so shift x by half an increment rowPts = rows[i+1] - rows[i] for j = 0, rowPts-1 do local pt = mesh:Point(basePt + rows[i] + j) pt.fPos.x = deltaX*(j+rem/2) + x1 pt.fPos.y = deltaY*(i-1) + y1 end end end -- ************************************************** -- *********************************************************************************************************************************** SQUARE TRIANGLE GRID -- ************************************************** function HS_ShapeTri_Grid:OnMouseDown_Rt_TriGrid(moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end local i, j, k -- set up the dimensions as total Size local gridDeep = self.triDeep + 1 -- the UI gives rows not rows of pts - so this many lines local gridWide = self.triWide + 1 local gridSize = gridDeep * gridWide -- -- first create the lines -- for i = 1, gridDeep do mesh:AddLonePoint(self.startVec, drawingFrame) for j = 2, gridWide do mesh:AppendPoint(self.startVec, drawingFrame) end end -- now add pairs of points to link and weld -- as you weld points so they disappear, so the next to weld is always the same reference -- first make the squares for i = 1, gridDeep-1 do local thisPts = n + (i-1) * gridWide local nextPts = thisPts + gridWide for j = 0, gridWide-1 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, j+thisPts, drawingFrame) mesh:WeldPoints(n+gridSize, j+nextPts, drawingFrame) end end -- now the diagonals for odd numbers for i = 1, gridDeep-1, 2 do local thisPts = n + (i-1) * gridWide local nextPts = thisPts + gridWide for j = 0, gridWide-1, 2 do if j-1 >= 0 then mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, j+thisPts, drawingFrame) mesh:WeldPoints(n+gridSize, j+nextPts-1, drawingFrame) end if j+1 < gridWide then mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, j+thisPts, drawingFrame) mesh:WeldPoints(n+gridSize, j+nextPts+1, drawingFrame) end end end -- and diagonals for even numbered rows for i = 2, gridDeep-1, 2 do local thisPts = n + (i-1) * gridWide local nextPts = thisPts + gridWide for j = 1, gridWide-1, 2 do if j-1 >= 0 then mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, j+thisPts, drawingFrame) mesh:WeldPoints(n+gridSize, j+nextPts-1, drawingFrame) end if j+1 < gridWide then mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, j+thisPts, drawingFrame) mesh:WeldPoints(n+gridSize, j+nextPts+1, drawingFrame) end end end -- create shapes for i = 1, self.triDeep do local p = {} local thisPt = n local whichShape = 2*math.fmod(i,2) -- if it is rows 1, 3... start with a type 2; rows 2, 4 ... start with type 0 for j = 1, self.triWide*2 do -- there are 2 triangles per square and 4 shapes of triangle ... if whichShape == 0 then -- top, top+1, btm p[1] = gridWide * (i-1) + thisPt p[2] = gridWide * (i-1) + thisPt + 1 p[3] = gridWide * (i) + thisPt whichShape = 1 elseif whichShape == 1 then -- top+1, btm, btm+1 p[1] = gridWide * (i-1) + thisPt + 1 p[2] = gridWide * (i) + thisPt p[3] = gridWide * (i) + thisPt + 1 whichShape = 2 thisPt = thisPt + 1 elseif whichShape == 2 then -- top, btm, btm +1 p[1] = gridWide * (i-1) + thisPt p[2] = gridWide * (i) + thisPt p[3] = gridWide * (i) + thisPt + 1 whichShape = 3 elseif whichShape == 3 then -- top top+1 btm+1 p[1] = gridWide * (i-1) + thisPt p[2] = gridWide * (i-1) + thisPt + 1 p[3] = gridWide * (i) + thisPt + 1 whichShape = 0 thisPt = thisPt + 1 end table.insert (shapePoints, {p[1], p[2], p[3]}) -- insert (t,p) failed!! scpoing issues?? end end -- and finally set peaked points for i = n, n + gridSize-1 do mesh:Point(i):SetCurvature(MOHO.PEAKED, drawingFrame) end mesh:SelectConnected() return gridSize end function HS_ShapeTri_Grid:OnMouseMoved_Rt_TriGrid(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end if (mouseEvent.altKey) then -- draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) end local n = mesh:CountPoints() local gridDeep = self.triDeep + 1 -- the UI gives rows not rows of pts - so this many lines local gridWide = self.triWide + 1 local deltaX = (x2-x1) / (gridWide-1) local deltaY = (y2-y1) / (gridDeep-1) local i local j local basePt = n-drawnPoints for i=0, gridDeep-1 do for j = 0, gridWide-1 do local pt = mesh:Point(basePt + j + gridWide*i) pt.fPos.x = deltaX*j + x1 pt.fPos.y = deltaY*i + y1 end end end -- ************************************************** -- *********************************************************************************************************************************** SQUARE HEX GRID -- ************************************************** function HS_ShapeTri_Grid:OnMouseDown_Sq_HexGrid(moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end local i, j, k -- set up the dimensions as total Size local gridDeep = self.sqhDeep + 1 -- the UI gives rows not rows of pts - so this many lines local gridWide = self.sqhWide + 1 local gridSize = gridDeep * gridWide -- -- first create the lines -- for i = 1, gridDeep do mesh:AddLonePoint(self.startVec, drawingFrame) for j = 2, gridWide do mesh:AppendPoint(self.startVec, drawingFrame) end end -- now add pairs of points to link and weld -- as you weld points so they disappear, so the next to weld is always the same reference -- first make the squares for i = 1, gridDeep-1 do local thisPts = n + (i-1) * gridWide local nextPts = thisPts + gridWide for j = 0, gridWide-1 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, j+thisPts, drawingFrame) mesh:WeldPoints(n+gridSize, j+nextPts, drawingFrame) end end -- now the diagonals top to one right for i = 1, gridDeep-1 do local thisPts = n + (i-1) * gridWide local nextPts = thisPts + gridWide for j = 0, gridWide-2 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n+gridSize, j+thisPts, drawingFrame) mesh:WeldPoints(n+gridSize, j+1+nextPts, drawingFrame) end end -- create shapes for i = 1, self.sqhDeep do local p = {} local thisPt = n local L_Shape = true -- start with an L for j = 1, self.sqhWide*2 do -- there are 2 triangles per square and 2 shapes of triangle ... if L_Shape then -- top, btm, btm +1 p[1] = gridWide * (i-1) + thisPt p[2] = gridWide * (i) + thisPt p[3] = gridWide * (i) + thisPt + 1 L_Shape = not L_Shape else -- top top+1 btm+1 p[1] = gridWide * (i-1) + thisPt p[2] = gridWide * (i-1) + thisPt + 1 p[3] = gridWide * (i) + thisPt + 1 L_Shape = not L_Shape thisPt = thisPt + 1 end --?? only do mouse up creation if > threshold shapes??? table.insert (shapePoints, {p[1], p[2], p[3]}) end end -- and finally set peaked points for i = n, n + gridSize-1 do mesh:Point(i):SetCurvature(MOHO.PEAKED, drawingFrame) end mesh:SelectConnected() return gridSize end function HS_ShapeTri_Grid:OnMouseMoved_Sq_HexGrid(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end if (mouseEvent.altKey) then -- draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) end local n = mesh:CountPoints() local gridDeep = self.sqhDeep + 1 -- the UI gives rows not rows of pts - so this many lines local gridWide = self.sqhWide + 1 local deltaX = (x2-x1) / (gridWide-1) local deltaY = (y2-y1) / (gridDeep-1) local i local j local basePt = n-drawnPoints for i=0, gridDeep-1 do for j = 0, gridWide-1 do local pt = mesh:Point(basePt + j + gridWide*i) pt.fPos.x = deltaX*j + x1 pt.fPos.y = deltaY*i + y1 end end end -- ************************************************** -- ****************************************************************************************************************************** CROSS -- ************************************************** HS_ShapeCross = {} HS_ShapeCross.scale = 1/3 -- scaling factor - swiss cross local startVec = LM.Vector2:new_local() local startPt function HS_ShapeCross:Identify() local t = {ident="SwissCross", tip="Swiss Cross: Ctrl: all corners rounded. Shift: height=width. Alt: draw from centre.", icon="hs_cross", self=HS_ShapeCross, gKey="HS_ShapeCross"} return t end function HS_ShapeCross:About() local body = [==[ This tool creates a Swiss Cross. Ctrl makes the points rounded (otherwise they are right angles). One data entry field is enabled: width of the arm of the cross as a ratio of the shortest side. This can be between 0.01 and 0.99 inclusive. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeCross:LoadPrefs(prefs) self.scale = prefs:GetFloat("HS_ShapeCross.scale", 0.333) -- swiss cross as fraction of shortest side end function HS_ShapeCross:SavePrefs(prefs) prefs:SetFloat("HS_ShapeCross.scale", self.scale) end function HS_ShapeCross:ResetPrefs() self.scale = 1/3 end function HS_ShapeCross:Labels () HS_Shape:SetFloat ("Arm width", self.scale) end function HS_ShapeCross:Scale (moho, view, value) self.scale = LM.Clamp(value, .01, .80) return self.scale end -- ====================================== function HS_ShapeCross:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local drawPts = 12 local curv if (mouseEvent.ctrlKey) then curv = HS_roundcorner -- >>> better curvature.... else curv = MOHO.PEAKED end startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLoop(moho, startVec, drawingFrame, drawPts, curv) return drawPts end function HS_ShapeCross:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent) local x1 = vec1.x local x2 = vec2.x local y1 = vec1.y local y2 = vec2.y local i local sides = LM.Vector2:new_local() local pointA = LM.Vector2:new_local() local pointB = LM.Vector2:new_local() local crossW local pt sides.x = x2 - x1 sides.y = y2 - y1 if math.abs(sides.x) > math.abs(sides.y) then crossW = sides.y * self.scale else crossW = sides.x * self.scale end if sides.x * crossW > 0 then pointA.x = x1 + (sides.x - crossW) / 2 pointB.x = x1 + (sides.x + crossW) / 2 else pointB.x = x1 + (sides.x - crossW) / 2 pointA.x = x1 + (sides.x + crossW) / 2 end if sides.y * crossW < 0 then pointB.y = y1 + (sides.y - crossW) / 2 pointA.y = y1 + (sides.y + crossW) / 2 else pointA.y = y1 + (sides.y - crossW) / 2 pointB.y = y1 + (sides.y + crossW) / 2 end pt = mesh:Point(startPt+0) pt.fPos.x = x1 pt.fPos.y = pointA.y pt = mesh:Point(startPt+1) pt.fPos.x = pointA.x pt.fPos.y = pointA.y pt = mesh:Point(startPt+2) pt.fPos.x = pointA.x pt.fPos.y = y1 pt = mesh:Point(startPt+3) pt.fPos.x = pointB.x pt.fPos.y = y1 pt = mesh:Point(startPt+4) pt.fPos.x = pointB.x pt.fPos.y = pointA.y pt = mesh:Point(startPt+5) pt.fPos.x = x2 pt.fPos.y = pointA.y pt = mesh:Point(startPt+6) pt.fPos.x = x2 pt.fPos.y = pointB.y pt = mesh:Point(startPt+7) pt.fPos.x = pointB.x pt.fPos.y = pointB.y pt = mesh:Point(startPt+8) pt.fPos.x = pointB.x pt.fPos.y = y2 pt = mesh:Point(startPt+9) pt.fPos.x = pointA.x pt.fPos.y = y2 pt = mesh:Point(startPt+10) pt.fPos.x = pointA.x pt.fPos.y = pointB.y pt = mesh:Point(startPt+11) pt.fPos.x = x1 pt.fPos.y = pointB.y if (mouseEvent.ctrlKey) then curv = HS_roundcorner -- >>> better curvature.... else curv = MOHO.PEAKED end for i = startPt, startPt+11 do mesh:Point(i):SetCurvature(curv, drawingFrame) end end -- ************************************************** -- ******************************************************************************************************************************************* STAR -- ************************************************** HS_ShapeStar = {} HS_ShapeStar.Points = 6 -- points on a star HS_ShapeStar.scale = 0.382 -- scaling factor - star local startVec = LM.Vector2:new_local() local startPt function HS_ShapeStar:Identify() local t = {ident="Star", tip="Star: Ctrl: all corners rounded. Shift: height=width. Alt: draw from centre.", icon="hs_star", self=HS_ShapeStar, gKey="HS_ShapeStar"} return t end function HS_ShapeStar:About() local body = [==[ This tool creates a Star with the chosen number of arms. Ctrl makes the points rounded (otherwise they are sharp). Two data entry fields are enabled: these are the number of arms on the star and the ratio of the width of the centre to total radius of the star. The star can have between 3 and 72 arms inclusive; the centre ratio can be between 0.001 and 0.9950 inclusive. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeStar:LoadPrefs(prefs) self.Points = prefs:GetInt("HS_ShapeStar.Points", 6) self.scale = prefs:GetFloat("HS_ShapeStar.scale", 0.382) -- star inner as a fraction of outer end function HS_ShapeStar:SavePrefs(prefs) prefs:SetInt("HS_ShapeStar.Points", self.Points) prefs:SetFloat("HS_ShapeStar.scale", self.scale) end function HS_ShapeStar:ResetPrefs() self.Points = 6 self.scale = 0.382 end function HS_ShapeStar:Labels () HS_Shape:SetInt1 ("Arms on Star", self.Points) HS_Shape:SetFloat ("Centre size ratio", self.scale) end function HS_ShapeStar:BigN (moho, view, value) self.Points = LM.Clamp(value, 3, bigNmax) return self.Points end function HS_ShapeStar:Scale (moho, view, value) self.scale = LM.Clamp(value, .001, .995) return self.scale end -- ====================================== function HS_ShapeStar:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local curv local drawPts = 2 * self.Points if (mouseEvent.ctrlKey) then curv = HS_roundcorner -- >>> better curvature.... else curv = MOHO.PEAKED end startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLoop(moho, startVec, drawingFrame, drawPts, curv) return drawPts end function HS_ShapeStar:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent) local x1 = vec1.x local x2 = vec2.x local y1 = vec1.y local y2 = vec2.y if (mouseEvent.ctrlKey) then curv = HS_roundcorner -- >>> better curvature.... else curv = MOHO.PEAKED end local numPoints = drawnPoints / 2 local ptID = startPt local angle = math.pi / 2 local dAngle = math.pi / numPoints local bigR = 1.0 local initRatio = self.scale local littleR = bigR*initRatio local scale = LM.Vector2:new_local() local offset = LM.Vector2:new_local() local pt scale.x = math.abs(x2 - x1) / 2 scale.y = math.abs(y2 - y1) / 2 offset.x = (x1 + x2) / 2 offset.y = (y1 + y2) / 2 for i = 1, numPoints do pt = mesh:Point(ptID) ptID = ptID + 1 pt.fPos.x = (bigR * math.cos(angle)) * scale.x + offset.x pt.fPos.y = (bigR * math.sin(angle)) * scale.y + offset.y pt:SetCurvature(curv, drawingFrame) angle = angle + dAngle pt = mesh:Point(ptID) ptID = ptID + 1 pt.fPos.x = (littleR * math.cos(angle)) * scale.x + offset.x pt.fPos.y = (littleR * math.sin(angle)) * scale.y + offset.y pt:SetCurvature(curv, drawingFrame) angle = angle + dAngle end end -- ************************************************** -- ********************************************************************************************************************************** POLYGON -- ************************************************** HS_ShapePolygon = {} HS_ShapePolygon.Sides = 6 -- sides on a polygon local startVec = LM.Vector2:new_local() local startPt function HS_ShapePolygon:Identify() local t = {ident="Polygon", tip="Polygon Ctrl: all corners rounded. Shift: height=width. Alt: draw from centre.", icon="hs_polygon", self=HS_ShapePolygon, gKey="HS_ShapePolygon"} return t end function HS_ShapePolygon:About() local body = [==[ This tool creates a polygon. One data entry field is enabled: the number of sides which can be between 3 and 24 inclusive. Ctrl rounds the points (they are peaked by default). Shift makes the height and width the same. Alt draws form the centre. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapePolygon:LoadPrefs(prefs) self.Sides = prefs:GetInt("HS_ShapePolygon.Sides", 6) end function HS_ShapePolygon:SavePrefs(prefs) prefs:SetInt("HS_ShapePolygon.Sides", self.Sides) end function HS_ShapePolygon:ResetPrefs() self.Sides = 6 end function HS_ShapePolygon:Labels () HS_Shape:SetInt1 ("Sides on Polygon", self.Sides) end function HS_ShapePolygon:BigN (moho, view, value) self.Sides = LM.Clamp(value, 3, bigNmed) return self.Sides end -- ====================================== function HS_ShapePolygon:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local curv local drawPts = self.Sides if (mouseEvent.ctrlKey) then curv = HS_roundcorner -- >>> better curvature.... else curv = MOHO.PEAKED end startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLoop(moho, startVec, drawingFrame, drawPts, curv) return drawPts end function HS_ShapePolygon:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec1 = LM.Vector2:new_local() -- start local vec2 = LM.Vector2:new_local() -- mouse now vec1, vec2 = HS_Shape:DrawingBounds (moho, mouseEvent) local x1 = vec1.x local x2 = vec2.x local y1 = vec1.y local y2 = vec2.y local pt local Curvature = MOHO.PEAKED if (mouseEvent.ctrlKey) then Curvature = HS_roundcorner end local dAngle = math.rad(360) / drawnPoints -- angle between apex in regular polygon local angle = (math.rad(180) - dAngle)/2 -- starting position (in effect the "point" on an odd-numbered sided polygon) if (y2>y1) then angle = angle + math.rad(180) -- flip if mouse higher on the page... end local scale = LM.Vector2:new_local() local offset = LM.Vector2:new_local() scale.x = math.abs(x2 - x1) / math.sqrt(2) scale.y = math.abs(y2 - y1) / math.sqrt(2) offset.x = (x1 + x2) / 2 offset.y = (y1 + y2) / 2 for i = 0, drawnPoints - 1 do pt = mesh:Point(startPt + i) pt.fPos.x = (scale.x * math.cos(angle)) + offset.x pt.fPos.y = (scale.y * math.sin(angle)) + offset.y pt:SetCurvature(Curvature, drawingFrame) angle = angle + dAngle end end -- ************************************************** -- ********************************************************************************************************************************** CONCENTRIC POLYGONS -- ************************************************** HS_ShapeConcentrics = {} HS_ShapeConcentrics.Concs = 2 -- how many concentric polygons HS_ShapeConcentrics.Sides = 6 -- sides on concentric polygons HS_ShapeConcentrics.startVec = LM.Vector2:new_local() function HS_ShapeConcentrics:Identify() local t = {ident="Concentrics", tip="Concentric Polygons: Ctrl: all corners rounded. Shift: height=width. Ctrl+Shift: Circles. Alt: draw from centre.", icon="hs_concentrics", self=HS_ShapeConcentrics, gKey="HS_ShapeConcentrics"} return t end function HS_ShapeConcentrics:About() local body = [==[ This tool creates concentric polygons: each is a separate shape with the largest at the bottom of the stack. Two data entry fields are enabled: the number of sides on each polygon which can be between 3 and 24 inclusive; the number of concentric shapes which can be between 1 and 24 inclusive. Ctrl rounds the points (they are peaked by default). Shift makes the height and width the same. Alt draws form the centre. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeConcentrics:LoadPrefs(prefs) self.Concs = prefs:GetInt("HS_ShapeConcentrics.Concs", 2) self.Sides = prefs:GetInt("HS_ShapeConcentrics.Sides", 6) end function HS_ShapeConcentrics:SavePrefs(prefs) prefs:SetInt("HS_ShapeConcentrics.Concs", self.Concs) prefs:SetInt("HS_ShapeConcentrics.Sides", self.Sides) end function HS_ShapeConcentrics:ResetPrefs() self.Concs = 2 self.Sides = 6 end function HS_ShapeConcentrics:Labels () HS_Shape:SetInt1 ("Sides on Polygon", self.Sides) HS_Shape:SetInt2 ("Concentric shapes", self.Concs) end function HS_ShapeConcentrics:BigN (moho, view, value) self.Sides = LM.Clamp(value, 3, bigNmed) return self.Sides end function HS_ShapeConcentrics:LittleN (moho, view, value) self.Concs = LM.Clamp(value, 1, bigNmed) return self.Concs end -- ====================================== function HS_ShapeConcentrics:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() -- how many points before we started?? self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end -- we add start points local drawPts = self.Sides local Curvature = MOHO.PEAKED if (mouseEvent.ctrlKey) then Curvature = HS_curvC * math.tan(math.pi/(2*self.Sides)) end local i, j for j = 0, self.Concs-1 do mesh:SelectNone() mesh:AddLonePoint(self.startVec, drawingFrame) for i = 0, self.Sides-1 do mesh:AppendPoint(self.startVec, drawingFrame) end local startPt = n + j*drawPts mesh:WeldPoints(startPt + drawPts, startPt, drawingFrame) for i = startPt, startPt+drawPts-1 do mesh:Point(i):SetCurvature(Curvature, drawingFrame) end mesh:SelectConnected() if (HS_Shape.autoFill) then local shapeID = moho:CreateShape(true, false, drawingFrame) if (shapeID >= 0) then if (not HS_Shape.autoOutline) then mesh:Shape(shapeID).fHasOutline = false end end elseif (HS_Shape.autoOutline) then moho:CreateShape(false, false, drawingFrame) end end for i = 0, (self.Sides * self.Concs)-1 do mesh:Point(i+n).fSelected = true end return self.Sides * self.Concs end function HS_ShapeConcentrics:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local i, j local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end if (mouseEvent.altKey) then -- draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) end local Curvature = MOHO.PEAKED if (mouseEvent.ctrlKey) then Curvature = HS_curvC * math.tan(math.pi/(2*self.Sides)) end local n = mesh:CountPoints() local ptID = n - (drawnPoints) -- the first one we drew (the numbers in the UI might have changed!) local dAngle = math.rad(360) / self.Sides -- angle between apex in regular polygon local angle = (math.rad(180) - dAngle)/2 -- starting position (in effect the "point" on an odd-numbered sided polygon) if (y2>y1) then angle = angle + math.rad(180) -- flip if mouse higher on the page... end local baseAngle = angle local scale = LM.Vector2:new_local() local offset = LM.Vector2:new_local() scale.x = math.abs(x2 - x1) / (math.sqrt(2) * self.Concs) scale.y = math.abs(y2 - y1) / (math.sqrt(2) * self.Concs) offset.x = (x1 + x2) / 2 offset.y = (y1 + y2) / 2 for j = self.Concs, 1, -1 do for i = 1, self.Sides do local pt = mesh:Point(ptID) ptID = ptID + 1 pt.fPos.x = (scale.x * j * math.cos(angle)) + offset.x pt.fPos.y = (scale.y * j * math.sin(angle)) + offset.y pt:SetCurvature(Curvature, drawingFrame) angle = angle + dAngle end angle = baseAngle end end -- ************************************************** -- ***************************************************************************************************************************************** LINE -- ************************************************** HS_ShapeLine = {} -- for line local doParallel = 0 local doRadial = 1 local ctLineOpts = 2 HS_ShapeLine.startVec = LM.Vector2:new_local() HS_ShapeLine.Points = 12 HS_ShapeLine.Parallels = 1 -- how many lines HS_ShapeLine.Sep = 0.1 -- how far apart (moho units) HS_ShapeLine.WidthDef = false -- use stroke width? HS_ShapeLine.scale = 0.382 HS_ShapeLine.Opt = doParallel local oldLineSep -- to save the line separation if we overwrite with default stroke width function HS_ShapeLine:Identify() local t = {ident="Line", tip="Straight Line(s): Ctrl: points are not Peaked. If more than one line: use Options Button to create Parallel or Radial. Shift and Alt depend on option.", icon="hs_line", self=HS_ShapeLine, gKey="HS_ShapeLine"} return t end function HS_ShapeLine:About() local body = [==[ The Line tool has two main modes of operation: >> Generate one straight line with points evenly spaced along it. >> Generate several lines. Shift key makes the line horizontal or, with the Alt key also pressed, Vertical. Control makes the points rounded otherwise they are peaked. Two data entry fields are always enabled: >> the number of points on each line – this constrained to be between 2 and 72 inclusive. >> how many lines to draw If more than one line is to be drawn, there is a button that cycles through the options of which there are two: >> create multiple parallel lines – in which case the spacing may be set manually or, by checking the “base on stroke width” box, the spacing is set automatically to bring the lines close together so that a standard brush will fill the gap. This automatic setting may be further manually adjusted. >> create radial lines – in which case one data entry field is enabled – this defines the distance from the origin to the innermost points as a ratio of the line length. This is constrained to be between 0 and .990. Note that if this option is set to be very small, there will be multiple points created at the same (or very close) location and it may prove difficult to select the origin point for a specific line. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeLine:LoadPrefs(prefs) self.Points = prefs:GetInt("HS_ShapeLine.Points", 12) self.Parallels = prefs:GetInt("HS_ShapeLine.Parallels", 1) self.scale = prefs:GetFloat("HS_ShapeLine.scale", 0.382) self.Opt = prefs:GetInt("HS_ShapeLine.eOpt", 0) self.WidthDef = prefs:GetBool("HS_ShapeLine.WidthDef", false) self.Sep = prefs:GetFloat("HS_ShapeLine.Sep", .1) end function HS_ShapeLine:SavePrefs(prefs) prefs:SetInt("HS_ShapeLine.Points", self.Points) prefs:SetInt("HS_ShapeLine.Parallels", self.Parallels) prefs:SetInt("HS_ShapeLine.Opt", self.Opt) prefs:SetFloat("HS_ShapeLine.Sep", self.Sep) prefs:SetFloat("HS_ShapeLine.scale", self.scale) prefs:SetBool("HS_ShapeLine.WidthDef", self.WidthDef) end function HS_ShapeLine:ResetPrefs() self.Points = 12 self.Parallels = 1 self.Opt = 0 self.Sep = 0.1 self.WidthDef = false self.scale = 0.382 end function HS_ShapeLine:Labels () HS_Shape:SetInt1 ("Points on Line", self.Points) HS_Shape:SetInt2 ("Number of lines", self.Parallels) local buttText, floatText, floatVal if self.Parallels > 1 then if self.Opt == doParallel then buttText = "Parallel" floatText = "Spacing" floatVal = self.Sep HS_Shape:SetCheckBox ("Base on Stroke Width", self.WidthDef) elseif self.Opt == doRadial then buttText = "Radial" floatText = "Inner Radius Ratio" floatVal = self.scale end HS_Shape:SetFloat (floatText, floatVal) HS_Shape:SetButton (buttText) end end function HS_ShapeLine:BigN (moho, view, value) self.Points = LM.Clamp(value, 2, bigNmax) return self.Points end function HS_ShapeLine:LittleN (moho, view, value) self.Parallels = LM.Clamp(value, 1, gridMax) return self.Parallels end function HS_ShapeLine:Scale (moho, view, value) local rtx if self.Opt == doParallel then self.Sep = LM.Clamp(value, .001, 1) rtx = self.Sep elseif self.Opt == doRadial then self.scale = LM.Clamp(value, 0, .99) rtx = self.scale end return rtx end function HS_ShapeLine:Button (moho, view, value) self.Opt = math.fmod(self.Opt + 1, ctLineOpts) end function HS_ShapeLine:CheckBox (moho, view, value) self.WidthDef = value if self.WidthDef then oldLineSep = self.Sep self.Sep = moho.NewShapeLineWidth() * 2 -- factor actually depends on brush depth else self.Sep = oldLineSep end end -- ====================================== function HS_ShapeLine:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local i, j local vec = LM.Vector2:new_local() self.basePt = mesh:CountPoints() if (moho.gridOn) then moho:SnapToGrid(mouseEvent.drawingVec) end self.startVec:Set(mouseEvent.drawingVec) vec:Set(mouseEvent.drawingVec) for j = 0, self.Parallels - 1 do mesh:AddLonePoint(vec, drawingFrame) for i = 0, self.Points - 2 do mesh:AppendPoint(self.startVec, drawingFrame) end end mesh:SelectNone() for i = 0, (self.Points * self.Parallels)-1 do mesh:Point(i+self.basePt).fSelected = true end if (HS_Shape.autoOutline) then local shapeID = moho:CreateShape(false, false, drawingFrame) end return self.Points * self.Parallels end function HS_ShapeLine:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) if drawnPoints ~= self.Points * self.Parallels then return false -- break out if the UI numbers have changed -- can happen in edit mode end if self.Opt == doParallel then self:OnMouseMovedP(moho, mouseEvent, mesh, drawingFrame, drawnPoints) elseif self.Opt == doRadial then self:OnMouseMovedR(moho, mouseEvent, mesh, drawingFrame, drawnPoints) end end function HS_ShapeLine:OnMouseMovedP(moho, mouseEvent, mesh, drawingFrame, drawnPoints) if (moho.gridOn) then moho:SnapToGrid(mouseEvent.drawingStartVec) moho:SnapToGrid(mouseEvent.drawingVec) end local vec = LM.Vector2:new_local() local baseVec = LM.Vector2:new_local() baseVec:Set(mouseEvent.drawingVec) vec:Set(baseVec) local offset = LM.Vector2:new_local() local endShift = LM.Vector2:new_local() local orth = LM.Vector2:new_local() offset:Set(self.startVec) offset = vec - offset orth = offset:GetOrthogonal() local theta = math.atan2(orth.y, orth.x) local i, k local j = self.Points local n = mesh:CountPoints() offset:Set(self.startVec) for k = self.Parallels-1, 0, -1 do for i = 1, j do local percent = (i - 1) / (j - 1) local deltaX = percent * (vec.x-offset.x) local deltaY = percent * (vec.y-offset.y) local xShift = deltaX local yShift = deltaY local pt = mesh:Point(n - (i + k * j)) if (mouseEvent.altKey) then if (mouseEvent.shiftKey) then xShift = 0 if theta < 0 then theta = - math.pi else theta = 0 end end else if (mouseEvent.shiftKey) then yShift = 0 if theta < 0 then theta = - math.pi/2 else theta = math.pi/2 end end end endShift.x = k*self.Sep*math.cos(theta) endShift.y = k*self.Sep*math.sin(theta) pt.fPos.y = offset.y + yShift + endShift.y pt.fPos.x = offset.x + xShift + endShift.x -- line is Straight (spiky) unless CTRL local Curvature = MOHO.PEAKED if (mouseEvent.ctrlKey) then Curvature = HS_roundcorner end pt:SetCurvature(Curvature, drawingFrame) end end end function HS_ShapeLine:OnMouseMovedR(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end -- if not (mouseEvent.altKey) then -- always draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) -- end local i, j, k local n = mesh:CountPoints() - drawnPoints -- the first point we drew local dAngle = math.rad(360) / self.Parallels -- angle between apex in regular polygon local angle = (math.rad(180) - dAngle)/2 -- starting position (in effect the "point" on an odd-numbered sided polygon) if (y2>y1) then angle = angle + math.rad(180) -- flip if mouse higher on the page... end local scale = LM.Vector2:new_local() local offset = LM.Vector2:new_local() local ratio = 1 scale.x = math.abs(x2 - x1) / math.sqrt(2) scale.y = math.abs(y2 - y1) / math.sqrt(2) offset.x = (x1 + x2) / 2 offset.y = (y1 + y2) / 2 for i = 0, self.Points-1 do -- for each ring... ratio = 1 - i *((1 - self.scale) /(self.Points - 1)) for j = 0, self.Parallels - 1 do -- for each line k = j*self.Points + i local pt = mesh:Point(n + k) pt.fPos.x = ratio * (scale.x * math.cos(angle)) + offset.x pt.fPos.y = ratio * (scale.y * math.sin(angle)) + offset.y local Curvature = MOHO.PEAKED -- line is (peaked points) unless CTRL if (mouseEvent.ctrlKey) then Curvature = HS_roundcorner end pt:SetCurvature(Curvature, drawingFrame) angle = angle + dAngle end end end -- ************************************************** -- ************************************************************************************************************************************* SINE -- ************************************************** HS_ShapeSine = {} HS_ShapeSine.Points = 40 HS_ShapeSine.Cycles = 2 -- how many 360 degree cycles we want HS_ShapeSine.scale = 0.4 -- relative amplitude - sine local maxCycles = 12 local ptsPerCycle = 6 -- we need 6 points per cycle or more local startVec = LM.Vector2:new_local() local startPt function HS_ShapeSine:Identify() local t = {ident="Sine", tip="Sine Curve: Ctrl: separate points by 15 degrees. Shift: Horizontal axis. Alt: First cycle is downwards.", icon="hs_sine", self=HS_ShapeSine, gKey="HS_ShapeSine"} return t end function HS_ShapeSine:About() local body = [==[ This tool creates a Sine wave. Dragging the “end” point extends the curve from zero to the selected number of cycles until the whole curve has been generated and then it stretches the curve. The Control key constrains the curve so that fewer cycles are drawn if there are insufficient points to keep the curve properly shaped (12 points for a half-cycle). The Alt key inverts the curve. The Shift key keeps the line of zero amplitude horizontal. Three data entry fields are enabled: the number of points per full cycle (6 to 60 inclusive), the maximum number of cycles (1 to 12) the peak-to-peak size of the curve (amplitude) relative to the default frame size (0.001 to 10 inclusive). ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeSine:LoadPrefs(prefs) self.Points = prefs:GetInt("HS_ShapeSine.Points", 40) self.Cycles = prefs:GetInt("HS_ShapeSine.Cycles", 2) self.scale = prefs:GetFloat("HS_ShapeSine.scale", 0.4) -- sine amplitude wrt the render area end function HS_ShapeSine:SavePrefs(prefs) prefs:SetInt("HS_ShapeSine.Points", self.Points) prefs:SetInt("HS_ShapeSine.Cycles", self.Cycles) prefs:SetFloat("HS_ShapeSine.scale", self.scale) end function HS_ShapeSine:ResetPrefs() -- self.Points = 40 self.Cycles = 2 self.Points = self.Cycles * 10 self.scale = 0.3 end function HS_ShapeSine:Labels () HS_Shape:SetInt1 ("Points Per Cycle", self.Points) HS_Shape:SetInt2 ("Cycles of Sine", self.Cycles) HS_Shape:SetFloat ("Amplitude", self.scale) end function HS_ShapeSine:BigN (moho, view, value) self.Points = LM.Clamp(value, ptsPerCycle, 60) return self.Points end function HS_ShapeSine:LittleN (moho, view, value) self.Cycles = LM.Clamp(value, 1, maxCycles) return self.Cycles end function HS_ShapeSine:Scale (moho, view, value) self.scale = LM.Clamp(value, .001, 10) return self.scale end --============ function HS_ShapeSine:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local drawnPoints = self.Points * self.Cycles startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLine (moho, vec, drawingFrame, drawnPoints, HS_circleCorner) -->> improve curvature in MouseMoved mesh:Point(startPt):SetCurvature(MOHO.PEAKED, drawingFrame) mesh:Point(startPt + drawnPoints - 1):SetCurvature(MOHO.PEAKED, drawingFrame) return drawnPoints end function HS_ShapeSine:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local maxCurveAngle = math.rad(maxCycles*360) local maxAngle = LM.Clamp (self.Cycles * math.rad(360), 0, maxCurveAngle) if (mouseEvent.ctrlKey) then maxAngle = drawnPoints * math.rad(15) -- if control then each point is at 15 degrees (24 per cycle) end local radius = (vec - startVec):Mag() local totalAngle = radius * self.Cycles * math.rad(360) totalAngle = LM.Clamp (totalAngle, 0, maxAngle) -- we draw fewer cycles than demanded until radius exceeeds a threshold if (mouseEvent.altKey) then totalAngle = -totalAngle end -- ?? better spacing along x?? i.e. by y local n = mesh:CountPoints() for i = 1, drawnPoints do local percent = (i - 1) / (drawnPoints - 1) local deltaX = percent * (vec.x-startVec.x) local deltaTrig = percent * totalAngle local pt = mesh:Point(n - i) pt.fPos.x = startVec.x + deltaX local deltaTrigY = math.sin(deltaTrig) -- range -1:1 local yScale = self.scale local yShift if (mouseEvent.shiftKey) then yShift = 0 else yShift = percent * (vec.y-startVec.y) end pt.fPos.y = startVec.y + yShift + (deltaTrigY*yScale) end end -- ************************************************** -- ************************************************************************************************************************************* PARABOLA -- ************************************************** HS_ShapeParabola = {} HS_ShapeParabola.Points = 16 -- points on a quadratic curve HS_ShapeParabola.scale = 2 -- relative amplitude - parabola local startVec = LM.Vector2:new_local() local startPt function HS_ShapeParabola:Identify() local t = {ident="Parabola", tip="Parabola: Ctrl: points are Peaked. Shift: Horizontal axis. Alt: Apex is inverted.", icon="hs_quad", self=HS_ShapeParabola, gKey="HS_ShapeParabola"} return t end function HS_ShapeParabola:About() local body = [==[ This tool creates a parabolic line with points evenly spaced along its x axis. Shift key aligns the end points of the line horizontally. The apex of the curve is, by default, in the same quadrant as the end point, but with the Alt key pressed the apex of the curve is inverted. Control makes the points peaked otherwise they are rounded. Two data entry fields are enabled: the number of points, which is constrained to be between 3 and 72 inclusive the steepness of the curve (0.001 to 10 inclusive) – a smaller number makes a flatter curve. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeParabola:LoadPrefs(prefs) self.Points = prefs:GetInt("HS_ShapeParabola.Points", 6) self.scale = prefs:GetFloat("HS_ShapeParabola.scale", 2) -- parabola amplitude wrt the render area end function HS_ShapeParabola:SavePrefs(prefs) prefs:SetInt("HS_ShapeParabola.Points", self.Points) prefs:SetFloat("HS_ShapeParabola.scale", self.scale) end function HS_ShapeParabola:ResetPrefs() self.Points = 16 self.scale = 2 end function HS_ShapeParabola:Labels () HS_Shape:SetInt1 ("Points on Parabola", self.Points) HS_Shape:SetFloat ("Steepness", self.scale) end function HS_ShapeParabola:BigN (moho, view, value) self.Points = LM.Clamp(value, 3, bigNmax) return self.Points end function HS_ShapeParabola:Scale (moho, view, value) self.scale = LM.Clamp(value, .001, 10) return self.scale end -- ====================================== function HS_ShapeParabola:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local curv if (mouseEvent.ctrlKey) then curv = HS_roundcorner -- >>> better curvature.... else curv = MOHO.PEAKED end startVec:Set(HS_Shape:DrawingPos(moho, mouseEvent)) startPt = HS_Shape:MakeLine(moho, startVec, drawingFrame, self.Points, curv) return self.Points end function HS_ShapeParabola:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local offset = LM.Vector2:new_local() local vec = LM.Vector2:new_local() offset, vec = HS_Shape:DrawingBounds (moho, mouseEvent, false, false) -- -- ** calculate quadratic coeffs for the end points we have. rebase to be realtive to start; then at "p2" y=ax^2+bx+c so calc b -- local relX = (vec.x-offset.x) local relY = (vec.y-offset.y) local dirX = 1 local dirY = 1 local altDir = 1 if (mouseEvent.altKey) then altDir = -1 else altDir = 1 end if (mouseEvent.shiftKey) then relY = 0 end if relY < 0 then dirY = 1 -- curve apex is down else dirY = -1 -- curve apex is up end --[[ if relX < 0 then dirX = 1 else dirX = -1 end ]] local dir = dirX*dirY*altDir local quadB if math.abs(relX) < 1e-3 then quadB = 0 else quadB = (relY-(relX*relX*dir))/relX end local pt local j = drawnPoints local n = startPt + j for i = 1, j do local percent = (i - 1) / (j - 1) local deltaX = percent * relX local deltaY = percent * relY local xShift = deltaX local yShift = self.scale*((deltaX*deltaX*dir) + (deltaX*quadB)) pt = mesh:Point(n - i) pt.fPos.y = offset.y + yShift -- + deltaY pt.fPos.x = offset.x + xShift -- line is curved unless CTRL local Curvature = HS_roundcorner if (mouseEvent.ctrlKey) then Curvature = MOHO.PEAKED end pt:SetCurvature(Curvature, drawingFrame) end end -- ************************************************** -- ********************************************************************************************************************* CONTROL POLYGON (SubTool Disptach) -- ************************************************** HS_ShapeCtrl_Poly = {} HS_ShapeCtrl_Poly.Sides = 6 -- sides on a control polygon HS_ShapeCtrl_Poly.Concs = 1 -- how many concentric control polygons HS_ShapeCtrl_Poly.startVec = LM.Vector2:new_local() function HS_ShapeCtrl_Poly:Identify() local t = {ident="Tri/quadMesh", tip="Tri/Quad mesh: Shift: height=width. (Alt and Ctrl not used).", icon="hs_ctrl_web", self=HS_ShapeCtrl_Poly, gKey="HS_ShapeCtrl_Poly"} return t end function HS_ShapeCtrl_Poly:About() local body = [==[ This tool creates a "web". The innermost polygon is subdivided into triangles; each successive outer polygon is made up of quadrilaterals. Two data entry fields are enabled: the number of spokes on the “web” which can be between 3 and 48 inclusive; the number of concentric rings which can be between 1 and 24 inclusive. ]==] return body .. "\n" .. HS_Shape:UILabel() end function HS_ShapeCtrl_Poly:LoadPrefs(prefs) self.Sides = prefs:GetInt("HS_ShapeCtrl_Poly.Sides", 6) self.Concs = prefs:GetInt("HS_ShapeCtrl_Poly.Concs", 1) end function HS_ShapeCtrl_Poly:SavePrefs(prefs) prefs:SetInt("HS_ShapeCtrl_Poly.Sides", self.Sides) prefs:SetInt("HS_ShapeCtrl_Poly.Concs", self.Concs) end function HS_ShapeCtrl_Poly:ResetPrefs() self.Sides = 6 self.Concs = 1 end function HS_ShapeCtrl_Poly:Labels () HS_Shape:SetInt1 ("Radials", self.Sides) HS_Shape:SetInt2 ("Concentric Rings", self.Concs) end function HS_ShapeCtrl_Poly:BigN (moho, view, value) self.Sides = LM.Clamp(value, 3, gridMax) return self.Sides end function HS_ShapeCtrl_Poly:LittleN (moho, view, value) self.Concs = LM.Clamp(value, 1, bigNmed) return self.Concs end -- ====================================== function HS_ShapeCtrl_Poly:OnMouseDown(moho, mouseEvent, mesh, drawingFrame) local j if self.Concs == 1 then j = self:OnMouseDownI(moho, mouseEvent, mesh, drawingFrame) else j = self:OnMouseDownM(moho, mouseEvent, mesh, drawingFrame) end return j end function HS_ShapeCtrl_Poly:OnMouseMoved(moho, mouseEvent, mesh, drawingFrame, drawnPoints) if self.Concs == 1 then self:OnMouseMovedI(moho, mouseEvent, mesh, drawingFrame, drawnPoints) else self:OnMouseMovedM(moho, mouseEvent, mesh, drawingFrame, drawnPoints) end end -- ************************************************** -- ********************************************************************************************************************* CONTROL POLYGON (INT Triangles) -- ************************************************** function HS_ShapeCtrl_Poly:OnMouseDownI(moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() -- how many points before we started?? self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end -- we make the first polygon outline points local drawPts = self.Sides local i mesh:AddLonePoint(self.startVec, drawingFrame) for i = 0, drawPts-1 do mesh:AppendPoint(self.startVec, drawingFrame) end -- then lose one by welding mesh:WeldPoints(n + drawPts, n, drawingFrame) -- now the lines from the centre to the corners for i = 0, drawPts-1 do mesh:AddLonePoint(self.startVec, drawingFrame) mesh:AppendPoint(self.startVec, drawingFrame) mesh:WeldPoints(n + drawPts + i, n + i, drawingFrame) end -- next weld points in the centre for i = drawPts-1, 1, -1 do mesh:WeldPoints(n + drawPts + i, n + drawPts, drawingFrame) end local Curvature = MOHO.PEAKED for i = n, n+drawPts do mesh:Point(i):SetCurvature(Curvature, drawingFrame) end for i = 0, drawPts-1 do mesh:SelectNone() mesh:Point(i+n).fSelected = true if i == drawPts-1 then mesh:Point(n).fSelected = true else mesh:Point(i+n+1).fSelected = true end mesh:Point(n+drawPts).fSelected = true if (HS_Shape.autoFill) then local shapeID = moho:CreateShape(true, false, drawingFrame) if (shapeID >= 0) then if (not HS_Shape.autoOutline) then mesh:Shape(shapeID).fHasOutline = false end end elseif (HS_Shape.autoOutline) then moho:CreateShape(false, false, drawingFrame) end end mesh:SelectConnected() return drawPts+1 end function HS_ShapeCtrl_Poly:OnMouseMovedI(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end -- if not (mouseEvent.altKey) then -- always draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) -- end local n = mesh:CountPoints() -- local ptID = n - drawnPoints -- first to move is the first we drew (on the polygon edge) local numPoints = drawnPoints - 1 -- but we'll ignore the last drawn centre point local dAngle = math.rad(360) / numPoints -- angle between apex in regular polygon local angle = (math.rad(180) - dAngle)/2 -- starting position (in effect the "point" on an odd-numbered sided polygon) if (y2>y1) then angle = angle + math.rad(180) -- flip if mouse higher on the page... end local scale = LM.Vector2:new_local() local offset = LM.Vector2:new_local() scale.x = math.abs(x2 - x1) / math.sqrt(2) scale.y = math.abs(y2 - y1) / math.sqrt(2) offset.x = (x1 + x2) / 2 offset.y = (y1 + y2) / 2 for i = 1, numPoints do local pt = mesh:Point(ptID) ptID = ptID + 1 pt.fPos.x = (scale.x * math.cos(angle)) + offset.x pt.fPos.y = (scale.y * math.sin(angle)) + offset.y angle = angle + dAngle end end -- ************************************************** -- ********************************************************************************************************************************** CONCENTRIC CONTROL POLYGONS -- ************************************************** function HS_ShapeCtrl_Poly:OnMouseDownM(moho, mouseEvent, mesh, drawingFrame) local n = mesh:CountPoints() -- how many points before we started?? self.startVec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(self.startVec) end -- we add start points local drawPts = self.Sides local i, j -- first the concentrics for j = 0, self.Concs-1 do mesh:SelectNone() mesh:AddLonePoint(self.startVec, drawingFrame) for i = 0, self.Sides-1 do mesh:AppendPoint(self.startVec, drawingFrame) end local startPt = n + j*drawPts mesh:WeldPoints(startPt + drawPts, startPt, drawingFrame) for i = startPt, startPt+drawPts-1 do mesh:Point(i):SetCurvature(Curvature, drawingFrame) end end -- now the lines from the centre to the corners mesh:AddLonePoint(self.startVec, drawingFrame) drawPts = self.Sides * self.Concs -- how many points we've drawn for i = 0, self.Sides-1 do mesh:AddLonePoint(self.startVec, drawingFrame) for j = 0, self.Concs-1 do mesh:AppendPoint(self.startVec, drawingFrame) end for j = self.Concs-1, 0, -1 do mesh:WeldPoints(n + drawPts + i + j, n + i + j*self.Sides, drawingFrame) end end -- next weld points in the centre for i = self.Sides-1, 1, -1 do mesh:WeldPoints(n + drawPts + i, n + drawPts, drawingFrame) end -- do the inner ring triangle shapes: (high numbered points) local s = n + ((self.Concs - 1) * self.Sides) for i = 0, self.Sides-1 do mesh:SelectNone() mesh:Point(i+s).fSelected = true if i == self.Sides-1 then mesh:Point(s).fSelected = true else mesh:Point(i+s+1).fSelected = true end mesh:Point(n+drawPts).fSelected = true -- centre if (HS_Shape.autoFill) then local shapeID = moho:CreateShape(true, false, drawingFrame) if (shapeID >= 0) then if (not HS_Shape.autoOutline) then mesh:Shape(shapeID).fHasOutline = false end end elseif (HS_Shape.autoOutline) then moho:CreateShape(false, false, drawingFrame) end end -- save the outer quads for later... local p = {} local loop = {} for i = 1, self.Sides do loop [i] = i-1 end loop [#loop+1] = 0 for i = 0, self.Concs-2 do for j = 1, self.Sides do p[1] = n + loop[j] + i * self.Sides p[2] = n + loop[j+1] + i * self.Sides p[3] = n + loop[j] + (i+1) * self.Sides p[4] = n + loop[j+1] + (i+1) * self.Sides table.insert (shapePoints, {p[1], p[2], p[3], p[4]}) end end for i = 0, self.Sides * self.Concs do mesh:Point(i+n).fSelected = true mesh:Point(i+n):SetCurvature(MOHO.PEAKED, drawingFrame) end return 1 + self.Sides * self.Concs -- the +1 is the middle one. end function HS_ShapeCtrl_Poly:OnMouseMovedM(moho, mouseEvent, mesh, drawingFrame, drawnPoints) local vec = LM.Vector2:new_local() vec:Set(mouseEvent.drawingVec) if (moho.gridOn) then moho:SnapToGrid(vec) end local i, j local x1 = self.startVec.x local x2 = vec.x local y1 = self.startVec.y local y2 = vec.y if (mouseEvent.shiftKey) then -- constrain width = height if (x2 > x1) then if (y2 > y1) then y2 = y1 + (x2 - x1) else y2 = y1 - (x2 - x1) end else if (y2 > y1) then y2 = y1 - (x2 - x1) else y2 = y1 + (x2 - x1) end end end -- if (mouseEvent.altKey) then -- always draw from center point x1 = x1 - (x2 - x1) y1 = y1 - (y2 - y1) -- end local n = mesh:CountPoints() local ptID = n - drawnPoints -- the first one we drew local dAngle = math.rad(360) / self.Sides -- angle between apex in regular polygon local angle = (math.rad(180) - dAngle)/2 -- starting position (in effect the "point" on an odd-numbered sided polygon) if (y2>y1) then angle = angle + math.rad(180) -- flip if mouse higher on the page... end local baseAngle = angle local scale = LM.Vector2:new_local() local offset = LM.Vector2:new_local() scale.x = math.abs(x2 - x1) / (math.sqrt(2) * self.Concs) scale.y = math.abs(y2 - y1) / (math.sqrt(2) * self.Concs) offset.x = (x1 + x2) / 2 offset.y = (y1 + y2) / 2 for j = self.Concs, 1, -1 do for i = 1, self.Sides do local pt = mesh:Point(ptID) ptID = ptID + 1 pt.fPos.x = (scale.x * j * math.cos(angle)) + offset.x pt.fPos.y = (scale.y * j * math.sin(angle)) + offset.y angle = angle + dAngle end angle = baseAngle end end -- ************************************************** -- ************************************************** -- ************************************************** -- ************************************************** -- ************************************************** -- ************************************************** -- ************************************************** -- *************************************************************************************************************************************** UI -- ************************************************** -- ************************************************** -- Sub tool control / info table -- ************************************************** HS_Shape.subToolDispatch = { HS_ShapeSector:Identify(), HS_ShapeOval:Identify(), HS_ShapeRectangle:Identify(), HS_ShapeBone:Identify(), HS_ShapeAnnulus:Identify(), HS_ShapeConcentrics:Identify(), HS_ShapePolygon:Identify(), HS_ShapeCtrl_Poly:Identify(), HS_ShapeRtTriangle:Identify(), HS_ShapeStar:Identify(), HS_ShapeCross:Identify(), HS_ShapeLine:Identify(), HS_ShapeSine:Identify(), HS_ShapeParabola:Identify(), HS_ShapeGrid:Identify(), HS_ShapeTri_Grid:Identify(), {ident="Void", LabelUI=HS_Shape.Labels_Void, self=HS_Shape, tip=nil, icon=nil, gKey="HS_Shape", about=HS_Shape.About_Void, load=nil, save=nil, reset=nil, onSel=nil, onExit=nil, mdn=nil, mmov=nil, alt=nil, bigN=nil, lilN=nil, rat=nil, but=nil, box=nil} } -- ************************************************** -- Tool options - data values -- ************************************************** HS_Shape.AUTOFILL = MOHO.MSG_BASE +10 HS_Shape.AUTOOUTLINE = MOHO.MSG_BASE + 11 HS_Shape.FRAME0 = MOHO.MSG_BASE + 12 -- Create shape on frame 0 if true / layerframe if not HS_Shape.EXTEND = MOHO.MSG_BASE + 20 -- a button to add extra functionality to the sub-tool buttons local extendMid = HS_Shape.EXTEND + 1 HS_Shape.ALT_EXTEND = extendMid + 1 -- Alt-button -- must be 2 bigger than extend HS_Shape.BIGN = MOHO.MSG_BASE + 30 -- points in a curve HS_Shape.LITTLEN = MOHO.MSG_BASE + 40 -- arms on a star etc HS_Shape.SCALE = MOHO.MSG_BASE + 50 -- scaling / roundness factor HS_Shape.BUTTON = MOHO.MSG_BASE + 60 -- the option button HS_Shape.ALT_BUTTON = HS_Shape.BUTTON + 1 -- alt + the option button HS_Shape.CHECK = MOHO.MSG_BASE + 70 -- the checkbox local extenderFncs = { {tag = "O", fnc = "onSel", tip = "Click or Alt-click for extended options."}, {tag = "?", fnc = "about", tip = "Tool Information", svc = "AboutTool"}, -- {tag = "A", fnc = "alt", tip = "Alternative option"}, {tag = "R", fnc = "reset", tip = "Reset tool options"} } local ctExtOpts = #extenderFncs local extenderChanged = true -- -- ** sub tool identifiers -- the message codes are assigned when the buttons are generated --- -- HS_Shape.First = MOHO.MSG_BASE + 102 HS_Shape.Last = HS_Shape.First + #HS_Shape.subToolDispatch - 2 -- set a default value -- -- ** message codes for alt-button (reserved for "edit") are asigned when generating the buttons -- HS_Shape.ALT_First = MOHO.MSG_BASE + 202 HS_Shape.ALT_Last = HS_Shape.ALT_First + #HS_Shape.subToolDispatch - 2 local longSpace = " " -- a sequence of spaces -- ************************************************** -- Create and respond to tool's UI -- ************************************************** function HS_Shape:DoLayout(moho, layout) -- -- find the path to the subtool icons local path = moho:UserAppDir() local pc = string.find(path,"\\") local mac = string.find(path,"/") local delim = "" if mac ~= nil then delim = "/" end if pc ~= nil then delim = "\\" end local i path = "ScriptResources" .. delim .. "hs_shape_icons" .. delim -- ?? offer alternative path for plug-ins? -- -- how many subtools did we get from "initialise"? -- HS_Shape.Last = HS_Shape.First + #HS_Shape.toolSeq - 1 HS_Shape.ALT_Last = HS_Shape.ALT_First + #HS_Shape.toolSeq - 1 self.shapeButtons = {} self.autoFillCheck = LM.GUI.CheckBox("Auto-fill", self.AUTOFILL) layout:AddChild(self.autoFillCheck) self.autoOutlineCheck = LM.GUI.CheckBox("Auto-stroke", self.AUTOOUTLINE) layout:AddChild(self.autoOutlineCheck) layout:AddPadding(10) --[[ if LM.GUI.ShortButton then -- this widget added in 13.5.2 but doesn't support alt message self.extenderButt = LM.GUI.ShortButton(extenderFncs[1].tag, self.EXTEND) else self.extenderButt = LM.GUI.Button(extenderFncs[1].tag, self.EXTEND) end ]] self.extenderButt = LM.GUI.Button(extenderFncs[1].tag, self.EXTEND) self.extenderButt:SetAlternateMessage(self.ALT_EXTEND) layout:AddChild(self.extenderButt) layout:AddPadding(10) for i = 1, #HS_Shape.toolSeq do if self.subToolDispatch[HS_Shape.toolSeq[i]].icon then table.insert(self.shapeButtons, LM.GUI.ImageButton(path .. self.subToolDispatch[HS_Shape.toolSeq[i]].icon, self.subToolDispatch[HS_Shape.toolSeq[i]].tip, true, i-1+HS_Shape.First, true)) else table.insert(self.shapeButtons, LM.GUI.ImageButton(path .. "void", self.subToolDispatch[HS_Shape.toolSeq[i]].tip, true, i-1+HS_Shape.First, true)) end end layout:PushH(LM.GUI.ALIGN_CENTER, 0) for i, but in ipairs(self.shapeButtons) do layout:AddChild(but) but:SetAlternateMessage(i+HS_Shape.ALT_First-1) end layout:Pop() self.optionButton = LM.GUI.Button (" ", self.BUTTON) layout:AddChild(self.optionButton) self.optionButton:SetAlternateMessage(self.ALT_BUTTON) self.pointsText = LM.GUI.DynamicText("", 150) layout:AddChild(self.pointsText, LM.GUI.ALIGN_RIGHT) layout:AddPadding(-20) self.bigNIn = LM.GUI.TextControl (0, "000", self.BIGN, LM.GUI.FIELD_UINT) layout:AddChild(self.bigNIn, LM.GUI.ALIGN_RIGHT) self.cyclesText = LM.GUI.DynamicText("", 150) layout:AddChild(self.cyclesText, LM.GUI.ALIGN_RIGHT) layout:AddPadding(-20) self.littleNIn = LM.GUI.TextControl (0, "00", self.LITTLEN, LM.GUI.FIELD_UINT) layout:AddChild(self.littleNIn, LM.GUI.ALIGN_RIGHT) self.scaleText = LM.GUI.DynamicText("", 150) layout:AddChild(self.scaleText, LM.GUI.ALIGN_RIGHT) layout:AddPadding(-20) self.scaleIn = LM.GUI.TextControl (0, "0.0000", self.SCALE, LM.GUI.FIELD_UFLOAT) self.scaleIn:SetWheelInc(0.01) layout:AddChild(self.scaleIn, LM.GUI.ALIGN_RIGHT) layout:AddPadding(5) self.checkIn = LM.GUI.CheckBox("", self.CHECK) layout:AddChild(self.checkIn, LM.GUI.ALIGN_RIGHT) self.checkText = LM.GUI.DynamicText("", 150) layout:AddChild(self.checkText, LM.GUI.ALIGN_RIGHT) layout:AddPadding(5) self.drawOn0Check = LM.GUI.CheckBox("Create Shape on Frame 0?", self.FRAME0) layout:AddChild(self.drawOn0Check, LM.GUI.ALIGN_RIGHT) end function HS_Shape:UpdateWidgets(moho) self.autoFillCheck:SetValue(self.autoFill) self.autoOutlineCheck:SetValue(self.autoOutline) self.drawOn0Check:SetValue(self.drawOn0) for i, but in ipairs(self.shapeButtons) do but:SetValue(self.buttonNum == HS_Shape.First + i - 1) end self.DoInputLabels() end function HS_Shape:HandleMessage(moho, view, msg) local i, toolId, oldTool, temp toolId = 0 -- for the data fields if (self.buttonNum >= self.First) and (self.buttonNum <= self.Last) then toolId = HS_Shape.toolSeq[(self.buttonNum - self.First + 1)] -- HS_Test_harness:tracePrint (thisUUT, "last subtool was", self.buttonNum - self.First + 1, "gives tool:", toolId) else toolId = #self.subToolDispatch -- the "void" entry at the end of the dispatch table -- HS_Test_harness:tracePrint (thisUUT, (self.buttonNum - self.First + 1), "is not a valid shape type - default void", toolId) end oldTool = toolId -- the tool id on entry if (msg == self.AUTOFILL) then self.autoFill = self.autoFillCheck:Value() elseif (msg == self.AUTOOUTLINE) then self.autoOutline = self.autoOutlineCheck:Value() elseif (msg == self.FRAME0) then self.drawOn0 = self.drawOn0Check:Value() elseif (msg == self.EXTEND) or (msg == self.ALT_EXTEND) then local inc = extendMid - msg self.extenderOpt = math.fmod(self.extenderOpt + ctExtOpts + inc, ctExtOpts) -- HS_Test_harness:tracePrint (thisUUT, "Extender butt press. Now opt", self.extenderOpt) extenderChanged = true HS_Shape:UpdateWidgets(moho) -- update anyway elseif (msg >= self.First and msg <= self.Last) then self.buttonNum = msg toolId = HS_Shape.toolSeq[(self.buttonNum - self.First + 1)] self.shapeType = HS_Shape.subToolDispatch[toolId].ident -- HS_Test_harness:tracePrint (thisUUT, "new subtool", self.buttonNum - self.First + 1, "gives new tool:", toolId, self.shapeType) for i, but in ipairs(self.shapeButtons) do but:SetValue(self.buttonNum == HS_Shape.First + i - 1) end local fnc = extenderFncs[self.extenderOpt+1].fnc -- the function in the addressed tool local rtx -- HS_Test_harness:tracePrint (thisUUT, "seeking extender fnc", fnc, toolId, self.shapeType) if fnc then if HS_Shape.subToolDispatch[toolId][fnc] then -- HS_Test_harness:tracePrint (thisUUT, "old tool was", oldTool, "Extfnc", extenderFncs[self.extenderOpt+1].fnc, toolId, self.shapeType) -- invoke the extender with "true" for first run rtx = self.subToolDispatch[toolId][fnc](self.subToolDispatch[toolId].self, moho, (oldTool ~= toolId), extenderChanged) extenderChanged = false local svc = extenderFncs[self.extenderOpt+1].svc if svc then -- the service here to handle any return self[svc] (HS_Shape, self.shapeType, rtx) end end end HS_Shape:UpdateWidgets(moho) -- update anyway elseif (msg >= self.ALT_First and msg <= self.ALT_Last) then self.buttonNum = msg - self.ALT_First + self.First toolId = HS_Shape.toolSeq[(msg - self.ALT_First + 1)] self.shapeType = self.subToolDispatch[toolId].ident -- HS_Test_harness:tracePrint (thisUUT, "Alt Function request for", toolId, self.shapeType) --[==[ alt key will be for edit // alt function is extender if HS_Shape.subToolDispatch[toolId].alt then -- HS_Test_harness:diagnosePrint (thisUUT, "Dispatch to alt Function", toolId, self.shapeType) self.subToolDispatch[toolId].alt(self.subToolDispatch[toolId].self) end ]==] HS_Shape:UpdateWidgets(moho) -- update anyway -- the tool was selected! elseif (msg == self.BIGN) then bigN = self.bigNIn:IntValue() if self.subToolDispatch[toolId].bigN then temp = self.subToolDispatch[toolId].bigN(self.subToolDispatch[toolId].self, moho, view, bigN) bigN = temp or bigN end self.bigNIn:SetValue(bigN) self.DoInputLabels() elseif (msg == self.LITTLEN) then littleN = self.littleNIn:IntValue() if self.subToolDispatch[toolId].lilN then temp = self.subToolDispatch[toolId].lilN(self.subToolDispatch[toolId].self, moho, view, littleN) littleN = temp or littleN end self.littleNIn:SetValue(littleN) self.DoInputLabels() elseif (msg == self.SCALE) then rat = self.scaleIn:FloatValue() if self.subToolDispatch[toolId].rat then temp = self.subToolDispatch[toolId].rat(self.subToolDispatch[toolId].self, moho, view, rat) rat = temp or rat end self.scaleIn:SetValue(rat) self.DoInputLabels() if (self.drawnPoints > 0) -- if we have something drawn that is of the same shape type, we'll assume it's being edited and (moho:CountSelectedPoints() == self.drawnPoints) and (self.startVec.x ~= nil) and (self.lastItem == self.shapeType) and (self.pointCt == moho:DrawingMesh():CountPoints()) and (self.lastUUID == moho.drawingLayer:UUID()) then self.editMode = true self.subToolDispatch[toolId].mmov(self.subToolDispatch[toolId].self, moho, FakeMouse, moho:Mesh(), drawingFrame, self.drawnPoints) if self.drawOn0 then moho:AddPointKeyframe(drawingFrame) moho:NewKeyframe(CHANNEL_POINT) moho:NewKeyframe(CHANNEL_CURVE) else moho:AddPointKeyframe(moho.drawingFrame) moho:NewKeyframe(CHANNEL_POINT) moho:NewKeyframe(CHANNEL_CURVE) end moho:UpdateUI() end elseif (msg == self.BUTTON) then if self.subToolDispatch[toolId].but then self.subToolDispatch[toolId].but(self.subToolDispatch[toolId].self, moho, view, 1) end self.DoInputLabels() elseif (msg == self.ALT_BUTTON) then if self.subToolDispatch[toolId].but then self.subToolDispatch[toolId].but(self.subToolDispatch[toolId].self, moho, view, -1) end self.DoInputLabels() elseif (msg == self.CHECK) then box = HS_Shape.checkIn:Value() if self.subToolDispatch[toolId].box then temp = self.subToolDispatch[toolId].box(self.subToolDispatch[toolId].self, moho, view, HS_Shape.checkIn:Value()) if temp ~= nil then box = temp end end self.checkIn:SetValue(box) self.DoInputLabels() end end function HS_Shape:DoInputLabels() -- -- generate the labels for the input fields -- HS_Shape:Labels_Void() -- clear out all local i = 0 if (HS_Shape.buttonNum >= HS_Shape.First) and (HS_Shape.buttonNum <= HS_Shape.Last) then i = HS_Shape.buttonNum - HS_Shape.First + 1 if HS_Shape.subToolDispatch[HS_Shape.toolSeq[i]].LabelUI then HS_Shape.subToolDispatch[HS_Shape.toolSeq[i]].LabelUI(HS_Shape.subToolDispatch[HS_Shape.toolSeq[i]].self) end end local len = string.len (longSpace) bigNText = string.sub(longSpace .. bigNText, -len) -- right align the text by taking the righthand substring littleNText = string.sub(longSpace .. littleNText, -len) floatText = string.sub(longSpace .. floatText, -len) HS_Shape.pointsText:SetValue(bigNText) HS_Shape.cyclesText:SetValue(littleNText) HS_Shape.scaleText:SetValue(floatText) HS_Shape.optionButton:SetLabel(buttText, false) HS_Shape.checkText:SetValue(checkText) HS_Shape.extenderButt:SetLabel(extenderFncs[HS_Shape.extenderOpt+1].tag, false) HS_Shape.extenderButt:SetToolTip(extenderFncs[HS_Shape.extenderOpt+1].tip) HS_Shape.littleNIn:Redraw() HS_Shape.bigNIn:Redraw() HS_Shape.scaleIn:Redraw() HS_Shape.optionButton:Redraw() HS_Shape.checkText:Redraw() HS_Shape.extenderButt:Redraw() end
HS Shape
Listed
Author: hayasidist
View Script
Script type: Tool
Uploaded: Dec 10 2022, 06:57
Last modified: Jan 03 2023, 09:24
Draws various shapes
HS Shape version 10.01 – release date 3 January 2023
=====================================================
10.01: Bug fix. Error message generated when drawing but no moho shape is created. [The required path is, however, correctly created.]
What’s new in this version (since version 9.67)?
================================================
Totally re-architected. This version is the foundation for the next revision which is intended to allow user-written sub-tools to be accommodated.
Added the “additional functions” for help and re-set subtools (see below).
Enabled the “ratio / scale” (floating point input) so that it can be changed (and thus change the drawn shape) immediately after the shape is drawn.
A keystroke handler is introduced. Ctrl-key directs the keystroke to the factory SelectPoints keystroke handler (this only services “delete selected points” at present); key (or alt-key or shift-key) is handled within this tool – again only “delete selected points” is currently supported. This difference is that the factory tool will delete all selected points; this tool will only delete points from the last shape created by this tool if it is “editable”.
The main set of drawing tools is essentially unchanged from V967 (Moho 13.5.3) from 07 April 2022.
There is a new (version 1.04) sub-tool to draw card suit shapes. This is provided as a separate file in the Utility folder.
======================
This set of tools is based on the SHAPE tools that come with Moho.
This version will work only in Moho 12.0 or later.
Auto-fill and auto-stroke work as expected.
In general, the Shift and Alt keys work as the standard Shape tool. Exceptions are noted below.
Inputs
======
There is an “additional functions” button (to the left of the sub-tool buttons). This changes the behaviour of the sub-tool selection buttons.
Clicking (or alt-clicking) the button cycles forwards (or backwards) through the options:
>>> O – normal operation of the sub-tool
>>> R – reset the sub-tool’s options (and leave the sub-tool active)
>>> ? – print the sub-tool “help/about” message (and leave the sub-tool active)
There are 3 numerical entry fields in the menu bar: two integers and one floating point. The data entered is context sensitive depending on the tool selected. Values are preserved across invocations of each sub-tool.
Values set outside limits are reset without any error message.
Note that where the choice of input values will result in a large number of shapes being created (e.g., a square grid with 48*48 cells) the elapsed time taken to create all these shapes will be high. (Where the potential for large numbers of individual shapes exists, shapes are created on mouse up and not on mouse down.)
There is a check-box to force all drawing on frame 0 (as is the approach in Moho 13.5.3 and later). If this is not checked, the shape is created or modified on the active frame.
There is also an options button that is context dependent to allow further options depending on the active sub-tool.
Editing a shape
================
Having drawn a shape using a sub-tool it is possible to change the detail of the shape by, optionally, changing a numeric input field (other than those that directly or indirectly define the number of points in the shape) and then holding down the control key and simultaneously clicking and dragging the mouse. Once the edit operation is in progress the control key may be released if its shape modifying action is not required. Note that the shift and alt keys remain set as they were when the shape was created.
NEW>>> Also, where the sub-tool uses the floating point input to control scale / ratio / roundness (etc) this value can be modified immediately after the shape has been drawn to change the drawn shape.
NEW>>> If a shape is created on frame 0 (e.g. with the “create on frame 0” checkbox checked) then modified on a different frame (with the “create on frame 0” checkbox unchecked) the shape will be animated.
SUB-TOOLS SUMMARY
=====================
Circle Sector Oval Round Cornered Rectangle
Bone Influence Shape Annulus Concentric Polygons
Polygon Tri/Quad Mesh Right-angled triangle
Star Swiss Cross
Multi-point line Sine Parabola
Rectangular Grid Triangular Grid Card Suit Shapes
TOOL DESCRIPTIONS
==================
CIRCLE SECTOR
This tool creates a sector of a circle. Dragging the mouse creates a sector of up (but excluding) a full circle. Ctrl constrains to a semi-circle. Shift constrains the sector to quadrant or semi-circle; Alt moves the initial radius from vertical to horizontal.
One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive.
OVAL
Generates an ellipse. Ctrl forces the point curvatures to “circular” (otherwise the curvature is set to give the correct ellipsoidal shape – from a straight line where one dimension is very small to near circular). Shift distributes the points equidistant from the centre. Ctrl-Shift together create a circle.
One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive. If the number of points chosen is not divisible by 4 the input is reduced to the nearest lower multiple of 4.
ROUND CORNERED RECTANGLE
One data entry field is enabled: the radius of the semicircle that forms the corner as a ratio of the length of the smallest side. It is constrained to be between 0.01 and 0.4.
There is a checkbox to make the corners smoother (additional points are created).
BONE INFLUENCE SHAPE
This is similar to a round cornered rectangle but has semicircles on the short edge.
The style of creation can be selected by a button which, when pressed, cycles through the options:
>> use a bounding box created by the mouse click-drag to define the shape. The shape will be aligned horizontally or vertically and the dimensions will be set from the bounding box
>> use the mouse drag to define the vector along which the shape is created. In this case the “radius of the end semicircle” needs to be manually entered.
ANNULUS
Creates a single shape composed of two concentric ovals. Shift creates concentric circles.
One data entry field is enabled: the ratio of the width of the centre to outer radius; it is constrained to be between 0.001 and 0.9950 inclusive. (A large number creates a large hole in the centre)
CONCENTRIC POLYGONS
Creates concentric polygons: each is a separate shape with the largest at the bottom of the stack. Control makes the points rounded (default is peaked).
Two data entry fields are enabled: the number of sides on each polygon which can be between 3 and 24 inclusive and the number of concentric shapes which can be between 1 and 24 inclusive.
POLYGON
One data entry field is enabled: the number of sides which can be between 3 and 24 inclusive.
TRI/QUAD WARP MESH
Two data entry fields are enabled: the number of spokes on the “web” which can be between This can be between 3 and 48 inclusive; and the number of concentric rings which can be between 1 and 24 inclusive. The innermost polygon is subdivided into triangles; each successive outer polygon is made up of quadrilaterals.
RIGHT ANGLED TRIANGLE
This generates a right angled triangle. No data entry fields are enabled.
STAR
Creates a Star with the chosen number of arms. Ctrl makes the points rounded (otherwise they are sharp).
Two data entry fields are enabled: these are the number of arms on the star and the ratio of the width of the centre to total radius of the star. The star can have between 3 and 72 arms inclusive; the centre ratio can be between 0.001 and 0.9950 inclusive.
SWISS CROSS
Creates a Swiss Cross. Ctrl makes the points rounded (otherwise they are right angles).
One data entry field is enabled: width of the arm of the cross as a ratio of the shortest side. This can be between 0.01 and 0.99 inclusive.
MULTI-POINT LINE
There are two main modes of operation:
>> Generate one straight line with points evenly spaced along it.
>> Generate several lines.
Shift key makes the line horizontal or, with the Alt key also pressed, Vertical. Control makes the points rounded otherwise they are peaked.
Two data entry fields are always enabled:
>> the number of points on each line – this constrained to be between 2 and 72 inclusive.
>> how many lines to draw
If more than one line is to be drawn, there is a button that cycles through the options of which there are two:
>> create multiple parallel lines – in which case the spacing may be set manually or, by checking the “base on stroke width” box the spacing is set automatically to bring the lines close together so that a standard brush will fill the gap. This automatic setting may be further manually adjusted.
>> create radial lines – in which case one data entry field is enabled – this defines the distance from the origin to the innermost points as a ratio of the line length and this is constrained to be between 0 and .990. Note that if this option is set to be very small, there will be multiple points created at the same (or very close) location and it may prove difficult to select the origin point for a specific line.
SINE
Sine wave generates a curve drawn with the number of points to create the selected number of half-cycles and with a peak-to-peak amplitude as set by the scale factor. Dragging the “end” point extends the curve from zero to the selected number of cycles until the whole curve has been generated and then it stretches the curve. The Control key constrains the curve so that fewer cycles are drawn if there are insufficient points to keep the curve properly shaped (12 points for a half-cycle). The Alt key inverts the curve. The Shift key keeps the line of zero amplitude horizontal.
Three data entry fields are enabled: the number of points in the drawn curve (6 to 144 inclusive), the maximum number of cycles 1 to 12) and the peak-to-peak size of the curve (amplitude) relative to the default frame size (0.001 to 10 inclusive).
PARABOLA
This generates a parabolic line with points evenly spaced along its x axis. Shift key aligns the end points of the line horizontally. The apex of the curve is, by default, in the same quadrant as the end point, but with the Alt key pressed the apex of the curve is inverted. Control makes the points peaked otherwise they are rounded.
Two data entry fields are enabled: the number of points, which is constrained to be between 3 and 72 inclusive and curvature (0.001 to 10 inclusive) – a smaller number makes a flatter curve.
GRID
Creates a grid of the chosen number of rows and columns.
Two data entry fields are enabled: these are the number of rows and columns. Both are constrained to be between 1 and 48 inclusive.
There is an options button to select the type of fill. Clicking on the button cycles through the options. There are two options:
>> create just one shape for the grid lines and one for the background fill (if the relevant autostroke / autofill options are set)
>> create separate shapes for each cell
GRID OF TRIANGLES
The options button is used to select the style of the grid of triangles. Clicking on the button cycles through the options. There are three grid variants:
>> a grid based on hexagons - two data entry fields are enabled: these are the number of rows and the number of triangles per row. The number of rows is constrained to be between 1 and 48 inclusive; the number of triangles per row is constrained to be between 2 and 48 inclusive. This produces a “ragged edge” grid of identical triangles.
>> a grid based on squares with alternate diagonals
>> a grid based on squares with the same diagonals
In these last two options two data entry fields are enabled: these are the number of rows and the number of squares per row. Both are constrained to be between 1 and 48 inclusive.
CARD SUIT SHAPES
This draws a club, diamond, heart or spade.
The options button is used to select the desired shape. Clicking on the button cycles through the options.
There is one checkbox to use a fill/stroke that is usual for the suit (red fill for diamond/heart, black fill for club/spade).
For club there is a "rotundness" value which adjusts the overall shape:
-- 0 is most spread out, 10 is the most compact
For diamond, there is a "curvature" value which adjusts the straightness of the edges:
-- 0 is straight, 10 is maximum curvature.
For heart and spade there is a "roundness" which adjusts the overall shape:
-- 0 is "pointy", 10 is roundest
Installation
============
Copy (optionally rename) folder hs_shapeV10-0 to a convenient location and run the “scripts / install script” script. When prompted select the relevant folder.
Notes and known issues
======================
The “install script” process has not been tested on a mac.
There is no Localisation support.
Shapes if created with inconsistent options, exhibit “interesting” behaviours. Oval, if drawn with few points that are forced to curvature (ctrl-key), creates a bone-like shape; Star, if created with a ratio of outer:inner radius of (nearly) 1 creates a polygon; Sine if created with few points creates some crazy loops; polygons with more than about 20 sides are virtually indistinguishable from ellipses / circles.
There are no known issues; but please let me know about errors, inconsistencies, unfriendly UI, ...
Requests for changes are always welcome. Just ask. If it’s easy it’ll get done quite quickly. If it’s hard ... well, it’ll go on the list.
Next Version of this tool
=========================
Planned changes for the next release(s) are:
>>> (over more than one release) Migrate sub-tools to be in separate .lua files
>>> provide more support for user-written sub-tools
>>> Additional features if/as requested
>>> Bug fixes if/as reported or resolved
This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 237

HS Shape
Listed
Author: hayasidist
View Script
Script type: Tool
Uploaded: Dec 10 2022, 06:57
Last modified: Jan 03 2023, 09:24
Draws various shapes
HS Shape version 10.01 – release date 3 January 2023
=====================================================
10.01: Bug fix. Error message generated when drawing but no moho shape is created. [The required path is, however, correctly created.]
What’s new in this version (since version 9.67)?
================================================
Totally re-architected. This version is the foundation for the next revision which is intended to allow user-written sub-tools to be accommodated.
Added the “additional functions” for help and re-set subtools (see below).
Enabled the “ratio / scale” (floating point input) so that it can be changed (and thus change the drawn shape) immediately after the shape is drawn.
A keystroke handler is introduced. Ctrl-key directs the keystroke to the factory SelectPoints keystroke handler (this only services “delete selected points” at present); key (or alt-key or shift-key) is handled within this tool – again only “delete selected points” is currently supported. This difference is that the factory tool will delete all selected points; this tool will only delete points from the last shape created by this tool if it is “editable”.
The main set of drawing tools is essentially unchanged from V967 (Moho 13.5.3) from 07 April 2022.
There is a new (version 1.04) sub-tool to draw card suit shapes. This is provided as a separate file in the Utility folder.
======================
This set of tools is based on the SHAPE tools that come with Moho.
This version will work only in Moho 12.0 or later.
Auto-fill and auto-stroke work as expected.
In general, the Shift and Alt keys work as the standard Shape tool. Exceptions are noted below.
Inputs
======
There is an “additional functions” button (to the left of the sub-tool buttons). This changes the behaviour of the sub-tool selection buttons.
Clicking (or alt-clicking) the button cycles forwards (or backwards) through the options:
>>> O – normal operation of the sub-tool
>>> R – reset the sub-tool’s options (and leave the sub-tool active)
>>> ? – print the sub-tool “help/about” message (and leave the sub-tool active)
There are 3 numerical entry fields in the menu bar: two integers and one floating point. The data entered is context sensitive depending on the tool selected. Values are preserved across invocations of each sub-tool.
Values set outside limits are reset without any error message.
Note that where the choice of input values will result in a large number of shapes being created (e.g., a square grid with 48*48 cells) the elapsed time taken to create all these shapes will be high. (Where the potential for large numbers of individual shapes exists, shapes are created on mouse up and not on mouse down.)
There is a check-box to force all drawing on frame 0 (as is the approach in Moho 13.5.3 and later). If this is not checked, the shape is created or modified on the active frame.
There is also an options button that is context dependent to allow further options depending on the active sub-tool.
Editing a shape
================
Having drawn a shape using a sub-tool it is possible to change the detail of the shape by, optionally, changing a numeric input field (other than those that directly or indirectly define the number of points in the shape) and then holding down the control key and simultaneously clicking and dragging the mouse. Once the edit operation is in progress the control key may be released if its shape modifying action is not required. Note that the shift and alt keys remain set as they were when the shape was created.
NEW>>> Also, where the sub-tool uses the floating point input to control scale / ratio / roundness (etc) this value can be modified immediately after the shape has been drawn to change the drawn shape.
NEW>>> If a shape is created on frame 0 (e.g. with the “create on frame 0” checkbox checked) then modified on a different frame (with the “create on frame 0” checkbox unchecked) the shape will be animated.
SUB-TOOLS SUMMARY
=====================
Circle Sector Oval Round Cornered Rectangle
Bone Influence Shape Annulus Concentric Polygons
Polygon Tri/Quad Mesh Right-angled triangle
Star Swiss Cross
Multi-point line Sine Parabola
Rectangular Grid Triangular Grid Card Suit Shapes
TOOL DESCRIPTIONS
==================
CIRCLE SECTOR
This tool creates a sector of a circle. Dragging the mouse creates a sector of up (but excluding) a full circle. Ctrl constrains to a semi-circle. Shift constrains the sector to quadrant or semi-circle; Alt moves the initial radius from vertical to horizontal.
One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive.
OVAL
Generates an ellipse. Ctrl forces the point curvatures to “circular” (otherwise the curvature is set to give the correct ellipsoidal shape – from a straight line where one dimension is very small to near circular). Shift distributes the points equidistant from the centre. Ctrl-Shift together create a circle.
One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive. If the number of points chosen is not divisible by 4 the input is reduced to the nearest lower multiple of 4.
ROUND CORNERED RECTANGLE
One data entry field is enabled: the radius of the semicircle that forms the corner as a ratio of the length of the smallest side. It is constrained to be between 0.01 and 0.4.
There is a checkbox to make the corners smoother (additional points are created).
BONE INFLUENCE SHAPE
This is similar to a round cornered rectangle but has semicircles on the short edge.
The style of creation can be selected by a button which, when pressed, cycles through the options:
>> use a bounding box created by the mouse click-drag to define the shape. The shape will be aligned horizontally or vertically and the dimensions will be set from the bounding box
>> use the mouse drag to define the vector along which the shape is created. In this case the “radius of the end semicircle” needs to be manually entered.
ANNULUS
Creates a single shape composed of two concentric ovals. Shift creates concentric circles.
One data entry field is enabled: the ratio of the width of the centre to outer radius; it is constrained to be between 0.001 and 0.9950 inclusive. (A large number creates a large hole in the centre)
CONCENTRIC POLYGONS
Creates concentric polygons: each is a separate shape with the largest at the bottom of the stack. Control makes the points rounded (default is peaked).
Two data entry fields are enabled: the number of sides on each polygon which can be between 3 and 24 inclusive and the number of concentric shapes which can be between 1 and 24 inclusive.
POLYGON
One data entry field is enabled: the number of sides which can be between 3 and 24 inclusive.
TRI/QUAD WARP MESH
Two data entry fields are enabled: the number of spokes on the “web” which can be between This can be between 3 and 48 inclusive; and the number of concentric rings which can be between 1 and 24 inclusive. The innermost polygon is subdivided into triangles; each successive outer polygon is made up of quadrilaterals.
RIGHT ANGLED TRIANGLE
This generates a right angled triangle. No data entry fields are enabled.
STAR
Creates a Star with the chosen number of arms. Ctrl makes the points rounded (otherwise they are sharp).
Two data entry fields are enabled: these are the number of arms on the star and the ratio of the width of the centre to total radius of the star. The star can have between 3 and 72 arms inclusive; the centre ratio can be between 0.001 and 0.9950 inclusive.
SWISS CROSS
Creates a Swiss Cross. Ctrl makes the points rounded (otherwise they are right angles).
One data entry field is enabled: width of the arm of the cross as a ratio of the shortest side. This can be between 0.01 and 0.99 inclusive.
MULTI-POINT LINE
There are two main modes of operation:
>> Generate one straight line with points evenly spaced along it.
>> Generate several lines.
Shift key makes the line horizontal or, with the Alt key also pressed, Vertical. Control makes the points rounded otherwise they are peaked.
Two data entry fields are always enabled:
>> the number of points on each line – this constrained to be between 2 and 72 inclusive.
>> how many lines to draw
If more than one line is to be drawn, there is a button that cycles through the options of which there are two:
>> create multiple parallel lines – in which case the spacing may be set manually or, by checking the “base on stroke width” box the spacing is set automatically to bring the lines close together so that a standard brush will fill the gap. This automatic setting may be further manually adjusted.
>> create radial lines – in which case one data entry field is enabled – this defines the distance from the origin to the innermost points as a ratio of the line length and this is constrained to be between 0 and .990. Note that if this option is set to be very small, there will be multiple points created at the same (or very close) location and it may prove difficult to select the origin point for a specific line.
SINE
Sine wave generates a curve drawn with the number of points to create the selected number of half-cycles and with a peak-to-peak amplitude as set by the scale factor. Dragging the “end” point extends the curve from zero to the selected number of cycles until the whole curve has been generated and then it stretches the curve. The Control key constrains the curve so that fewer cycles are drawn if there are insufficient points to keep the curve properly shaped (12 points for a half-cycle). The Alt key inverts the curve. The Shift key keeps the line of zero amplitude horizontal.
Three data entry fields are enabled: the number of points in the drawn curve (6 to 144 inclusive), the maximum number of cycles 1 to 12) and the peak-to-peak size of the curve (amplitude) relative to the default frame size (0.001 to 10 inclusive).
PARABOLA
This generates a parabolic line with points evenly spaced along its x axis. Shift key aligns the end points of the line horizontally. The apex of the curve is, by default, in the same quadrant as the end point, but with the Alt key pressed the apex of the curve is inverted. Control makes the points peaked otherwise they are rounded.
Two data entry fields are enabled: the number of points, which is constrained to be between 3 and 72 inclusive and curvature (0.001 to 10 inclusive) – a smaller number makes a flatter curve.
GRID
Creates a grid of the chosen number of rows and columns.
Two data entry fields are enabled: these are the number of rows and columns. Both are constrained to be between 1 and 48 inclusive.
There is an options button to select the type of fill. Clicking on the button cycles through the options. There are two options:
>> create just one shape for the grid lines and one for the background fill (if the relevant autostroke / autofill options are set)
>> create separate shapes for each cell
GRID OF TRIANGLES
The options button is used to select the style of the grid of triangles. Clicking on the button cycles through the options. There are three grid variants:
>> a grid based on hexagons - two data entry fields are enabled: these are the number of rows and the number of triangles per row. The number of rows is constrained to be between 1 and 48 inclusive; the number of triangles per row is constrained to be between 2 and 48 inclusive. This produces a “ragged edge” grid of identical triangles.
>> a grid based on squares with alternate diagonals
>> a grid based on squares with the same diagonals
In these last two options two data entry fields are enabled: these are the number of rows and the number of squares per row. Both are constrained to be between 1 and 48 inclusive.
CARD SUIT SHAPES
This draws a club, diamond, heart or spade.
The options button is used to select the desired shape. Clicking on the button cycles through the options.
There is one checkbox to use a fill/stroke that is usual for the suit (red fill for diamond/heart, black fill for club/spade).
For club there is a "rotundness" value which adjusts the overall shape:
-- 0 is most spread out, 10 is the most compact
For diamond, there is a "curvature" value which adjusts the straightness of the edges:
-- 0 is straight, 10 is maximum curvature.
For heart and spade there is a "roundness" which adjusts the overall shape:
-- 0 is "pointy", 10 is roundest
Installation
============
Copy (optionally rename) folder hs_shapeV10-0 to a convenient location and run the “scripts / install script” script. When prompted select the relevant folder.
Notes and known issues
======================
The “install script” process has not been tested on a mac.
There is no Localisation support.
Shapes if created with inconsistent options, exhibit “interesting” behaviours. Oval, if drawn with few points that are forced to curvature (ctrl-key), creates a bone-like shape; Star, if created with a ratio of outer:inner radius of (nearly) 1 creates a polygon; Sine if created with few points creates some crazy loops; polygons with more than about 20 sides are virtually indistinguishable from ellipses / circles.
There are no known issues; but please let me know about errors, inconsistencies, unfriendly UI, ...
Requests for changes are always welcome. Just ask. If it’s easy it’ll get done quite quickly. If it’s hard ... well, it’ll go on the list.
Next Version of this tool
=========================
Planned changes for the next release(s) are:
>>> (over more than one release) Migrate sub-tools to be in separate .lua files
>>> provide more support for user-written sub-tools
>>> Additional features if/as requested
>>> Bug fixes if/as reported or resolved
=====================================================
10.01: Bug fix. Error message generated when drawing but no moho shape is created. [The required path is, however, correctly created.]
What’s new in this version (since version 9.67)?
================================================
Totally re-architected. This version is the foundation for the next revision which is intended to allow user-written sub-tools to be accommodated.
Added the “additional functions” for help and re-set subtools (see below).
Enabled the “ratio / scale” (floating point input) so that it can be changed (and thus change the drawn shape) immediately after the shape is drawn.
A keystroke handler is introduced. Ctrl-key directs the keystroke to the factory SelectPoints keystroke handler (this only services “delete selected points” at present); key (or alt-key or shift-key) is handled within this tool – again only “delete selected points” is currently supported. This difference is that the factory tool will delete all selected points; this tool will only delete points from the last shape created by this tool if it is “editable”.
The main set of drawing tools is essentially unchanged from V967 (Moho 13.5.3) from 07 April 2022.
There is a new (version 1.04) sub-tool to draw card suit shapes. This is provided as a separate file in the Utility folder.
======================
This set of tools is based on the SHAPE tools that come with Moho.
This version will work only in Moho 12.0 or later.
Auto-fill and auto-stroke work as expected.
In general, the Shift and Alt keys work as the standard Shape tool. Exceptions are noted below.
Inputs
======
There is an “additional functions” button (to the left of the sub-tool buttons). This changes the behaviour of the sub-tool selection buttons.
Clicking (or alt-clicking) the button cycles forwards (or backwards) through the options:
>>> O – normal operation of the sub-tool
>>> R – reset the sub-tool’s options (and leave the sub-tool active)
>>> ? – print the sub-tool “help/about” message (and leave the sub-tool active)
There are 3 numerical entry fields in the menu bar: two integers and one floating point. The data entered is context sensitive depending on the tool selected. Values are preserved across invocations of each sub-tool.
Values set outside limits are reset without any error message.
Note that where the choice of input values will result in a large number of shapes being created (e.g., a square grid with 48*48 cells) the elapsed time taken to create all these shapes will be high. (Where the potential for large numbers of individual shapes exists, shapes are created on mouse up and not on mouse down.)
There is a check-box to force all drawing on frame 0 (as is the approach in Moho 13.5.3 and later). If this is not checked, the shape is created or modified on the active frame.
There is also an options button that is context dependent to allow further options depending on the active sub-tool.
Editing a shape
================
Having drawn a shape using a sub-tool it is possible to change the detail of the shape by, optionally, changing a numeric input field (other than those that directly or indirectly define the number of points in the shape) and then holding down the control key and simultaneously clicking and dragging the mouse. Once the edit operation is in progress the control key may be released if its shape modifying action is not required. Note that the shift and alt keys remain set as they were when the shape was created.
NEW>>> Also, where the sub-tool uses the floating point input to control scale / ratio / roundness (etc) this value can be modified immediately after the shape has been drawn to change the drawn shape.
NEW>>> If a shape is created on frame 0 (e.g. with the “create on frame 0” checkbox checked) then modified on a different frame (with the “create on frame 0” checkbox unchecked) the shape will be animated.
SUB-TOOLS SUMMARY
=====================
Circle Sector Oval Round Cornered Rectangle
Bone Influence Shape Annulus Concentric Polygons
Polygon Tri/Quad Mesh Right-angled triangle
Star Swiss Cross
Multi-point line Sine Parabola
Rectangular Grid Triangular Grid Card Suit Shapes
TOOL DESCRIPTIONS
==================
CIRCLE SECTOR
This tool creates a sector of a circle. Dragging the mouse creates a sector of up (but excluding) a full circle. Ctrl constrains to a semi-circle. Shift constrains the sector to quadrant or semi-circle; Alt moves the initial radius from vertical to horizontal.
One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive.
OVAL
Generates an ellipse. Ctrl forces the point curvatures to “circular” (otherwise the curvature is set to give the correct ellipsoidal shape – from a straight line where one dimension is very small to near circular). Shift distributes the points equidistant from the centre. Ctrl-Shift together create a circle.
One data entry field is enabled: this is the number of points and is constrained to be between 4 and 72 inclusive. If the number of points chosen is not divisible by 4 the input is reduced to the nearest lower multiple of 4.
ROUND CORNERED RECTANGLE
One data entry field is enabled: the radius of the semicircle that forms the corner as a ratio of the length of the smallest side. It is constrained to be between 0.01 and 0.4.
There is a checkbox to make the corners smoother (additional points are created).
BONE INFLUENCE SHAPE
This is similar to a round cornered rectangle but has semicircles on the short edge.
The style of creation can be selected by a button which, when pressed, cycles through the options:
>> use a bounding box created by the mouse click-drag to define the shape. The shape will be aligned horizontally or vertically and the dimensions will be set from the bounding box
>> use the mouse drag to define the vector along which the shape is created. In this case the “radius of the end semicircle” needs to be manually entered.
ANNULUS
Creates a single shape composed of two concentric ovals. Shift creates concentric circles.
One data entry field is enabled: the ratio of the width of the centre to outer radius; it is constrained to be between 0.001 and 0.9950 inclusive. (A large number creates a large hole in the centre)
CONCENTRIC POLYGONS
Creates concentric polygons: each is a separate shape with the largest at the bottom of the stack. Control makes the points rounded (default is peaked).
Two data entry fields are enabled: the number of sides on each polygon which can be between 3 and 24 inclusive and the number of concentric shapes which can be between 1 and 24 inclusive.
POLYGON
One data entry field is enabled: the number of sides which can be between 3 and 24 inclusive.
TRI/QUAD WARP MESH
Two data entry fields are enabled: the number of spokes on the “web” which can be between This can be between 3 and 48 inclusive; and the number of concentric rings which can be between 1 and 24 inclusive. The innermost polygon is subdivided into triangles; each successive outer polygon is made up of quadrilaterals.
RIGHT ANGLED TRIANGLE
This generates a right angled triangle. No data entry fields are enabled.
STAR
Creates a Star with the chosen number of arms. Ctrl makes the points rounded (otherwise they are sharp).
Two data entry fields are enabled: these are the number of arms on the star and the ratio of the width of the centre to total radius of the star. The star can have between 3 and 72 arms inclusive; the centre ratio can be between 0.001 and 0.9950 inclusive.
SWISS CROSS
Creates a Swiss Cross. Ctrl makes the points rounded (otherwise they are right angles).
One data entry field is enabled: width of the arm of the cross as a ratio of the shortest side. This can be between 0.01 and 0.99 inclusive.
MULTI-POINT LINE
There are two main modes of operation:
>> Generate one straight line with points evenly spaced along it.
>> Generate several lines.
Shift key makes the line horizontal or, with the Alt key also pressed, Vertical. Control makes the points rounded otherwise they are peaked.
Two data entry fields are always enabled:
>> the number of points on each line – this constrained to be between 2 and 72 inclusive.
>> how many lines to draw
If more than one line is to be drawn, there is a button that cycles through the options of which there are two:
>> create multiple parallel lines – in which case the spacing may be set manually or, by checking the “base on stroke width” box the spacing is set automatically to bring the lines close together so that a standard brush will fill the gap. This automatic setting may be further manually adjusted.
>> create radial lines – in which case one data entry field is enabled – this defines the distance from the origin to the innermost points as a ratio of the line length and this is constrained to be between 0 and .990. Note that if this option is set to be very small, there will be multiple points created at the same (or very close) location and it may prove difficult to select the origin point for a specific line.
SINE
Sine wave generates a curve drawn with the number of points to create the selected number of half-cycles and with a peak-to-peak amplitude as set by the scale factor. Dragging the “end” point extends the curve from zero to the selected number of cycles until the whole curve has been generated and then it stretches the curve. The Control key constrains the curve so that fewer cycles are drawn if there are insufficient points to keep the curve properly shaped (12 points for a half-cycle). The Alt key inverts the curve. The Shift key keeps the line of zero amplitude horizontal.
Three data entry fields are enabled: the number of points in the drawn curve (6 to 144 inclusive), the maximum number of cycles 1 to 12) and the peak-to-peak size of the curve (amplitude) relative to the default frame size (0.001 to 10 inclusive).
PARABOLA
This generates a parabolic line with points evenly spaced along its x axis. Shift key aligns the end points of the line horizontally. The apex of the curve is, by default, in the same quadrant as the end point, but with the Alt key pressed the apex of the curve is inverted. Control makes the points peaked otherwise they are rounded.
Two data entry fields are enabled: the number of points, which is constrained to be between 3 and 72 inclusive and curvature (0.001 to 10 inclusive) – a smaller number makes a flatter curve.
GRID
Creates a grid of the chosen number of rows and columns.
Two data entry fields are enabled: these are the number of rows and columns. Both are constrained to be between 1 and 48 inclusive.
There is an options button to select the type of fill. Clicking on the button cycles through the options. There are two options:
>> create just one shape for the grid lines and one for the background fill (if the relevant autostroke / autofill options are set)
>> create separate shapes for each cell
GRID OF TRIANGLES
The options button is used to select the style of the grid of triangles. Clicking on the button cycles through the options. There are three grid variants:
>> a grid based on hexagons - two data entry fields are enabled: these are the number of rows and the number of triangles per row. The number of rows is constrained to be between 1 and 48 inclusive; the number of triangles per row is constrained to be between 2 and 48 inclusive. This produces a “ragged edge” grid of identical triangles.
>> a grid based on squares with alternate diagonals
>> a grid based on squares with the same diagonals
In these last two options two data entry fields are enabled: these are the number of rows and the number of squares per row. Both are constrained to be between 1 and 48 inclusive.
CARD SUIT SHAPES
This draws a club, diamond, heart or spade.
The options button is used to select the desired shape. Clicking on the button cycles through the options.
There is one checkbox to use a fill/stroke that is usual for the suit (red fill for diamond/heart, black fill for club/spade).
For club there is a "rotundness" value which adjusts the overall shape:
-- 0 is most spread out, 10 is the most compact
For diamond, there is a "curvature" value which adjusts the straightness of the edges:
-- 0 is straight, 10 is maximum curvature.
For heart and spade there is a "roundness" which adjusts the overall shape:
-- 0 is "pointy", 10 is roundest
Installation
============
Copy (optionally rename) folder hs_shapeV10-0 to a convenient location and run the “scripts / install script” script. When prompted select the relevant folder.
Notes and known issues
======================
The “install script” process has not been tested on a mac.
There is no Localisation support.
Shapes if created with inconsistent options, exhibit “interesting” behaviours. Oval, if drawn with few points that are forced to curvature (ctrl-key), creates a bone-like shape; Star, if created with a ratio of outer:inner radius of (nearly) 1 creates a polygon; Sine if created with few points creates some crazy loops; polygons with more than about 20 sides are virtually indistinguishable from ellipses / circles.
There are no known issues; but please let me know about errors, inconsistencies, unfriendly UI, ...
Requests for changes are always welcome. Just ask. If it’s easy it’ll get done quite quickly. If it’s hard ... well, it’ll go on the list.
Next Version of this tool
=========================
Planned changes for the next release(s) are:
>>> (over more than one release) Migrate sub-tools to be in separate .lua files
>>> provide more support for user-written sub-tools
>>> Additional features if/as requested
>>> Bug fixes if/as reported or resolved
This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 237