-- ---------------------------------------------------- -- Provide Moho with the name of this script object -- ---------------------------------------------------- ScriptName = "SS_SVGImport" -- ************************************************** -- SVG Import - Import SVG vector files -- version: 01.12 MH12+ #521104 -- by: Sam Cogheil (SimplSam) -- ************************************************** --[[ ***** Licence & Warranty ***** Copyright 2022 - Sam Cogheil (SimplSam) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Conditions require preservation of copyright and license notices. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works. You can: Use - use/reuse freely, even commercially Adapt - remix, transform, and build upon for any purpose Share - redistribute the material in any medium or format Adapt / Share under the following terms: Attribution - You must give appropriate credit, provide a link to the Apache 2.0 license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. Licensed works, modifications and larger works may be distributed under different License terms and without source code. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. The Developer Sam Cogheil / SimplSam will not be liable for any direct, indirect or consequential loss of actual or anticipated - data, revenue, profits, business, trade or goodwill that is suffered as a result of the use of the software provided. ]] --[[ ***** SPECIAL THANKS to: * Stan (and team) @ MOHO Scripting -- https://mohoscripting.com * The friendly faces @ Lost Marble Moho forum -- https://www.lostmarble.com/forum * SVG ref docs: https://developer.mozilla.org/en-US/docs/Web/SVG/ (Team Mozilla) * SVG tech spec: https://www.w3.org/TR/SVG11/ (SVG Working Group / W3C Team) * Lua XML parse: https://github.com/manoelcampos/xml2lua (Manoel Campos da Silva Filho / Paul Chakravarti) * Arc to Path: https://stackoverflow.com/questions/30277646/svg-convert-arcs-to-cubic-bezier https://github.com/BigBadaboom/androidsvg/blob/master/androidsvg/src/main/java/com/caverock/androidsvg/utils/SVGAndroidRenderer.java (Paul LeBeau) ]] -- ---------------------------------------------------- -- General information about this script -- ---------------------------------------------------- SS_SVGImport = {} local SS_SVGImportDialog = {} local SS_SVGImportPrefsDialog = {} local xml2lua local handler local ss_tool_icon_bin_found function SS_SVGImport:Name() return "SS SVG Import" end function SS_SVGImport:Version() return "01.12 #5211" end function SS_SVGImport:UILabel() return "SVG Import" end function SS_SVGImport:Description() return "SVG Import Files" end function SS_SVGImport:Creator() return "Sam Cogheil (SimplSam)" end function SS_SVGImport:IsBeginnerScriptmoho() return false end function SS_SVGImport:IsEnabled(moho) return (moho.document:CurrentDocAction() == "") end function SS_SVGImport:IsRelevant(moho) if (not _did_resources_package_path) then package.path = package.path .. ";" .. moho:UserAppDir() .. "/scripts/ScriptResources/?.lua;" _did_resources_package_path = true end xml2lua = xml2lua or require("xml2lua.xml2lua") handler = handler or require("xml2lua.dom") self._MOHO_Version = moho:AppVersion() and moho:AppVersion():match("(%d+%.?%d*)")+0 or -1 return true end function SS_SVGImport:ColorizeIcon() return false end -- ************************************************** -- Recurring values -- ************************************************** SS_SVGImport._MOHO_Version = -1 SS_SVGImport.debugMode = false SS_SVGImport.laterMode = true SS_SVGImport.minify = true SS_SVGImport.minifyLayers = true SS_SVGImport.minifyLayersUnnamed = false SS_SVGImport.minifyGroups = true SS_SVGImport.minifyPostOp = false SS_SVGImport.recenter = true SS_SVGImport.expandGroups = false SS_SVGImport.browseFirst = false SS_SVGImport.hideDialog = false --= highlight missing image content SS_SVGImport.hilightImg = false SS_SVGImport.hilightImgColor = "#00FF00" SS_SVGImport.CIRCLE_CURV_MAGIC = 0.391379 -- the magic number for a circle curvature SS_SVGImport.STROKE_LINE_ENDS = {butt=0, round=1, square=1} -- mh 0 = !round, 1 round SS_SVGImport.SVG_COLOR = {['']='0,0,0', none='0,0,0', transparent='99,66,33', aliceblue='240,248,255', antiquewhite='250,235,215', aqua='0,255,255', aquamarine='127,255,212', azure='240,255,255', beige='245,245,220', bisque='255,228,196', black='0,0,0', blanchedalmond='255,235,205', blue='0,0,255', blueviolet='138,43,226', brown='165,42,42', burlywood='222,184,135', cadetblue='95,158,160', chartreuse='127,255,0', chocolate='210,105,30', coral='255,127,80', cornflowerblue='100,149,237', cornsilk='255,248,220', crimson='220,20,60', cyan='0,255,255', darkblue='0,0,139', darkcyan='0,139,139', darkgoldenrod='184,134,11', darkgray='169,169,169', darkgreen='0,100,0', darkgrey='169,169,169', darkkhaki='189,183,107', darkmagenta='139,0,139', darkolivegreen='85,107,47', darkorange='255,140,0', darkorchid='153,50,204', darkred='139,0,0', darksalmon='233,150,122', darkseagreen='143,188,143', darkslateblue='72,61,139', darkslategray='47,79,79', darkslategrey='47,79,79', darkturquoise='0,206,209', darkviolet='148,0,211', deeppink='255,20,147', deepskyblue='0,191,255', dimgray='105,105,105', dimgrey='105,105,105', dodgerblue='30,144,255', firebrick='178,34,34', floralwhite='255,250,240', forestgreen='34,139,34', fuchsia='255,0,255', gainsboro='220,220,220', ghostwhite='248,248,255', gold='255,215,0', goldenrod='218,165,32', gray='128,128,128', grey='128,128,128', green='0,128,0', greenyellow='173,255,47', honeydew='240,255,240', hotpink='255,105,180', indianred='205,92,92', indigo='75,0,130', ivory='255,255,240', khaki='240,230,140', lavender='230,230,250', lavenderblush='255,240,245', lawngreen='124,252,0', lemonchiffon='255,250,205', lightblue='173,216,230', lightcoral='240,128,128', lightcyan='224,255,255', lightgoldenrodyellow='250,250,210', lightgray='211,211,211', lightgreen='144,238,144', lightgrey='211,211,211', lightpink='255,182,193', lightsalmon='255,160,122', lightseagreen='32,178,170', lightskyblue='135,206,250', lightslategray='119,136,153', lightslategrey='119,136,153', lightsteelblue='176,196,222', lightyellow='255,255,224', lime='0,255,0', limegreen='50,205,50', linen='250,240,230', magenta='255,0,255', maroon='128,0,0', mediumaquamarine='102,205,170', mediumblue='0,0,205', mediumorchid='186,85,211', mediumpurple='147,112,219', mediumseagreen='60,179,113', mediumslateblue='123,104,238', mediumspringgreen='0,250,154', mediumturquoise='72,209,204', mediumvioletred='199,21,133', midnightblue='25,25,112', mintcream='245,255,250', mistyrose='255,228,225', moccasin='255,228,181', navajowhite='255,222,173', navy='0,0,128', oldlace='253,245,230', olive='128,128,0', olivedrab='107,142,35', orange='255,165,0', orangered='255,69,0', orchid='218,112,214', palegoldenrod='238,232,170', palegreen='152,251,152', paleturquoise='175,238,238', palevioletred='219,112,147', papayawhip='255,239,213', peachpuff='255,218,185', peru='205,133,63', pink='255,192,203', plum='221,160,221', powderblue='176,224,230', purple='128,0,128', red='255,0,0', rosybrown='188,143,143', royalblue='65,105,225', saddlebrown='139,69,19', salmon='250,128,114', sandybrown='244,164,96', seagreen='46,139,87', seashell='255,245,238', sienna='160,82,45', silver='192,192,192', skyblue='135,206,235', slateblue='106,90,205', slategray='112,128,144', slategrey='112,128,144', snow='255,250,250', springgreen='0,255,127', steelblue='70,130,180', tan='210,180,140', teal='0,128,128', thistle='216,191,216', tomato='255,99,71', turquoise='64,224,208', violet='238,130,238', wheat='245,222,179', white='255,255,255', whitesmoke='245,245,245', yellow='255,255,0', yellowgreen='154,205,50', rebeccapurple='102,51,153'} SS_SVGImport.fileMru = {} SS_SVGImport.FILE_MRU_MAX = 20 SS_SVGImport.didCancelCount = 0 SS_SVGImport.layerHasID = {} -- ************************************************** -- SS_SVGImportDialog -- ************************************************** SS_SVGImportDialog.RESET = MOHO.MSG_BASE + 01 SS_SVGImportDialog.SELECT_FILE = MOHO.MSG_BASE + 02 SS_SVGImportDialog.SELECT_RECENT = MOHO.MSG_BASE + 03 SS_SVGImportDialog.SELECT_DELETE = MOHO.MSG_BASE + 04 SS_SVGImportDialog.SCALE_ = MOHO.MSG_BASE + 10 SS_SVGImportDialog.MINIFY_ = MOHO.MSG_BASE + 30 SS_SVGImportDialog.MINIFY_LAYERS = SS_SVGImportDialog.MINIFY_ + 01 SS_SVGImportDialog.MINIFY_GROUPS = SS_SVGImportDialog.MINIFY_ + 02 SS_SVGImportDialog.MINIFY_POSTOP = SS_SVGImportDialog.MINIFY_ + 03 SS_SVGImportPrefsDialog.PREF_ = MOHO.MSG_BASE + 40 SS_SVGImportPrefsDialog.PREF_BROWSE_FIRST = SS_SVGImportPrefsDialog.PREF_ + 01 SS_SVGImportPrefsDialog.PREF_HIDE_DIALOG = SS_SVGImportPrefsDialog.PREF_ + 02 SS_SVGImportPrefsDialog.PREF_DEBUG_MODE = SS_SVGImportPrefsDialog.PREF_ + 03 SS_SVGImport.scaleFactors = {-.5, -1, -2, 0, .5, 1, 2} -- 0 == d#ivider, -ve == canvas, +ve == entity, 1 = 100% ... SS_SVGImport.scaleDefault = SS_SVGImportDialog.SCALE_ + 1 -- indx SS_SVGImport.scaleMode = SS_SVGImport.scaleDefault SS_SVGImport.filename = "" -- ************************************************** -- Preference functions -- ************************************************** function SS_SVGImport:LoadPrefs(prefs) self.filename = prefs:GetString("SS_SVGImport.filename", "") self.scaleMode = prefs:GetInt("SS_SVGImport.scaleMode", SS_SVGImport.scaleDefault) if (self.scaleMode < SS_SVGImportDialog.SCALE_ +1) or (self.scaleMode > SS_SVGImportDialog.SCALE_ + #self.scaleFactors) then self.scaleMode = self.scaleDefault end self.expandGroups = prefs:GetBool("SS_SVGImport.expandGroups", false) self.minify = prefs:GetBool("SS_SVGImport.minify", true) self.minifyLayers = prefs:GetBool("SS_SVGImport.minifyLayers", true) self.minifyLayersUnnamed = prefs:GetBool("SS_SVGImport.minifyLayersUnnamed", false) self.minifyGroups = prefs:GetBool("SS_SVGImport.minifyGroups", true) self.minifyPostOp = prefs:GetBool("SS_SVGImport.minifyPostOp", false) self.recenter = prefs:GetBool("SS_SVGImport.recenter", true) self.browseFirst = prefs:GetBool("SS_SVGImport.browseFirst", false) self.debugMode = prefs:GetBool("SS_SVGImport.debugMode", false) self.hideDialog = false for i = 1, self.FILE_MRU_MAX do self.fileMru[i] = prefs:GetString("SS_SVGImport.fileMru_" .. i, nil) end end function SS_SVGImport:SavePrefs(prefs) prefs:SetString("SS_SVGImport.filename", self.filename) prefs:SetInt("SS_SVGImport.scaleMode", self.scaleMode) prefs:SetBool("SS_SVGImport.expandGroups", self.expandGroups) prefs:SetBool("SS_SVGImport.minify", self.minify) prefs:SetBool("SS_SVGImport.minifyLayers", self.minifyLayers) prefs:SetBool("SS_SVGImport.minifyLayersUnnamed", self.minifyLayersUnnamed) prefs:SetBool("SS_SVGImport.minifyGroups", self.minifyGroups) prefs:SetBool("SS_SVGImport.minifyPostOp", self.minifyPostOp) prefs:SetBool("SS_SVGImport.recenter", self.recenter) prefs:SetBool("SS_SVGImport.browseFirst", self.browseFirst) prefs:SetBool("SS_SVGImport.debugMode", self.debugMode) for i = 1, self.FILE_MRU_MAX do if ((self.fileMru[i] ~= nil) and (self.fileMru[i] ~= "")) then prefs:SetString("SS_SVGImport.fileMru_" .. i, self.fileMru[i]) end end end function SS_SVGImport:ResetPrefs() self.scaleMode = SS_SVGImport.scaleDefault self.expandGroups = false self.minify = true self.minifyLayers = true self.minifyLayersUnnamed = false self.minifyGroups = true self.minifyPostOp = false self.recenter = true self.browseFirst = false self.hideDialog = false self.debugMode = false end -- ************************************************** -- The guts of this script -- ************************************************** function SS_SVGImport:Run(moho) local mohodoc = moho.document local docHigh = mohodoc:Height() local docWide = mohodoc:Width() local docRatio = docWide / docHigh local frame0 = 0 local wasFrame = moho.frame local laterPrint = "" local laterPrintCount = 0 local laterPrintMax = 20 local debugPrintCount = 0 local debugPrintMax = 20 self.tgCount = 0 -- totGrps self.tlCount = 0 -- totLyrs self.layerHasID = {} -- store defs, styles, clips, masks & gradients local defs = {} local styles = {} local clips = {} local masks = {} local grads = {} local filen, _didCancel, _didBrowse moho:SetSelLayer(mohodoc:Layer(mohodoc:CountLayers() -1), false, true) -- slct top local function ssRound(inNum, f) f = 10^(f or 2) return (LM.Round(inNum * f)/f) end local function dprint(...) -- debug print if (self.debugMode) then local mString = table.concat({...}, " ") if (debugPrintCount < debugPrintMax) then print(mString) debugPrintCount = debugPrintCount +1 else if (debugPrintCount == debugPrintMax) then print(" ... ") debugPrintCount = debugPrintCount +1 end end end end local function lprint(...) -- later print if (self.laterMode and self.debugMode) then if (laterPrintCount < laterPrintMax) then local mString = table.concat({...}, " ") laterPrint = laterPrint .. mString .. "\n" laterPrintCount = laterPrintCount +1 else if (laterPrintCount == laterPrintMax) then laterPrint = laterPrint .. " ... " .. "\n" laterPrintCount = laterPrintCount +1 end end end end local function xprint(...) -- local mString = "x " .. table.concat({...}, " ") -- print(mString) end -- Colors to RGB - Hex/A, Text, RGB/A local function Hex2rgb (color) if (color) then if (color:sub(1,4) == "rgba") then -- return color:match("(%d+),(%d+),(%d+),(%d*%.?%d+)") return color:match("%s*(%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*,%s*(%d*%.?%d+)%s*") elseif (color:sub(1,3) == "rgb") then return color:match("%s*(%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*") -- #wsbg 1.0 elseif (color:sub(1, 1) == '#') then -- hex -- Hex RGB #000 / #000000; RGBA #0000, #00000000 local hex = color:sub(2) if hex:len() == 6 then return tonumber("0x"..hex:sub(1,2)), tonumber("0x"..hex:sub(3,4)), tonumber("0x"..hex:sub(5,6)) elseif hex:len() == 3 then -- #000 return (tonumber("0x"..hex:sub(1,1))*17), (tonumber("0x"..hex:sub(2,2))*17), (tonumber("0x"..hex:sub(3,3))*17) elseif hex:len() == 4 then -- #000f return (tonumber("0x"..hex:sub(1,1))*17), (tonumber("0x"..hex:sub(2,2))*17), (tonumber("0x"..hex:sub(3,3))*17), (tonumber("0x"..hex:sub(4,4))*17)/255 elseif hex:len() == 8 then return tonumber("0x"..hex:sub(1,2)), tonumber("0x"..hex:sub(3,4)), tonumber("0x"..hex:sub(5,6)), tonumber("0x"..hex:sub(7,8))/255 else return 0,0,0 -- blk end else -- textname (assumed) if (color:sub(1,3) ~= "url") then -- not filter etc. if (self.SVG_COLOR[color]) then -- color lookup return self.SVG_COLOR[color]:match("(%d+),(%d+),(%d+)") else return 0,0,0 -- blk end else -- gradients local gradName _, _, gradName = color:find("url%((#.*)%)") local grad = gradName and grads[gradName] if (not grad) then lprint("No Gradient found for: ", gradName) return 106,108,110 -- mid grey (instead of filter, gradient) else -- cheap Mid color local _opacity, _red, _grn, _blu = 0, 0, 0, 0 for _, stop in ipairs(grad._ss_stops) do _opacity = _opacity + (stop._ss_styles["stop-opacity"] or 1) local _r, _g, _b = Hex2rgb(stop._ss_styles["stop-color"]) _red = _red + _r _grn = _grn + _g _blu = _blu + _b end _red = LM.Round(_red / #grad._ss_stops) _grn = LM.Round(_grn / #grad._ss_stops) _blu = LM.Round(_blu / #grad._ss_stops) _opacity = _opacity / #grad._ss_stops return _red, _grn, _blu, _opacity end end end else -- lprint("NIL Color @ ", self._name) -- debug color was nil return 0,0,0 --blk end end -- Tokenize Path e.g. vals="-0.43151.123.0.66 -8-3.4-15.1-8.2-21.1-14.2-6-6-10.8-13-14.2-21.1-3.4-8-5.1-16.6-5.1-25.8 -7.5e-4,0.347-77.8,0.0053 10e-4,.010410.002,0.01567" local function ParseSVGPath(_path) local out = {} for var, vals in _path:gmatch("([a-df-zA-DF-Z])([^a-df-zA-DF-Z]*)") do -- split by Directive (M, L, h) local line = { var } -- de-dot 1.5.6 > 1.5 .6 or 1.5-.6 > 1.5 -.6 vals = vals:gsub("(%.%d+)(%.)", "%1 %2") vals = vals:gsub("(%.%d+)(%.)", "%1 %2") vals = vals:gsub("(%d)(-[.]?%d)", "%1 %2") vals = vals:gsub("(%d)(-[.]?%d)", "%1 %2") for val in vals:gmatch("([+-]?[%deE.+-]+)") do -- split by Values (-21.1, -6, 7.5e-4) line[#line +1] = val end out[#out +1] = line end return out end local function normx(px) return (((px/docWide) * 2 * docRatio) -docRatio) end local function normy(py) return (((py/docHigh) * -2) +1) end local function denormx(x) return (docWide/2 + (x * (docHigh/2))) end local function denormy(y) return (docHigh/2 - (y * (docHigh/2))) end local function deltax(px) return ((px/docWide) * 2 * docRatio) end local function deltay(py) return ((py/docHigh) * -2) end local normX0, normY0 = normx(0), normy(0) -- show/hide browse dialog (if opt), allow reshow diag if 3x browse + cancel if (self.browseFirst or self.hideDialog) then filen = LM.GUI.OpenFile("Get me some SVGs ...") _didBrowse = true _didCancel = filen == "" if _didCancel then if (self.hideDialog) then self.didCancelCount = self.didCancelCount +1 if (self.didCancelCount < 3) then return else self.didCancelCount = 0 end else self.didCancelCount = 0 end end end SS_SVGImportDialog.filePathInputPre = "" if (not self.hideDialog or _didCancel) then if _didBrowse then SS_SVGImportDialog.filePathInputPre = filen end local dlog = SS_SVGImportDialog:new(moho) if (dlog:DoModal() == LM.GUI.MSG_CANCEL) then self.didCancelCount = 0 return end else if (not _didCancel) then self.filename = filen end end self.didCancelCount = 0 self.startTime = os.clock() self.filename = self.filename or "" -- local _, extName = self.filename:match"^(.*)%.([^%.]*)$" local _,filenameSafe, extName = self.filename:match("^(.*)[\\/](.+)%.([^%.]*)$") filenameSafe = filenameSafe or "" extName = extName or "" dprint("\n\n---------= ".. self:Name() .." ".. self:Version() .." =---------- [ " .. filenameSafe .." . ".. extName .." ] -----------------------\n\n") if (self.filename ~= "") and (extName == "svg") and os.rename(self.filename, self.filename) then if (wasFrame ~= 0) then moho:SetCurFrame(frame0) end local svgHandler = handler:new() local svgParser = xml2lua.parser(svgHandler) svgParser:parse(xml2lua.loadFile(self.filename)) local mesh = nil local rgbColor = LM.rgb_color:new_local() local xy1, xy2, vec = LM.Vector2:new_local(), LM.Vector2:new_local(), LM.Vector2:new_local() ----------------------- -- Prefind ID's and store them - with transforms (clip/mask) ----------------------- local function PrepSVG(current, _ref, level, params) level = level or 0 params = params or {} local node = current or current.node local name = node._name or "unnamed" local xtype = node._type or "untyped" local attributes = node._attr or {} local id = attributes.id or "" local inheritStyles, styleAttrib = {}, {} local passedStyles = params.styles or {} inheritStyles.fill = passedStyles.fill inheritStyles.opacity = passedStyles.opacity inheritStyles.stroke = passedStyles.stroke local function GetStyles(_attr) -- Kill redundancy todo local _attributes = _attr or attributes if (_attributes["class"] or _attributes["style"]) then if (_attributes["class"]) then for _class in _attributes["class"]:gmatch("([%w_-]+)") do -- split by alphanum _ - if (styles[_class]) then for token in styles[_class]:gmatch("([^;]+)") do local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)") if (style) then styleAttrib[style] = value end end end end end if (_attributes["style"]) then for token in _attributes["style"]:gmatch("([^;]+)") do local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)") if (style) then styleAttrib[style] = value end end end inheritStyles.fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "") or inheritStyles.fill -- inheritStyles.stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "") or inheritStyles.stroke if (styleAttrib["opacity"]) then inheritStyles.opacity = (inheritStyles.opacity or 1) * styleAttrib["opacity"] end if (_attr) then local _newStyle = {} for _varname, _varval in pairs(styleAttrib) do _newStyle[_varname] = _varval end return(_newStyle) -- optionally reqd by caller end end inheritStyles.fill = _attributes.fill or inheritStyles.fill end if (xtype == "ELEMENT") then if (name == "svg") or (name == "g") or (name == "defs") then local children = node._children or {} local _subref = {grCount=0, elCount=0, xlCount=0, gradRefs={}} GetStyles() for _, child in ipairs(children) do PrepSVG(child, _subref, level +1, {styles=inheritStyles}) end if ((name == "svg") or (name == "g")) then _subref.xlCount = _subref.xlCount + _subref.elCount node._ss_isgroup = true node._ss_grCount = _subref.grCount or 0 -- # of groups node._ss_elCount = _subref.elCount or 0 -- # of child draw elements node._ss_xlCount = _subref.xlCount or 0 _ref.grCount = _ref.grCount + 1 _ref.xlCount = _ref.xlCount + _subref.xlCount -- recurse elem count if (attributes["clip-path"] or styleAttrib["clip-path"]) or (attributes["mask"] or styleAttrib["mask"]) then node._ss_is_masked = true end if (name == "g") then defs["#" .. id] = node end elseif ((name == "defs") and (#_subref.gradRefs > 0)) then -- rerun Gradient defs with refs (href's) for _, child in ipairs(_subref.gradRefs) do PrepSVG(child, _subref, level +1, {styles=inheritStyles, doGradRef=true}) end end elseif ((name == "path") or (name == "circle") or (name == "ellipse") or (name == "line") or (name == "rect") or (name == "polyline") or (name == "polygon") or (name == "image")) then defs["#" .. id] = node -- raw node ref _ref.elCount = _ref.elCount + 1 elseif (name == "use") then node._ss_useref = attributes["xlink:href"] or attributes["href"] _ref.elCount = _ref.elCount + 1 elseif (name == "linearGradient") or (name == "radialGradient") then local _href = (attributes["xlink:href"] or attributes["href"]) if (_href and (not node._ss_gradref)) then -- has href. so delay processing node._ss_gradref = _href table.insert(_ref.gradRefs, node) else local children if (node._ss_gradref) then children = grads[node._ss_gradref] and grads[node._ss_gradref]._children or {} -- steal kids from ref else children = node._children or {} end local thisStops = {} for _, child in ipairs(children) do if (child._name == "stop") then if (child._attr["style"]) then child._ss_styles = GetStyles(child._attr) else child._ss_styles = {} child._ss_styles["offset"] = child._attr["offset"] child._ss_styles["stop-color"] = child._attr["stop-color"] child._ss_styles["stop-opacity"] = child._attr["stop-opacity"] or 1 end table.insert(thisStops, child) --store raw stops w/ style end end node._ss_stops = thisStops grads["#" .. id] = node -- raw ref end elseif (name == "clipPath") then if (attributes["transform"]) then local thisTrans = {} for token in attributes["transform"]:gmatch("%S+%([%deE ,.+-]+%)") do table.insert(thisTrans, token) --store raw end node._ss_transforms = thisTrans end clips["#" .. id] = node -- raw ref elseif (name == "mask") then if (attributes["transform"]) then local thisTrans = {} for token in attributes["transform"]:gmatch("%S+%([%deE ,.+-]+%)") do table.insert(thisTrans, token) --store raw end node._ss_transforms = thisTrans end GetStyles() node._ss_styles = inheritStyles masks["#" .. id] = node -- raw ref elseif (name == "style") then --styles local _text = (node._children[1]._text:gsub("%c", "")):gsub("}", "}\n") -- 1 liner, split by '}' for line in _text:gmatch("([%C]+)") do -- non special if (line:match("^%s*(.-)%s*$") ~= "") then --< trim local _, _, _styles, value = line:find("([%C]+)[{]%s*(.*%S+)%s*[}]") -- .st1, #st3, .st2 { fill-rule: bla; } for style in _styles:gmatch("%.([^%s,]+)") do if (style and value) then styles[style] = (styles[style] or "") .. value end end end end end end end ----------------------- -- Walk the walk ----------------------- local function WalkSVG(parentLayer, current, _ref, level, params) level = level or 0 params = params or {} local spaces = string.rep(" ",level) local node = current or current.node local name = node._name or "unnamed" local xtype = node._type or "untyped" local attributes = node._attr or {} local id = attributes.id or "" local hasFill, hasLine local hasZ = false local isPathStarted, seenM local transforms = {} local styleAttrib = {} local _label local isMasked = params.is_masked local useAttributes = {} local _fill, _stroke, _opacity, _stroke_opacity, _fill_opacity, _stroke_width, _stroke_end local inheritStyles = {} local passedStyles = params.styles or {} self._name = name inheritStyles.opacity = passedStyles.opacity inheritStyles.fill = passedStyles.fill inheritStyles.stroke = passedStyles.stroke for _, val in ipairs(params.transforms or {}) do table.insert(transforms, val) end local function Sign(x) return x >= 0 and 1 or -1 -- treat case 0 as 1 end local function InstantiateShape(shapeId, oriX, oriY) -- Transform color etc. local _alpha, _rgbA if (shapeId >= 0) then local shape = mesh:Shape(shapeId) if (id ~= "") then shape:SetName(id) end shape:MakePlain() -- rids Gradients etc inherited from Style panel if (shape.fInheritedStyle) or (shape.fInheritedStyle2) then shape:RemoveStyles() end shape.fHasFill = hasFill shape.fHasOutline = hasLine if (hasLine) then rgbColor.r, rgbColor.g, rgbColor.b, _rgbA = Hex2rgb(_stroke) _alpha = (_stroke_opacity or _opacity or 1) * 255 rgbColor.a = _rgbA and _rgbA * _alpha or _alpha shape.fMyStyle.fLineCol:SetValue(0, rgbColor) shape.fMyStyle.fLineWidth = (_stroke_width or 1) / docHigh _stroke_end = _stroke_end or "butt" shape.fMyStyle.fLineCaps = self.STROKE_LINE_ENDS[_stroke_end] and self.STROKE_LINE_ENDS[_stroke_end] or 0 end if (hasFill) then rgbColor.r, rgbColor.g, rgbColor.b, _rgbA = Hex2rgb(_fill) _alpha = (_fill_opacity or _opacity or 1) * 255 rgbColor.a = _rgbA and _rgbA * _alpha or _alpha shape.fMyStyle.fFillCol:SetValue(0, rgbColor) end if ((not hasFill) and (not hasLine) and (_fill ~= "none") and (_fill ~= "transparent")) then -- No Fill spec nor Line so ... Fill rgbColor.r, rgbColor.g, rgbColor.b = 0, 0, 0 rgbColor.a = (_fill_opacity or _opacity or 1) * 255 shape.fMyStyle.fFillCol:SetValue(0, rgbColor) shape.fHasFill = true end -- Transform dimensions local vec2 = LM.Vector2:new_local() for iTrans = #transforms, 1, -1 do local transform = transforms[iTrans] local _, _, command, token = transform:find("(%w+)[(](.*)[)]") if (command == "translate") then local _, _, _tx, _ty = token:find("([%deE.-]+)[ ,]*([%deE.-]*)") vec2:Set(deltax(_tx or 0), deltay((_ty and _ty~='') and _ty or 0)) mesh:PrepMovePoints() mesh:TranslatePoints(vec2) elseif (command == "scale") then local _, _, _sx, _sy = token:find("([%deE.-]+)[ ,]*([%deE.-]*)") mesh:PrepMovePoints() _sx = _sx or 1 _sy = (_sy and _sy~='') and _sy or _sx local flipHandles = Sign(_sx+0) ~= Sign(_sy+0) vec2:Set(normX0, normY0) if (_sx == _sy) then -- (~ 70-100x faster)! mesh:ScalePoints(_sx, _sy, vec2, flipHandles) else -- dist? local handles = {} for i =0, shape:CountPoints() -1 do local point = mesh:Point(shape:GetPoint(i)) local curve, _ptID = point:Curve(0) --, _ptID) local preVec = curve:GetControlHandle(_ptID, frame0, true) local pstVec = curve:GetControlHandle(_ptID, frame0, false) if (preVec ~= point.fPos) or (pstVec ~= point.fPos) then local handle = {cu=curve, pid=_ptID, preX=preVec.x, preY=preVec.y, pstX = pstVec.x, pstY = pstVec.y} table.insert(handles, handle) end end mesh:ScalePoints(_sx, _sy, vec2) -- 70% of time (28/40) for _, handle in ipairs(handles) do -- todo optimise - ignore same as point / use matrix? vec:Set((_sx * (handle.preX - normX0)) + normX0, (_sy * (handle.preY - normY0)) + normY0) handle.cu:SetControlHandle(handle.pid, vec, frame0, true) vec:Set((_sx * (handle.pstX - normX0)) + normX0, (_sy * (handle.pstY - normY0)) + normY0) handle.cu:SetControlHandle(handle.pid, vec, frame0, false) end end elseif (command == "rotate") then local _, _, _ra, _rx, _ry = token:find("([%deE.-]+)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)") _ra = math.rad(-_ra or 0) _rx = (_rx and _rx~='') and normx(_rx) or normX0 _ry = (_ry and _ry~='') and normy(_ry) or normY0 vec2:Set(_rx, _ry) mesh:PrepMovePoints() mesh:RotatePoints(_ra, vec2) -- todo origin elseif (command == "matrix") then -- X [a c e] [Sx*Cosθ Sy*-Sinθ Tx] -- Y [b d f] [Sx*Sinθ Sy*Cosθ Ty] -- Z [0 0 1] local _x, _y local _, _, _a, _b, _c, _d, _e, _f = token:find("([%deE.-]+)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)") -- todo check got 6 or ('' or 0) _e, _f = deltax(_e), deltay(_f) local flipHandles = Sign(_a+0) ~= Sign(_d+0) mesh:PrepMovePoints() for i =0, shape:CountPoints() -1 do local point = mesh:Point(shape:GetPoint(i)) _x = (_a * (point.fPos.x - normX0)) + (-_c * (point.fPos.y - normY0)) + _e _y = (-_b * (point.fPos.x - normX0)) + (_d * (point.fPos.y - normY0)) + _f point.fPos:Set(normX0 + _x, normY0 + _y) if (flipHandles) then point:FlipControlHandles(frame0) end end local _sx = math.sqrt(_a^2 + _b^2) local _sy = math.sqrt(_c^2 + _d^2) local _scaleFactor = ssRound(math.sqrt(_sx * _sy),4) -- Scale matrix LineWidth #wsbg 1.0 521030 if (shape.fHasOutline and (_scaleFactor ~= 1)) then local style = shape.fMyStyle style.fLineWidth = style.fLineWidth * _scaleFactor end elseif (command == "skewX") then local _, _, _ra = token:find("([%deE.-]+)") mesh:PrepMovePoints() local _tan = math.tan(math.rad(_ra)) for i =0, shape:CountPoints() -1 do local point = mesh:Point(shape:GetPoint(i)) point.fPos.x = point.fPos.x - (_tan * (point.fPos.y - normY0)) end elseif (command == "skewY") then local _, _, _ra = token:find("([%deE.-]+)") mesh:PrepMovePoints() local _tan = math.tan(math.rad(_ra)) for i =0, shape:CountPoints() -1 do local point = mesh:Point(shape:GetPoint(i)) point.fPos.y = point.fPos.y - (_tan * (point.fPos.x - normX0)) end end end moho:AddPointKeyframe(moho.drawingFrame, moho.drawingLayer, true) end end if (xtype == "ELEMENT") then local _subref = {grCount=0, elCount=0, hasLayer=false, hasMaskLayer=false} if (name == "svg") or (name == "g") or ((name == "use") and defs[node._ss_useref] and defs[node._ss_useref]._ss_isgroup) then --or (name == "svg") then if (name == "use") then node = defs[node._ss_useref] end local children = node._children or {} local group = parentLayer local didMakeGroup local _minifyGroups = self.minifyGroups and self.minify local _minifyLayers = self.minifyLayers and self.minify local _minifyUnnamed = ((not self.minifyLayersUnnamed) or id == "") --= Skip Create Group logic (pre-op consolidation) if (level == 0) or (node._ss_xlCount == 0) or _minifyGroups and _minifyUnnamed and (((not node._ss_is_masked) and (not isMasked) and ((node._ss_elCount + node._ss_grCount == 1) and -- #wsbg 1.0 only elCount ((not self.minifyLayersUnnamed) or (id == ""))) ) or ((not node._ss_is_masked) and _minifyLayers and (node._ss_grCount == 0))) then node._ss_group_layer = parentLayer -- print("GSkipped: ", name, " ", id) else group = moho:CreateNewLayer(MOHO.LT_GROUP) group = moho:LayerAsGroup(group) if (id ~= "") then group:SetName(id) self.layerHasID[group:UUID()] = true end node._ss_group_layer = group moho:PlaceLayerInGroup(group, parentLayer, true) group:Expand(self.expandGroups) if (params.mask_mode) then group:SetMaskingMode(MOHO.MM_ADD_MASK_INVIS) end _ref.grCount = _ref.grCount +1 self.tgCount = self.tgCount +1 -- totl groups didMakeGroup = true end --= Group Style #511220 if (attributes["class"] or attributes["style"]) then if (attributes["class"]) then for _class in attributes["class"]:gmatch("([%w_-]+)") do -- split by alphanum _ - if (styles[_class]) then for token in styles[_class]:gmatch("([^;]+)") do local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)") if (style) then styleAttrib[style] = value end end else -- Class/Style not found lprint("Class/GStyle not found: ", _class) end end end if (attributes["style"]) then -- can override for token in attributes["style"]:gmatch("([^;]+)") do local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)") if (style) then styleAttrib[style] = value end end end inheritStyles.fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "") or inheritStyles.fill inheritStyles.stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "") or inheritStyles.stroke if (styleAttrib["opacity"]) then inheritStyles.opacity = (inheritStyles.opacity or 1) * styleAttrib["opacity"] end -- inheritStyles.stroke_opacity = styleAttrib["stroke-opacity"] -- inheritStyles.fill_opacity = styleAttrib["fill-opacity"] -- inheritStyles.stroke_width = styleAttrib["stroke-width"] and styleAttrib["stroke-width"]:match("[%d|%.]+") end inheritStyles.fill = attributes.fill or inheritStyles.fill inheritStyles.stroke = attributes.stroke or inheritStyles.stroke if (attributes["opacity"]) then inheritStyles.opacity = (inheritStyles.opacity or 1) * attributes["opacity"] end -- inheritStyles.stroke_opacity = attributes["stroke-opacity"] -- inheritStyles.fill_opacity = attributes["fill-opacity"] -- inheritStyles.stroke_width = attributes["stroke-width"] and attributes["stroke-width"]:match("[%d|%.]+") -- Todo? if (attributes["clip-path"] or styleAttrib["clip-path"]) then local clipName if (attributes["clip-path"]) then _, _, clipName = attributes["clip-path"]:find("%((#.*)%)") else _, _, clipName = styleAttrib["clip-path"]:find("%((#.*)%)") end local clip = clipName and clips[clipName] if (not clip) then lprint("NoGClip for: ", clipName, " @ ", name) else clipName = clipName:sub(2) local _children = clip._children or {} if (#_children > 0) then if (self.debugMode) then group:SetUserTags("gcmask " .. clipName) end group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL) for _, child in ipairs(_children) do -- add mask layer(s) WalkSVG(group, child, _subref, level + 1, {transforms=transforms, mask_type="clip", mask_mode=MOHO.MM_ADD_MASK_INVIS, name_prefix="mask " .. clipName .. " ", is_mask=true}) end isMasked = true end end end if (attributes["mask"] or styleAttrib["mask"]) then local maskName if (attributes["mask"]) then _, _, maskName = attributes["mask"]:find("%((#.*)%)") else _, _, maskName = styleAttrib["mask"]:find("%((#.*)%)") end local mask = maskName and masks[maskName] if (not mask) then lprint("NoGMask for: ", maskName, " @ ", name) else maskName = maskName:sub(2) local _children = mask._children or {} if (#_children > 0) then if (self.debugMode) then group:SetUserTags("gmmask " .. maskName) end group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL) for _, child in ipairs(_children) do WalkSVG(group, child, _subref, level + 1, {styles=mask._ss_styles, transforms=transforms, mask_type="mask", mask_mode="SS_MASK_AUTO", name_prefix="mask " .. maskName .. " ", is_mask=true}) end isMasked = true end end end --= Group Transforms #511221 if (attributes["transform"]) then for token in attributes["transform"]:gmatch("%S+%([%deE ,.+-]+%)") do table.insert(transforms, token) --store raw #511223 end end for _, child in ipairs(children) do WalkSVG(node._ss_group_layer, child, _subref, level + 1, {parent_id=id, styles=inheritStyles, transforms=transforms, is_masked=isMasked, is_mask=params.is_mask}) -- *** end if (didMakeGroup) then _ref.hasLayer = false end elseif ((name == "circle") or (name == "ellipse") or (name == "line") or (name == "rect") or (name == "path") or (name == "polyline") or (name == "polygon") or (name == "use") or (name == "image")) then local def if (name == "use") then local ref = attributes["xlink:href"] or attributes["href"] -- href svg2.0 if (ref) then def = defs[ref] else lprint("NoRef for: ", name) end if (not def) then lprint("NoDef for: ", ref) else name = def._name or "unnamed" useAttributes = attributes -- swap to def attributes attributes = def._attr or {} id = useAttributes.id or attributes.id or "" if ((name == "circle") or (name == "ellipse") or (name == "line") or (name == "rect") or (name == "path") or (name == "polyline") or (name == "polygon") or (name == "image")) then -- nop else lprint(spaces .. " def Can't Handle: ", name) return end end end if (attributes["class"] or attributes["style"]) then if (attributes["class"]) then for _class in attributes["class"]:gmatch("([%w_-]+)") do -- split by alphanum _ - if (styles[_class]) then for token in styles[_class]:gmatch("([^;]+)") do local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)") if (style and value) then styleAttrib[style] = value end end else lprint("Class/Style not found: ", _class)-- Class/Style not found end end end if (attributes["style"]) then -- can override for token in attributes["style"]:gmatch("([^;]+)") do local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)") if (style and value) then styleAttrib[style] = value end end end _fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "") _stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "") _opacity = styleAttrib["opacity"] _stroke_opacity = styleAttrib["stroke-opacity"] _fill_opacity = styleAttrib["fill-opacity"] _stroke_width = styleAttrib["stroke-width"] and styleAttrib["stroke-width"]:match("[%d|%.]+") -- TODo px em etc. _stroke_end = styleAttrib["stroke-linecap"] and styleAttrib["stroke-linecap"]:gsub("%s+", "") end _fill = attributes.fill or _fill _stroke = attributes.stroke or _stroke _opacity = attributes["opacity"] or _opacity _stroke_opacity = attributes["stroke-opacity"] or _stroke_opacity _fill_opacity = attributes["fill-opacity"] or _fill_opacity _stroke_width = attributes["stroke-width"] and attributes["stroke-width"]:match("[%d|%.]+") or _stroke_width -- TODo _stroke_end = attributes["stroke-linecap"] and attributes["stroke-linecap"]:gsub("%s+", "") or _stroke_end if (not _fill) or (not _stroke) then if (useAttributes["style"]) then for token in useAttributes["style"]:gmatch("([^;]+)") do local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)") styleAttrib[style] = value end if (not _fill) then _fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "") or "#000000" end if (not _stroke) then _stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "") or "#000000" end if (not _opacity) then _opacity = styleAttrib["opacity"] end if (not _stroke_opacity) then _stroke_opacity = styleAttrib["stroke-opacity"] end if (not _fill_opacity) then _fill_opacity = styleAttrib["fill-opacity"] end if (not _stroke_width) then _stroke_width = styleAttrib["stroke-width"] and styleAttrib["stroke-width"]:match("[%d|%.]+") end if (not _stroke_end) then _stroke_end = styleAttrib["stroke-linecap"] and styleAttrib["stroke-linecap"]:gsub("%s+", "") end else if (not _fill) then _fill = useAttributes.fill or _fill end if (not _stroke) then _stroke = useAttributes.stroke or _stroke end if (not _opacity) then _opacity = useAttributes["opacity"] end if (not _stroke_opacity) then _stroke_opacity = useAttributes["stroke-opacity"] end if (not _fill_opacity) then _fill_opacity = useAttributes["fill-opacity"] end if (not _stroke_width) then _stroke_width = useAttributes["stroke-width"] and useAttributes["stroke-width"]:match("[%d|%.]+") end if (not _stroke_end) then _stroke_end = useAttributes["stroke-linecap"] and useAttributes["stroke-linecap"]:gsub("%s+", "") end end end _fill = _fill or inheritStyles.fill or "#000000" _stroke = _stroke or inheritStyles.stroke if (inheritStyles.opacity) then _opacity = (_opacity or 1) * inheritStyles.opacity end hasFill = _fill or "none" hasFill = (_fill ~= "none") and (_fill ~= "transparent") hasLine = (_stroke or "none") ~= "none" if ((attributes["clip-path"] or styleAttrib["clip-path"])) then local clipName if (attributes["clip-path"]) then _, _, clipName = attributes["clip-path"]:find("%((#.*)%)") else _, _, clipName = styleAttrib["clip-path"]:find("%((#.*)%)") end local clip = clipName and clips[clipName] if (not clip) then lprint("NoClip for: ", clipName, " @ ", name) else clipName = clipName:sub(2) local children = clip._children or {} if (#children > 0) then local group if (clipName ~= _ref.lastMask) or (not _ref.lastGroup) then group = moho:CreateNewLayer(MOHO.LT_GROUP) group = moho:LayerAsGroup(group) group:SetName(clipName) if (self.debugMode) then group:SetUserTags("cmask") end group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL) _ref.lastMask = clipName _ref.lastGroup = group moho:PlaceLayerInGroup(group, parentLayer, true) group:Expand(SS_SVGImport.expandGroups) for _, child in ipairs(children) do -- add mask layer(s) WalkSVG(group, child, _subref, level +1, {transforms=transforms, mask_type="clip", mask_mode=MOHO.MM_ADD_MASK_INVIS, name_prefix="mask ", is_mask=true}) end _ref.hasLayer = false else group = _ref.lastGroup --= reuse last mask group (avoid loads of single masked layers) end parentLayer = group isMasked = true _ref.grCount = _ref.grCount +1 self.tgCount = self.tgCount +1 end end end if ((attributes["mask"] or styleAttrib["mask"])) then local maskName if (attributes["mask"]) then _, _, maskName = attributes["mask"]:find("%((#.*)%)") else _, _, maskName = styleAttrib["mask"]:find("%((#.*)%)") end local mask = maskName and masks[maskName] if (not mask) then lprint("NoMask for: ", maskName, " @ ", name) else maskName = maskName:sub(2) local children = mask._children or {} if (#children > 0) then local group if (maskName ~= _ref.lastMask) or (not _ref.lastGroup) then group = moho:CreateNewLayer(MOHO.LT_GROUP) group = moho:LayerAsGroup(group) group:SetName(maskName) if (self.debugMode) then group:SetUserTags("mmask") end group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL) _ref.lastMask = maskName _ref.lastGroup = group moho:PlaceLayerInGroup(group, parentLayer, true) group:Expand(SS_SVGImport.expandGroups) for _, child in ipairs(children) do -- add masking layer(s) WalkSVG(group, child, _subref, level + 1, {styles=mask._ss_styles, transforms=transforms, mask_type="mask", mask_mode="SS_MASK_AUTO", name_prefix="mask ", is_mask=true}) end _ref.hasLayer = false else group = _ref.lastGroup --= reuse last mask group (avoid loads of single masked layers) end parentLayer = group isMasked = true _ref.grCount = _ref.grCount +1 self.tgCount = self.tgCount +1 end end end if (not params.is_mask) then -- lprint("not is_mask: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) ) if (isMasked and _ref.lastMask) then -- lprint(" last mask: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) ) self.lastWasMasked = true elseif self.lastWasMasked then -- lprint(" not but lastWasMasked: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) ) _ref.hasLayer = false _ref.lastMask = nil self.lastWasMasked = false else -- lprint(" not isMasked: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) ) _ref.lastMask = nil end end --= Create/Omit Vector Layer logic _minify local _dontMinifyUnnamed = ((self.minifyLayersUnnamed and self.minifyLayers and self.minify) and id ~= "") if (params.is_mask and ((not _ref.hasMaskLayer) or (params.mask_type == "mask" and _ref.maskFill ~= _fill))) or -- todo mask outline (not params.is_mask and not _ref.hasLayer) or (not self.minifyLayers) or (not self.minify) or (not params.is_mask and _dontMinifyUnnamed) then local layer = moho:CreateNewLayer(MOHO.LT_VECTOR) layer = moho:LayerAsVector(layer) moho:PlaceLayerInGroup(layer, parentLayer, true) local _parentPriority = (params.parent_id) and (params.parent_id ~= "") and (params.parent_id ~= parentLayer:Name()) and (not parentLayer:LayerByName(params.parent_id)) if (id ~= "") and not (_parentPriority) then -- use my id unless I alone & parentName priority layer:SetName((params.name_prefix or "") .. id ) self.layerHasID[layer:UUID()] = true else if _parentPriority then layer:SetName(params.parent_id) -- #wsbg 1.0 self.layerHasID[layer:UUID()] = true else -- force layer1 name if 1st if (parentLayer:CountLayers() == 1) then layer:SetName("Layer 1") end end if (params.name_prefix) then layer:SetName(params.name_prefix .. " " .. layer:Name()) end end mesh = layer:Mesh() -- do mask if (params.mask_mode) then if (params.mask_mode == "SS_MASK_AUTO") then rgbColor.r, rgbColor.g, rgbColor.b = Hex2rgb(_fill) if (rgbColor.r == 0) and (rgbColor.g == 0) and (rgbColor.b == 0) then layer:SetMaskingMode(MOHO.MM_SUB_MASK_INVIS) else --todo Stroke _opacity = (((rgbColor.r + rgbColor.g + rgbColor.b) / 3) * (_fill_opacity or _opacity or 1)) / 255 layer:SetMaskingMode(MOHO.MM_ADD_MASK_INVIS) end else layer:SetMaskingMode(params.mask_mode) end end if (params.is_mask) and (self._MOHO_Version >= 13.5) then layer:SetIgnoredByLayerPicker(true) -- mask layers unselectable end if (not params.is_mask) then -- prevent mask layers merging with draw layers _ref.hasLayer = true and (not _dontMinifyUnnamed) _ref.hasMaskLayer = false _ref.maskFill = nil else _ref.hasLayer = false _ref.hasMaskLayer = true _ref.maskFill = _fill end _ref.elCount = _ref.elCount +1 self.tlCount = self.tlCount +1 -- totl layrs end -- extract transforms for later use local _attributes = attributes["transform"] or useAttributes["transform"] if (_attributes) then -- #wsbg 1.0 #521030 for token in _attributes:gmatch("%S+%([%deE ,.+-]+%)") do table.insert(transforms, token) end end --======== Create Shapes ========-- if (name == "circle") then local cx = (useAttributes.x or 0) + (attributes.cx or 0) -- TODO - Can be px, pt etc. local cy = (useAttributes.y or 0) + (attributes.cy or 0) local r = attributes.r or 0 -- bounding box local x1 = cx -r local y1 = cy -r local x2 = cx +r local y2 = cy +r -- normalise x1 = ((x1/docWide) * 2 * docRatio) -docRatio x2 = ((x2/docWide) * 2 * docRatio) -docRatio cx = ((cx/docWide) * 2 * docRatio) -docRatio y1 = ((y1/docHigh) * -2) +1 y2 = ((y2/docHigh) * -2) +1 cy = ((cy/docHigh) * -2) +1 local pointsOffset = mesh:CountPoints() mesh:SelectNone() vec:Set(cx, y1) mesh:AddLonePoint(vec, frame0) -- top vec:Set(x2, cy) mesh:AppendPoint(vec, frame0) -- rgt vec:Set(cx, y2) mesh:AppendPoint(vec, frame0) -- bot vec:Set(x1, cy) mesh:AppendPoint(vec, frame0) -- lft vec:Set(cx, y1) mesh:AppendPoint(vec, frame0) -- top agin mesh:WeldPoints(pointsOffset + 4, pointsOffset, frame0) mesh:Point(pointsOffset + 0):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) mesh:Point(pointsOffset + 1):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) mesh:Point(pointsOffset + 2):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) mesh:Point(pointsOffset + 3):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) mesh:SelectConnected() local shapeID = moho:CreateShape(hasFill) InstantiateShape(shapeID, normx(0), normy(0)) elseif (name == "ellipse") then local cx = (useAttributes.x or 0) + (attributes.cx or 0) -- TODO - Can be px, pt etc. local cy = (useAttributes.y or 0) + (attributes.cy or 0) local rx = attributes.rx or 0 local ry = attributes.ry or 0 -- bounding box local x1 = cx -rx local y1 = cy -ry local x2 = cx +rx local y2 = cy +ry -- normalise x1 = ((x1/docWide) * 2 * docRatio) -docRatio x2 = ((x2/docWide) * 2 * docRatio) -docRatio cx = ((cx/docWide) * 2 * docRatio) -docRatio y1 = ((y1/docHigh) * -2) +1 y2 = ((y2/docHigh) * -2) +1 cy = ((cy/docHigh) * -2) +1 local pointsOffset = mesh:CountPoints() mesh:SelectNone() vec:Set(cx, y1) mesh:AddLonePoint(vec, frame0) -- top vec:Set(x2, cy) mesh:AppendPoint(vec, frame0) -- rgt vec:Set(cx, y2) mesh:AppendPoint(vec, frame0) -- bot vec:Set(x1, cy) mesh:AppendPoint(vec, frame0) -- lft vec:Set(cx, y1) mesh:AppendPoint(vec, frame0) -- top agin mesh:WeldPoints(pointsOffset + 4, pointsOffset, frame0) local f = math.abs(x2 - x1) - math.abs(y2 - y1) local eliFactor = .354 --< trial and err 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, 0.66667 / eliFactor) * eliFactor mesh:Point(pointsOffset + 0):SetCurvature(f, frame0) mesh:Point(pointsOffset + 2):SetCurvature(f, frame0) 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, 0.66667 / eliFactor) * eliFactor mesh:Point(pointsOffset + 1):SetCurvature(f, frame0) mesh:Point(pointsOffset + 3):SetCurvature(f, frame0) else mesh:Point(pointsOffset + 0):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) mesh:Point(pointsOffset + 1):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) mesh:Point(pointsOffset + 2):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) mesh:Point(pointsOffset + 3):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0) end mesh:SelectConnected() local shapeID = moho:CreateShape(hasFill) InstantiateShape(shapeID, cx, cy) elseif (name == "line") then local x1 = (useAttributes.x or 0) + (attributes.x1 or 0) local y1 = (useAttributes.y or 0) + (attributes.y1 or 0) local x2 = (useAttributes.x or 0) + (attributes.x2 or 0) local y2 = (useAttributes.y or 0) + (attributes.y2 or 0) -- normalise x1 = ((x1/docWide) * 2 * docRatio) -docRatio x2 = ((x2/docWide) * 2 * docRatio) -docRatio y1 = ((y1/docHigh) * -2) +1 y2 = ((y2/docHigh) * -2) +1 xy1:Set(x1,y1) xy2:Set(x2,y2) local pointsOffset = mesh:CountPoints() mesh:SelectNone() mesh:AddLonePoint(xy1, 0) -- frame0 mesh:AppendPoint(xy2, 0) mesh:Point(pointsOffset + 0):SetCurvature(MOHO.PEAKED, 0) mesh:Point(pointsOffset + 1):SetCurvature(MOHO.PEAKED, 0) mesh:SelectConnected() local shapeID = moho:CreateShape(false) hasFill = false InstantiateShape(shapeID, x1, y1) elseif (name == "polyline") or (name == "polygon") then local x1, y1 local points_data = (attributes.points or "") local pointsOffset = mesh:CountPoints() isPathStarted = false mesh:SelectNone() for point_AX, point_AY in points_data:gmatch("([%d%.-]+)[, ]+([%d%.-]+)") do -- normalise x1 = ((point_AX/docWide) * 2 * docRatio) -docRatio y1 = ((point_AY/docHigh) * -2) +1 xy1:Set(x1,y1) if (not isPathStarted) then mesh:AddLonePoint(xy1, 0) -- frame0 isPathStarted = true xy2:Set(xy1) else mesh:AppendPoint(xy1, 0) end end local numPoints = mesh:CountPoints() -pointsOffset if (name == "polygon") or hasFill then mesh:AppendPoint(xy2, 0) -- back to start mesh:WeldPoints(pointsOffset + numPoints, pointsOffset, 0) if (name == "polyline") then --#5211 create as filled, but hide last edge #wsbg 1.10 local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:SetSegmentOn(numPoints -1, false) end else -- hasFill = false (already false) end for iOffset = pointsOffset, pointsOffset + numPoints -1 do mesh:Point(iOffset):SetCurvature(MOHO.PEAKED, 0) end mesh:SelectConnected() local shapeID = moho:CreateShape(hasFill) InstantiateShape(shapeID, xy2.x, xy2.y) -- #521030 faux img #wsbg 1.0 width has px (image) .. todo store useAttrib pre de-marked elseif (name == "rect") or (name == "image" and self.hilightImg) then local x1 = (useAttributes.x or "0"):match("[%d%.-]+") + (attributes.x or "0"):match("[%d%.-]+") -- Can be px, pt etc. local y1 = (useAttributes.y or "0"):match("[%d%.-]+") + (attributes.y or "0"):match("[%d%.-]+") local x2 = x1 + (useAttributes.width or "0"):match("[%d%.-]+") + (attributes.width or "0"):match("[%d%.-]+") -- todo 0 illegal local y2 = y1 + (useAttributes.height or "0"):match("[%d%.-]+") + (attributes.height or "0"):match("[%d%.-]+") -- if (x2 == x1) or (y2 == y1) then -- -- todo end this -- end --#511227 local rx = attributes.rx or "auto" local ry = attributes.ry or "auto" rx = (rx == "auto") and ry or rx ry = (ry == "auto") and rx or ry --- rx,ry valid == val, %, auto, local isRound if (rx ~= "auto") or (ry ~= "auto") then rx = (rx == "auto") and ry or rx ry = (ry == "auto") and rx or ry rx = LM.Clamp(rx, 1, attributes.width * .49) -- 49% (spec: 50%) ry = LM.Clamp(ry, 1, attributes.height * .49) rx, ry = deltax(rx), deltay(ry) isRound = true end -- normalise x1 = normx(x1) x2 = normx(x2) y1 = normy(y1) y2 = normy(y2) xy1:Set(x1,y1) xy2:Set(x2,y2) local pointsOffset = mesh:CountPoints() mesh:SelectNone() if (isRound) then -- todo: cheap approximation vec:Set(x1 +rx, y1) mesh:AddLonePoint(vec, 0) vec:Set(x2 -rx, y1) mesh:AppendPoint(vec, 0) vec:Set(x2, y1 +ry) mesh:AppendPoint(vec, 0) vec:Set(x2, y2 -ry) mesh:AppendPoint(vec, 0) vec:Set(x2 -rx, y2) mesh:AppendPoint(vec, 0) vec:Set(x1 +rx, y2) mesh:AppendPoint(vec, 0) vec:Set(x1, y2 -ry) mesh:AppendPoint(vec, 0) vec:Set(x1, y1 +ry) mesh:AppendPoint(vec, 0) vec:Set(x1 +rx, y1) mesh:AppendPoint(vec, 0) local numPoints = mesh:CountPoints() -pointsOffset -1 mesh:WeldPoints(pointsOffset + numPoints, pointsOffset, 0) local curve = mesh:Point(pointsOffset):Curve(0, frame0) local curvx, curvy = rx/3, ry/3 vec:Set(x1 +curvx, y1) curve:SetControlHandle(0, vec, 0, true) -- pre vec:Set(x2 -curvx, y1) curve:SetControlHandle(1, vec, 0, false, false) -- pst vec:Set(x2, y1 +curvy) curve:SetControlHandle(2, vec, 0, true, false) -- pre vec:Set(x2, y2 -curvy) curve:SetControlHandle(3, vec, 0, false, false) -- pst vec:Set(x2 -curvx, y2) curve:SetControlHandle(4, vec, 0, true, false) -- pre vec:Set(x1 +curvx, y2) curve:SetControlHandle(5, vec, 0, false, false) -- pst vec:Set(x1, y2 -curvy) curve:SetControlHandle(6, vec, 0, true, false) -- pst vec:Set(x1, y1 +curvy) curve:SetControlHandle(7, vec, 0, false, false) -- pre for iOffset = 0, numPoints -1 do if ((iOffset % 2) == 0) then curve:AimControlHandleAtNeighbor(iOffset, 0, false) -- pst else curve:AimControlHandleAtNeighbor(iOffset, 0, true) -- pre end end else vec:Set(x1, y1) mesh:AddLonePoint(vec, 0) vec:Set(x2, y1) mesh:AppendPoint(vec, 0) vec:Set(x2, y2) mesh:AppendPoint(vec, 0) vec:Set(x1, y2) mesh:AppendPoint(vec, 0) vec:Set(x1, y1) mesh:AppendPoint(vec, 0) local numPoints = mesh:CountPoints() -pointsOffset -1 mesh:WeldPoints(pointsOffset + numPoints, pointsOffset, 0) for iOffset = pointsOffset, pointsOffset + numPoints -1 do mesh:Point(iOffset):SetCurvature(MOHO.PEAKED, 0) end end if (name == "image") then hasFill = true _fill = self.hilightImgColor end mesh:SelectConnected() local shapeID = moho:CreateShape(hasFill) InstantiateShape(shapeID, x1, y1) elseif (name == "path") then local pointX, pointY, pointAx, pointAy, bezx1, bezy1, bezx2, bezy2, bezx3, bezy3 local pathData = ParseSVGPath(attributes.d or "") local pointsOffset = mesh:CountPoints() local lastOffset = pointsOffset local isLastCurved = false local isFirstCurved = false local lastAction = "" local thisPeaked = {[true] = MOHO.SMOOTH, [false] = MOHO.PEAKED} local numPoints = 0 local dataSize, dataCount, dataOffset local pointsCurveData, pointsLineData = {}, {} isPathStarted, seenM = false, false mesh:SelectNone() local function ArcTo(_lastX, _lastY, rx, ry, angle, largeFlag, sweepFlag, x, y) local function LineTo(_px, _py) vec:Set(_px, _py) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:AimControlHandleAtNeighbor(numPoints -1, 0, false) -- pst (prev point) point = mesh:Point(pointsOffset + numPoints) local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsLineData, pointData) isLastCurved = false numPoints = numPoints +1 end local function ArcToBeziers(angleStart, angleExtent) local numSegments = math.ceil(math.abs(angleExtent) * 2 / math.pi) -- (angleExtent / 90deg) local angleIncrement = angleExtent / numSegments -- The length of each control point vector is given by the following formula. local controlLength = (4 / 3) * math.sin(angleIncrement / 2) / (1 + math.cos(angleIncrement / 2)) local coords = {} --[numSegments * 6] for i=0, numSegments -1 do local _angle = angleStart + (i * angleIncrement) -- Calculate the control vector at this angle local dx = math.cos(_angle) local dy = math.sin(_angle) local offset = i * 6 -- First control point coords[offset +1] = (dx - controlLength * dy) coords[offset +2] = (dy + controlLength * dx) -- Second control point _angle = _angle + angleIncrement dx = math.cos(_angle) dy = math.sin(_angle) coords[offset +3] = (dx + controlLength * dy) coords[offset +4] = (dy - controlLength * dx) -- Endpoint of bezier coords[offset +5] = dx coords[offset +6] = dy end return coords end if (_lastX == x and _lastY == y) then -- If endpoints are same, exit (as per spec) return end -- Handle degenerate case (behaviour specified by the spec) if (rx == 0 or ry == 0) then LineTo(x, y) return end -- Sign of the radii is ignored (behaviour specified by the spec) rx = math.abs(rx) ry = math.abs(ry) local TWO_PI = math.pi * 2 -- Convert angle from degrees to radians local angleRad = math.rad(angle % 360) angleRad = (angleRad > math.pi) and (-TWO_PI + angleRad) or angleRad local cosAngle = math.cos(angleRad) local sinAngle = math.sin(angleRad) -- We simplify the calculations by transforming the arc so that the origin is at the -- midpoint calculated above followed by a rotation to line up the coordinate axes -- with the axes of the ellipse. -- Compute the midpoint of the line between the current and the end point local dx2 = (_lastX - x) / 2 local dy2 = (_lastY - y) / 2 -- Step 1 : Compute (_x1', _y1') -- _x1,_y1 is the midpoint vector rotated to take the arc's angle out of consideration (sc: anti-rotated) local _x1 = (cosAngle * dx2 + sinAngle * dy2) local _y1 = (-sinAngle * dx2 + cosAngle * dy2) local rx_sq = rx^2 local ry_sq = ry^2 local x1_sq = _x1^2 local y1_sq = _y1^2 -- Check that radii are large enough. -- If they are not, the spec says to scale them up so they are. -- This is to compensate for potential rounding errors/differences between SVG implementations. local radiiCheck = x1_sq / rx_sq + y1_sq / ry_sq if (radiiCheck > 0.99999) then local radiiScale = math.sqrt(radiiCheck) * 1.00001 rx = radiiScale * rx ry = radiiScale * ry rx_sq = rx^2 ry_sq = ry^2 end -- Step 2 : Compute (cx1, cy1) - the transformed centre point local sign = (largeFlag == sweepFlag) and -1 or 1 --(largeFlag == sweepFlag) and -1 : 1 local sq = ((rx_sq * ry_sq) - (rx_sq * y1_sq) - (ry_sq * x1_sq)) / ((rx_sq * y1_sq) + (ry_sq * x1_sq)) sq = (sq < 0) and 0 or sq --and 0 : sq local coef = sign * math.sqrt(sq) local cx1 = coef * ((rx * _y1) / ry) local cy1 = coef * -((ry * _x1) / rx) -- Step 3 : Compute (cx, cy) from (cx1, cy1) local sx2 = (_lastX + x) / 2.0 local sy2 = (_lastY + y) / 2.0 local cx = sx2 + (cosAngle * cx1 - sinAngle * cy1) local cy = sy2 + (sinAngle * cx1 + cosAngle * cy1) -- Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle) local ux = (_x1 - cx1) / rx local uy = (_y1 - cy1) / ry local vx = (-_x1 - cx1) / rx local vy = (-_y1 - cy1) / ry -- Angle betwen two vectors is +/- acos( u.v / len(u) * len(v)) -- Where '.' is the dot product. And +/- is calculated from the sign of the cross product (u x v) local p, n, v -- Compute the start angle -- The angle between (ux,uy) and the 0deg angle (1,0) n = math.sqrt((ux^2) + (uy^2)) p = ux sign = (uy < 0) and -1.0 or 1.0 local angleStart = sign * math.acos(p / n); -- No need for checkedArcCos() here. (p >= n) should always be true. -- Compute the angle extent n = math.sqrt(ux^2 + uy^2) * math.sqrt(vx^2 + vy^2) p = ux * vx + uy * vy sign = ((ux * vy - uy * vx) < 0) and -1.0 or 1.0 v = p / n local angleExtent = sign * ((v < -1.0) and math.pi or ((v > 1.0) and 0 or math.acos(v))) -- sign * checkedArcCos(p / n) -- Catch angleExtent of 0, which will cause problems later in arc ToBeziers if (angleExtent == 0) then LineTo(x, y) return end if (not sweepFlag and angleExtent > 0) then angleExtent = angleExtent - TWO_PI elseif (sweepFlag and angleExtent < 0) then angleExtent = angleExtent + TWO_PI end -- Many elliptical arc implementations including the Java2D and Android ones, only -- support arcs that are axis aligned. Therefore we need to substitute the arc -- with bezier curves. The following method call will generate the beziers for -- a unit circle that covers the arc angles we want. local bezierPoints = ArcToBeziers(angleStart, angleExtent) -- Calculate a transformation matrix that will move and scale these bezier points to the correct location. -- Final step is to add the bezier curves to the path for i=1, #bezierPoints, 6 do --== PrePend (reverse Postpend) local mtx = LM.Matrix:new_local() local vec2 = LM.Vector2:new_local() mtx:Translate(cx, cy, 0) mtx:Rotate(LM.Z_AXIS, angleRad) mtx:Scale(rx, ry, 1) for ii = i, i+4, 2 do vec2:Set(bezierPoints[ii], bezierPoints[ii+1]) mtx:Transform(vec2) bezierPoints[ii], bezierPoints[ii+1] = vec2.x, vec2.y end local bezAx1 = bezierPoints[i+0] local bezAy1 = bezierPoints[i+1] local bezAx2 = bezierPoints[i+2] local bezAy2 = bezierPoints[i+3] local _pointAX = bezierPoints[i+4] local _pointAY = bezierPoints[i+5] bezx1 = normx(bezAx1) bezy1 = normy(bezAy1) local _pointX = normx(_pointAX) --normx(x local _pointY = normy(_pointAY) --normy(y) bezx2 = normx(bezAx2) bezy2 = normy(bezAy2) if (lastAction == "M") then isFirstCurved = true end vec:Set(_pointX, _pointY) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?) xy2:Set(bezx1, bezy1) curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst point = mesh:Point(pointsOffset + numPoints) local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsCurveData, pointData) isLastCurved = true numPoints = numPoints +1 end end for iAction, action in ipairs(pathData) do local pathAction = action[1] if ((pathAction == "M") or (pathAction == "m")) then -- Moveto dataSize = 2 dataCount = (#action -1) / dataSize for iData = 0, dataCount -1 do dataOffset = iData * dataSize if (iAction < #pathData) or (dataCount > 1) then -- avoid lonely M. dupe M -- fix my bad export if (pathAction == "M") or (not seenM) then -- ABS (Force ABS at very start (only)) pointAx = action[2 + dataOffset] pointAy = action[3 + dataOffset] pointX = normx(pointAx) pointY = normy(pointAy) _label = "MOVE" seenM = true else -- Rel pointX = pointX + deltax(action[2 + dataOffset]) pointY = pointY + deltay(action[3 + dataOffset]) pointAx = denormx(pointX) -- Todo why Ax wrong ??? pointAy = denormy(pointY) _label = "move" end vec:Set(pointX, pointY) if ((iAction < #pathData) and (pathData[iAction +1][1]:upper() ~= 'M')) or (dataCount > 1) then if (dataOffset == 0) then -- [Mm] command if (isPathStarted) then -- M .... m ..... m .... M --= 2nd+ visit to Move in a Path mesh:SelectConnected() if (not hasZ) and ((hasFill) or ((vec.x == xy1.x) and (vec.y == xy1.y))) then -- Force a shape weld on prev closed shapelet -- TODO: Hide Edge? i.e. contnued path?? think dot dash mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, 0) mesh:SelectConnected() --? end end xy1:Set(pointX, pointY) -- first XY of path seg / subpath isPathStarted = true hasZ = false -- reset pointsOffset = mesh:CountPoints() --preempt new point lastOffset = pointsOffset mesh:AddLonePoint(vec, frame0) -- frame0 numPoints = 1 isLastCurved = false isFirstCurved = false else -- additional M data (treat as L/l) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:AimControlHandleAtNeighbor(numPoints -1, frame0, false) -- pst (prev point) point = mesh:Point(pointsOffset + numPoints) local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsLineData, pointData) isLastCurved = false numPoints = numPoints +1 end end else -- if (little M m) -move current end end elseif (pathAction == "L") or (pathAction == "l") then -- Lineto dataSize = 2 dataCount = (#action -1) / dataSize for iData = 0, dataCount -1 do dataOffset = iData * dataSize if (pathAction == "L") then pointAx = action[2 + dataOffset] pointAy = action[3 + dataOffset] pointX = normx(pointAx) pointY = normy(pointAy) _label = "LINE" else pointAx = pointAx + action[2 + dataOffset] pointAy = pointAy + action[3 + dataOffset] pointX = pointX + deltax(action[2 + dataOffset]) pointY = pointY + deltay(action[3 + dataOffset]) _label = "line" end vec:Set(pointX, pointY) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:AimControlHandleAtNeighbor(numPoints -1, frame0, false) -- pst (prev point) point = mesh:Point(pointsOffset + numPoints) local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsLineData, pointData) isLastCurved = false numPoints = numPoints +1 end elseif (pathAction == "H") or (pathAction == "h") then -- Horiz line dataSize = 1 dataCount = (#action -1) / dataSize for iData = 0, dataCount -1 do dataOffset = iData * dataSize if (pathAction == "H") then pointAx = action[2 + dataOffset] pointX = normx(pointAx) _label = "HORI" else pointAx = pointAx + action[2 + dataOffset] pointX = pointX + deltax(action[2 + dataOffset]) _label = "hori" end vec:Set(pointX, pointY) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:AimControlHandleAtNeighbor(curve:PointID(point), frame0, false) isLastCurved = false point = mesh:Point(pointsOffset + numPoints) local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsLineData, pointData) numPoints = numPoints +1 end elseif (pathAction == "V") or (pathAction == "v") then -- Vert line dataSize = 1 dataCount = (#action -1) / dataSize for iData = 0, dataCount -1 do dataOffset = iData * dataSize if (pathAction == "V") then pointAy = action[2 + dataOffset] pointY = normy(pointAy) _label = "VERT" else pointAy = pointAy + action[2 + dataOffset] pointY = pointY + deltay(action[2 + dataOffset]) _label = "vert" end vec:Set(pointX, pointY) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:AimControlHandleAtNeighbor(curve:PointID(point), frame0, false) isLastCurved = false point = mesh:Point(pointsOffset + numPoints) local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsLineData, pointData) numPoints = numPoints +1 end elseif (pathAction == "C") or (pathAction == "c") then -- Cubic Bezier dataSize = 6 dataCount = (#action -1) / dataSize for iData = 0, dataCount -1 do dataOffset = iData * dataSize if (pathAction == "C") then -- ABS bezx1 = normx(action[2 + dataOffset]) bezy1 = normy(action[3 + dataOffset]) bezx2 = normx(action[4 + dataOffset]) bezy2 = normy(action[5 + dataOffset]) pointAx = action[6 + dataOffset] pointAy = action[7 + dataOffset] pointX = normx(pointAx) pointY = normy(pointAy) _label = "CURV" else -- Rel bezx1 = pointX + deltax(action[2 + dataOffset]) bezy1 = pointY + deltay(action[3 + dataOffset]) bezx2 = pointX + deltax(action[4 + dataOffset]) bezy2 = pointY + deltay(action[5 + dataOffset]) pointAx = pointAx + action[6 + dataOffset] pointAy = pointAy + action[7 + dataOffset] pointX = pointX + deltax(action[6 + dataOffset]) pointY = pointY + deltay(action[7 + dataOffset]) _label = "curv" end if (lastAction == "M") then isFirstCurved = true end vec:Set(pointX, pointY) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?) xy2:Set(bezx1, bezy1) curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst point = mesh:Point(pointsOffset + numPoints) local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsCurveData, pointData) isLastCurved = true numPoints = numPoints +1 end elseif (pathAction == "S") or (pathAction == "s") then -- Smooth Cubic Bezier dataSize = 4 dataCount = (#action -1) / dataSize for iData = 0, dataCount -1 do dataOffset = iData * dataSize if (lastAction == "C" or lastAction == "S") then -- C, S bezx1 = (pointX - bezx2) + pointX bezy1 = (pointY - bezy2) + pointY else bezx1, bezy1 = pointX, pointY end if (pathAction == "S") then bezx2 = normx(action[2 + dataOffset]) bezy2 = normy(action[3 + dataOffset]) pointAx = action[4 + dataOffset] pointAy = action[5 + dataOffset] pointX = normx(pointAx) pointY = normy(pointAy) _label = "SBEZ" else bezx2 = deltax(action[2 + dataOffset]) + pointX bezy2 = deltay(action[3 + dataOffset]) + pointY -- pointAx = -1 -- pointAy = -1 pointAx = pointAx + action[4 + dataOffset] pointAy = pointAy + action[5 + dataOffset] pointX = deltax(action[4 + dataOffset]) + pointX pointY = deltay(action[5 + dataOffset]) + pointY _label = "sbez" end if (lastAction == "M") then isFirstCurved = true end -- todo dedupe vec:Set(pointX, pointY) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?) xy2:Set(bezx1, bezy1) curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst point = mesh:Point(pointsOffset + numPoints) local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsCurveData, pointData) isLastCurved = true numPoints = numPoints +1 end elseif (pathAction == "Q") or (pathAction == "q") or (pathAction == "T") or (pathAction == "t") then -- Quadratic Cubic Bezier if (pathAction == "Q") or (pathAction == "q") then dataSize = 4 else dataSize = 2 -- T, t end dataCount = (#action -1) / dataSize for iData = 0, dataCount -1 do dataOffset = iData * dataSize if (pathAction == "Q") then bezx3 = normx(action[2 + dataOffset]) bezy3 = normy(action[3 + dataOffset]) bezx1 = (pointX + 2 * bezx3) / 3 bezy1 = (pointY + 2 * bezy3) / 3 pointAx = action[4 + dataOffset] pointAy = action[5 + dataOffset] pointX = normx(pointAx) pointY = normy(pointAy) bezx2 = (pointX + 2 * bezx3) / 3 bezy2 = (pointY + 2 * bezy3) / 3 _label = "QBEZ" elseif (pathAction == "q") then bezx3 = deltax(action[2 + dataOffset]) + pointX bezy3 = deltay(action[3 + dataOffset]) + pointY bezx1 = (pointX + 2 * bezx3) / 3 bezy1 = (pointY + 2 * bezy3) / 3 pointAx = action[4 + dataOffset] + pointAx pointAy = action[5 + dataOffset] + pointAy pointX = deltax(action[4 + dataOffset]) + pointX pointY = deltay(action[5 + dataOffset]) + pointY bezx2 = (pointX + 2 * bezx3) / 3 bezy2 = (pointY + 2 * bezy3) / 3 _label = "qbez" elseif (pathAction == "T") or (pathAction == "t") then if (lastAction == "Q") or (lastAction == "T") then bezx3 = (pointX - bezx3) + pointX bezy3 = (pointY - bezy3) + pointY else bezx3 = pointX bezy3 = pointY end if (pathAction == "T") then bezx1 = (pointX + 2 * bezx3) / 3 bezy1 = (pointY + 2 * bezy3) / 3 pointAx = action[2 + dataOffset] pointAy = action[3 + dataOffset] pointX = normx(pointAx) pointY = normy(pointAy) _label = "TBEZ" else bezx1 = (pointX + 2 * bezx3) / 3 bezy1 = (pointY + 2 * bezy3) / 3 pointAx = pointAx + action[2 + dataOffset] pointAy = pointAy + action[3 + dataOffset] pointX = pointX + deltax(action[2 + dataOffset]) pointY = pointY + deltay(action[3 + dataOffset]) _label = "TBEZ" end bezx2 = (pointX + 2 * bezx3) / 3 bezy2 = (pointY + 2 * bezy3) / 3 end if (lastAction == "M") then isFirstCurved = true end -- todo dedupe vec:Set(pointX, pointY) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?) xy2:Set(bezx1, bezy1) curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst point = mesh:Point(pointsOffset + numPoints) local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsCurveData, pointData) isLastCurved = true numPoints = numPoints +1 end elseif (pathAction == "A") or (pathAction == "a") then -- Elliptical Arc dataSize = 7 dataCount = (#action -1) / dataSize local radiX, radiY, rAngle, bLarge, bSweep for iData = 0, dataCount -1 do local lastX, lastY = pointAx, pointAy dataOffset = iData * dataSize radiX = action[2 + dataOffset] radiY = action[3 + dataOffset] rAngle = action[4 + dataOffset] -- ra bLarge = action[5 + dataOffset]+0 ~= 0 bSweep = action[6 + dataOffset]+0 ~= 0 if (pathAction == "A") then -- ABS pointAx = action[7 + dataOffset] pointAy = action[8 + dataOffset] pointX = normx(pointAx) pointY = normy(pointAy) _label = "ARC" else -- Rel pointAx = pointAx + action[7 + dataOffset] pointAy = pointAy + action[8 + dataOffset] pointX = pointX + deltax(action[7 + dataOffset]) pointY = pointY + deltay(action[8 + dataOffset]) _label = "arc" end if (lastAction == "M") then isFirstCurved = true end ArcTo(lastX, lastY, radiX, radiY, rAngle, bLarge, bSweep, pointAx, pointAy) end elseif (pathAction == "Z") or (pathAction == "z") then -- Closepath -- rules: Close path if last cmd or followed by [Mm]. hasZ = true -- if 1st & last not same(ish) if ((ssRound(vec.x,4) ~= ssRound(xy1.x,4)) or (ssRound(vec.y,4) ~= ssRound(xy1.y,4))) then vec:Set(xy1.x, xy1.y) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:AimControlHandleAtNeighbor(numPoints -1, frame0, false) -- pst (prev point) point = mesh:Point(pointsOffset) --1st point local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsLineData, pointData) isLastCurved = false numPoints = numPoints +1 end if ((iAction < #pathData) and (pathData[iAction +1][1]:upper() == 'M')) then pointX, pointY = vec.x, vec.y --?? set AX ??? else end local _result = mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, frame0) if (not _result) then dprint("weldfail: @1578") end mesh:SelectConnected() if ((iAction < #pathData) and (pathData[iAction +1][1]:upper() ~= 'M')) then -- start new subpath - bit like m (actuall or 0,0) pointX, pointY = xy1.x, xy1.y -- first XY of path seg vec:Set(xy1) isPathStarted = true hasZ = false -- reset pointsOffset = mesh:CountPoints() --preempt new point lastOffset = pointsOffset mesh:AddLonePoint(vec, frame0) -- frame0 numPoints = 1 isLastCurved = false isFirstCurved = false end else dprint ((spaces .. " ** UNKNOWN Path Action: " .. pathAction)) end lastAction = pathAction:upper() end if (isPathStarted and numPoints > 1) then --= After Path if (not hasZ) and (((ssRound(vec.x,4) == ssRound(xy1.x,4)) and (ssRound(vec.y,4) == ssRound(xy1.y,4)))) then -- Force Close on last shapelet if no Z and lastpoint==firstpoint if (mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, 0)) then mesh:Point(pointsOffset + 0):SetCurvature(thisPeaked[(isLastCurved) or (isFirstCurved)], frame0) -- ??? end elseif (not hasZ) and hasFill then -- Force Close on last shapelet if no Z but filled vec:Set(xy1.x, xy1.y) mesh:AppendPoint(vec, frame0) local point = mesh:Point(pointsOffset + numPoints -1) local curve = point:Curve(0, frame0) curve:AimControlHandleAtNeighbor(curve:PointID(point), frame0, false) point = mesh:Point(pointsOffset) local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)} table.insert(pointsLineData, pointData) local tmpPt = curve:GetControlHandle(pointData.pID, frame0, false) mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, 0) curve:SetControlHandle(pointData.pID, tmpPt, frame0, false, false) curve:SetSegmentOn(numPoints -1, false) -- hide edge numPoints = numPoints +1 else if (not hasZ) then mesh:Point(mesh:CountPoints() -1):SetCurvature(thisPeaked[isLastCurved], frame0) -- endpoint ??? end end mesh:SelectConnected() -- limited Loops for Curve Pre handles for _, pointData in ipairs(pointsCurveData) do local curve = mesh:Curve(pointData.cID) --todo efficy xy2:Set(pointData.x2, pointData.y2) local targetPID = (pointData.pID < curve:CountPoints()) and pointData.pID or 0 if (targetPID ~= 0) then curve:SetControlHandle(targetPID, xy2, frame0, true, false)-- todo Same curve ??? else curve:SetControlHandle(0, xy2, frame0, true, false) -- todo Same curve ??? end end for _, pointData in ipairs(pointsLineData) do local curve = mesh:Curve(pointData.cID) --todo efficy local targetPID = (pointData.pID < curve:CountPoints()) and pointData.pID or 0 curve:AimControlHandleAtNeighbor(targetPID, frame0, true) if (targetPID == 1) then curve:AimControlHandleAtNeighbor(0, frame0, false) end --fixup welds end local shapeID = moho:CreateShape(false) InstantiateShape(shapeID, 0, 0) end end elseif (name == "defs") or (name == "style") or (name == "clipPath") or (name == "mask") or (name == "title") or (name == "desc") then -- NOP processed elsewhere (or silently ignored) else -- dprint(spaces .. " x2 Can't Handle: ", name) -- todo @ linenum end end if (mesh) then mesh:SelectNone() end end ----------------------- -- Resize realign etc (post-op) ----------------------- local function SizeSVG(groupLayer) groupLayer = moho:LayerAsGroup(groupLayer) for iSubLayer = 0, groupLayer:CountLayers() - 1 do local subLayer = groupLayer:Layer(iSubLayer) if (subLayer:IsGroupType()) then SizeSVG(subLayer) else if (subLayer:LayerType() == MOHO.LT_VECTOR) then subLayer = moho:LayerAsVector(subLayer) local _mesh = subLayer:Mesh() if (_mesh) then _mesh:SelectAll() _mesh:PrepMovePoints() _mesh:TransformPoints(self.transVec, self.scaleFactor, self.scaleFactor, 0, self.orientVec) moho:AddPointKeyframe(0, subLayer, true) _mesh:SelectNone() for iShape = 0, _mesh:CountShapes() -1 do local shape = _mesh:Shape(iShape) if (shape) and (shape.fHasOutline) then local style = shape.fMyStyle style.fLineWidth = style.fLineWidth * self.scaleFactor end end end end end end end ----------------------- -- MashUp Group + Vector neighbors (post-op == slow & brutal)- todo Unnamed? ----------------------- local function MashSVG(parentLayer) local mergeLayer, mergeMesh, maskMode local _mashCount = 0 local mashed = {} parentLayer = moho:LayerAsGroup(parentLayer) for iSubLayer = 0, parentLayer:CountLayers() - 1 do -- dangling vectors (1 grp -> 1 vec) local subLayer = parentLayer:Layer(iSubLayer) if (subLayer:IsGroupType()) then _mashCount = _mashCount + MashSVG(subLayer) if (subLayer:CountLayers() == 1) then -- move veclayer to parent, remove group, inherit name .. masking local subSubLayer = subLayer:Layer(0) -- subSubLayer:SetUserTags(subSubLayer:UserTags() .. " " .. subSubLayer:Name()) subSubLayer:SetName(subLayer:Name()) self.layerHasID[subSubLayer:UUID()] = self.layerHasID[subLayer:UUID()] subSubLayer:SetMaskingMode(subLayer:MaskingMode()) moho:PlaceLayerBehindAnother(subSubLayer, subLayer) moho:DeleteLayer(subLayer) _mashCount = _mashCount +1 end mergeLayer = nil mergeMesh = nil maskMode = nil else if (subLayer:LayerType() == MOHO.LT_VECTOR) and ((subLayer:MaskingMode() == MOHO.MM_NOTMASKED) or (subLayer:MaskingMode() == MOHO.MM_MASKED)) then --todo lastMaskMode match subLayer = moho:LayerAsVector(subLayer) if (not mergeLayer) or (subLayer:MaskingMode() ~= maskMode) then --reset mergeLayer = subLayer mergeMesh = subLayer:Mesh() maskMode = subLayer:MaskingMode() moho:SetSelLayer(mergeLayer) else -- merge vects ?? -- if (not self.minifyLayersUnnamed) or (mergeLayer:Name():sub(1,6) == "Layer " and subLayer:Name():sub(1,6) == "Layer ") then if (not self.minifyLayersUnnamed) or (not self.layerHasID[mergeLayer:UUID()] and not self.layerHasID[subLayer:UUID()]) then local _mesh = subLayer:Mesh() _mesh:SelectAll() moho:Copy(_mesh) -- #wsbg 1.11 kills BitMap bufrer moho:CopyAlternate(_mesh) moho:PasteAlternate() --> mergeLayer mergeMesh:DeselectPoints() table.insert(mashed, subLayer) subLayer:SetVisible(false) -- will del -- subLayer:SetName("_2BD_" .. subLayer:Name()) _mashCount = _mashCount +1 else mergeLayer = nil mergeMesh = nil end end else mergeLayer = nil mergeMesh = nil maskMode = nil end end end for _, _mash in ipairs(mashed) do moho:DeleteLayer(_mash) end return _mashCount end ----------------------- -- SVG Processing starts here ... ----------------------- local svgRoot = svgHandler.root if (svgRoot._name == "svg") then mohodoc:PrepMultiUndo() mohodoc:SetDirty() local rootLayer = moho:CreateNewLayer(MOHO.LT_GROUP) self.tgCount = self.tgCount +1 rootLayer:SetName(filenameSafe) rootLayer = moho:LayerAsGroup(rootLayer) rootLayer:SetVisible(false) rootLayer:Expand(false) svgRoot._attr.id = filenameSafe local lStats = {grCount=0, elCount=0, xlCount=0} --==# Prep for import (back refs etc.) :recursive PrepSVG(svgRoot, lStats) dprint("------ Prep DONE ------ @ ", os.date(), " - ", ssRound(os.clock() - self.startTime)) self.lastTime = os.clock() --==# Do the Import (The Big One) :recursive WalkSVG(rootLayer, svgRoot, {grCount=0, elCount=0, hasLayer=false, hasMaskLayer=false}, 0, {}) self.endTime = os.clock() dprint(("------ Walk DONE ------ @ %s - %s / %s --- Gr: %d, la: %d, el: %d"):format(os.date(), ssRound(self.endTime - self.lastTime), ssRound(os.clock() - self.startTime), self.tgCount, self.tlCount, lStats.xlCount)) self.lastTime2 = self.endTime --> at this point SVG is imported <-- --# now resize / relocate post-op self.scaleFactor = 1 self.transVec, self.scaleVec, self.orientVec = LM.Vector2:new_local(), LM.Vector2:new_local(), LM.Vector2:new_local() local bbox = rootLayer:Bounds(frame0) local bbWide, bbHigh = bbox.fMax.x - bbox.fMin.x, bbox.fMax.y - bbox.fMin.y -- set centered? if (self.recenter) then self.transVec:Set(-bbox.fMin.x -bbWide/2, -bbox.fMin.y -bbHigh/2) end -- origin self.orientVec:Set(bbox.fMin.x +bbWide/2, bbox.fMin.y +bbHigh/2) --- retain ratio local docRatioW, docRatioH = docRatio*2, 1*2 local scaleIndex = self.scaleMode - SS_SVGImportDialog.SCALE_ local scaleValue = self.scaleFactors[scaleIndex] self.scaleFactor = scaleValue if (scaleValue < 0) then --< Scale to CANV if (bbWide > docRatioW) or (bbHigh > docRatioH) then -- shrink if (bbWide/docRatioW) >= (bbHigh/docRatioH) then self.scaleFactor = (docRatioW/bbWide) else self.scaleFactor = (docRatioH/bbHigh) end elseif (bbWide < docRatioW) or (bbHigh < docRatioH) then -- grow if (docRatioW/bbWide) >= (docRatioH/bbHigh) then self.scaleFactor = (docRatioH/bbHigh) else self.scaleFactor = (docRatioW/bbWide) end end self.scaleFactor = self.scaleFactor * math.abs(scaleValue) else --< Scale asset self.scaleFactor = scaleValue end --==# Resize/Scale :recursive if ((self.scaleFactor ~= 1) or (self.recenter)) then SizeSVG(rootLayer) --< uses self.scaleFactor end self.endTime = os.clock() dprint(("------ Size DONE ------ @ %s - %s / %s"):format(os.date(), ssRound(self.lastTime - self.lastTime), ssRound(os.clock() - self.startTime))) self.lastTime3 = self.endTime --==# MashUp/Down Vectors neighbors & single layer groups - post-op consolidation :recursive local mashLoops, mashCount, wasMashCount = 0, 0 repeat wasMashCount = mashCount if (self.minify and self.minifyPostOp) then mashLoops = mashLoops + 1 mashCount = mashCount + MashSVG(rootLayer) end until (mashCount == wasMashCount) or (mashLoops > 10) -- 10 = sanity chk moho:SetSelLayer(mohodoc:Layer(mohodoc:CountLayers() -1), false, true) -- go top layer rootLayer:Expand(SS_SVGImport.expandGroups) rootLayer:SetVisible(true) -- was hidden during import self.lastTime = os.clock() dprint(("-----= Mash DONE =----- @ %s - %s / %s --- Loops: %d, Mashed: %d"):format(os.date(), ssRound(self.lastTime - self.lastTime3), ssRound(os.clock() - self.startTime), mashLoops, mashCount)) if (laterPrint ~= "") then print(laterPrint) end else dprint("NOT an SVG Doc") -- todo end if (moho.frame ~= wasFrame) then moho:SetCurFrame(wasFrame) end else if (self.filename ~= "") then dprint("SVG File not Found: ", self.filename) else dprint("File not specified !") end end end -- ------------- -- Main Dialog -- ------------- function SS_SVGImportDialog:new(moho) local d = LM.GUI.SimpleDialog("SimplSam - Import SVG files", SS_SVGImportDialog) local l = d:GetLayout() local padi = 20 d.prefsDlog = SS_SVGImportPrefsDialog:new() d.prefsPopup = LM.GUI.PopupDialog("☰") -- more settings ... d.prefsPopup:SetDialog(d.prefsDlog) d.prefsPopup:SetToolTip('Preference settings ...') l:PushH(LM.GUI.ALIGN_RIGHT) l:AddChild(d.prefsPopup, LM.GUI.ALIGN_RIGHT) l:Pop() l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) l:PushH() l:AddChild(LM.GUI.StaticText("SVG Filename: "), LM.GUI.ALIGN_LEFT) d.filePathInput = LM.GUI.TextControl(360, ".", 0, LM.GUI.FIELD_TEXT) l:AddChild(d.filePathInput, LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.Button("Browse ...", self.SELECT_FILE), LM.GUI.ALIGN_LEFT) l:Pop() l:PushH() l:AddChild(LM.GUI.StaticText("Recently used: "), LM.GUI.ALIGN_LEFT) d.recentUsedList = LM.GUI.TextList(520, 120, self.SELECT_RECENT) l:AddChild(d.recentUsedList, LM.GUI.ALIGN_LEFT) local iconFile = moho:UserAppDir() .. "/scripts/scriptresources/ss_tools/ss_bin.png" if (ss_tool_icon_bin_found or os.rename(iconFile, iconFile)) then --< sess d.deleteButton = LM.GUI.ImageButton("ScriptResources/ss_tools/ss_bin", "Remove selected entry", false, self.SELECT_DELETE) ss_tool_icon_bin_found = ss_tool_icon_bin_found or true else if (SS_SVGImport._MOHO_Version >= 13.5) then d.deleteButton = LM.GUI.ShortButton("x", self.SELECT_DELETE) else d.deleteButton = LM.GUI.Button("X", self.SELECT_DELETE) end d.deleteButton:SetToolTip('Remove selected entry') end l:AddChild(d.deleteButton, LM.GUI.ALIGN_CENTER) l:Pop() l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) -- -- l:PushH() l:PushV() --------------- l:AddChild(LM.GUI.StaticText("Scaling:"), LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.StaticText("")) --------------- l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- l:AddChild(LM.GUI.StaticText("Consolidation:"), LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.StaticText("")) l:AddChild(LM.GUI.StaticText("")) l:AddChild(LM.GUI.StaticText("")) l:AddChild(LM.GUI.StaticText("")) --------------- l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- l:AddChild(LM.GUI.StaticText("General:"), LM.GUI.ALIGN_LEFT) l:Pop() l:PushV() d.scaleMenu = LM.GUI.Menu('Scaling') for i, scale in ipairs(SS_SVGImport.scaleFactors) do if (scale == 1) then d.scaleMenu:AddItem("100% (no scale)", 0, self.SCALE_ + i) elseif (scale < 0) then d.scaleMenu:AddItem(("Fit to %s%% of Screen"):format(math.abs(scale) * 100), 0, self.SCALE_ + i) elseif (scale > 0) then d.scaleMenu:AddItem(("%s%%"):format(scale * 100), 0, self.SCALE_ + i) else d.scaleMenu:AddItem("", 0, self.SCALE_) --- end end d.scaleMenuPopup = LM.GUI.PopupMenu(140, true) d.scaleMenuPopup:SetMenu(d.scaleMenu) l:AddChild(d.scaleMenuPopup, LM.GUI.ALIGN_LEFT, 0) d.recenterCheck = LM.GUI.CheckBox('Center') l:AddChild(d.recenterCheck, LM.GUI.ALIGN_LEFT, 0) --------------- l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- d.minifyCheck = LM.GUI.CheckBox('Consolidate layers', d.MINIFY_) l:AddChild(d.minifyCheck, LM.GUI.ALIGN_LEFT) d.minifyLayersCheck = LM.GUI.CheckBox('Vector layers', d.MINIFY_LAYERS) l:AddChild(d.minifyLayersCheck, LM.GUI.ALIGN_LEFT, padi) d.minifyGroupsCheck = LM.GUI.CheckBox('Group layers', d.MINIFY_GROUPS) l:AddChild(d.minifyGroupsCheck, LM.GUI.ALIGN_LEFT, padi) d.minifyLayersUnnamedCheck = LM.GUI.CheckBox('Only Unnamed') l:AddChild(d.minifyLayersUnnamedCheck, LM.GUI.ALIGN_LEFT, padi) d.minifyPostOpCheck = LM.GUI.CheckBox('Post-Op reduce', d.MINIFY_POSTOP) l:AddChild(d.minifyPostOpCheck, LM.GUI.ALIGN_LEFT, padi) --------------- l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- d.expandGroupsCheck = LM.GUI.CheckBox('Expand Group tree') l:AddChild(d.expandGroupsCheck, LM.GUI.ALIGN_LEFT, 0) l:Pop() l:PushV() l:AddChild(LM.GUI.StaticText(" - Scale the Image relative to the Canvas or to itself"), LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.StaticText(" - Center the imported Image in the Canvas"), LM.GUI.ALIGN_LEFT) --------------- l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- l:AddChild(LM.GUI.StaticText(" - Enable / Disable Moho Layer consolidation (as below):"), LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.StaticText("- Combine adjacent Vector layers into one"), LM.GUI.ALIGN_LEFT, padi) l:AddChild(LM.GUI.StaticText("- Ungroup Groups which have only one child Vector layer"), LM.GUI.ALIGN_LEFT, padi) l:AddChild(LM.GUI.StaticText("- Do Not process 'significant' / named SVG layers"), LM.GUI.ALIGN_LEFT, padi) l:AddChild(LM.GUI.StaticText("- Maximize consolidation post-operation (** slow **)"), LM.GUI.ALIGN_LEFT, padi) --------------- l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- l:AddChild(LM.GUI.StaticText(" - Show expanded group / layer structure of import"), LM.GUI.ALIGN_LEFT) l:Pop() l:Pop() l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- if (SS_SVGImport._MOHO_Version >= 13.5) then l:AddChild(LM.GUI.ShortButton("Reset", self.RESET), LM.GUI.ALIGN_RIGHT) else l:AddChild(LM.GUI.Button("Reset", self.RESET), LM.GUI.ALIGN_RIGHT) end return d end function SS_SVGImportDialog:UpdateWidgets() self.filePathInput:SetValue(self.filePathInputPre ~= "" and self.filePathInputPre or SS_SVGImport.filename) self.filePathInputPre = "" self.expandGroupsCheck:SetValue(SS_SVGImport.expandGroups) self.minifyCheck:SetValue(SS_SVGImport.minify) self.minifyLayersCheck:SetValue(SS_SVGImport.minifyLayers) self.minifyLayersUnnamedCheck:SetValue(SS_SVGImport.minifyLayersUnnamed) self.minifyGroupsCheck:SetValue(SS_SVGImport.minifyGroups) self.minifyPostOpCheck:SetValue(SS_SVGImport.minifyPostOp) self.minifyLayersCheck:Enable(SS_SVGImport.minify) self.minifyLayersUnnamedCheck:Enable(SS_SVGImport.minify) self.minifyGroupsCheck:Enable(SS_SVGImport.minify) self.minifyPostOpCheck:Enable(SS_SVGImport.minify) self.recenterCheck:SetValue(SS_SVGImport.recenter) self.scaleMenu:UncheckAll() -- reqd 4 reset self.scaleMenu:SetChecked(SS_SVGImport.scaleMode, true) for _, mru in pairs(SS_SVGImport.fileMru) do if ((mru ~= nil) and (mru ~= "")) then self.recentUsedList:AddItem(mru, false) end end self.scaleMenuPopup:Redraw() -- reqd Mac end function SS_SVGImportDialog:OnValidate() end function SS_SVGImportDialog:HandleMessage(msg) if (msg == self.SELECT_FILE) then local filen = LM.GUI.OpenFile("Get me some SVGs ...") if (filen ~= nil) and (filen ~= "") then self.filePathInput:SetValue(filen) end elseif (msg == self.SELECT_RECENT) then self.filePathInput:SetValue(self.recentUsedList:SelItemLabel()) elseif (msg == self.SELECT_DELETE) then local count = self.recentUsedList:CountItems() self.recentUsedList:RemoveItem(self.recentUsedList:SelItem(), false) if (self.recentUsedList:CountItems() ~= count) then SS_SVGImport.fileMru = {} for i = 1, self.recentUsedList:CountItems() do SS_SVGImport.fileMru[i] = self.recentUsedList:GetItem(i -1) end end elseif (msg == self.MINIFY_) then SS_SVGImport.minify = self.minifyCheck:Value() self.minifyLayersCheck:Enable(SS_SVGImport.minify) self.minifyGroupsCheck:Enable(SS_SVGImport.minify) self.minifyPostOpCheck:Enable(SS_SVGImport.minify) elseif (msg == self.MINIFY_LAYERS) then SS_SVGImport.minifyLayers = self.minifyLayersCheck:Value() elseif (msg == self.MINIFY_GROUPS) then SS_SVGImport.minifyGroups = self.minifyGroupsCheck:Value() elseif (msg == self.MINIFY_POSTOP) then SS_SVGImport.minifyPostOp = self.minifyPostOpCheck:Value() elseif (msg == self.RESET) then SS_SVGImport:ResetPrefs() -- for MH12 empty the TxtList after Reset for _ = 1, self.recentUsedList:CountItems() do self.recentUsedList:RemoveItem(0, false) end self:UpdateWidgets() self.scaleMenuPopup:Redraw() end self.minifyLayersUnnamedCheck:Enable(SS_SVGImport.minify and (SS_SVGImport.minifyLayers or SS_SVGImport.minifyGroups or SS_SVGImport.minifyPostOp)) end function SS_SVGImportDialog:OnOK() SS_SVGImport.filename = self.filePathInput:Value() or "" SS_SVGImport.recenter = self.recenterCheck:Value() SS_SVGImport.scaleMode = self.scaleMenu:FirstCheckedMsg() SS_SVGImport.expandGroups = self.expandGroupsCheck:Value() SS_SVGImport.minifyLayers = self.minifyLayersCheck:Value() SS_SVGImport.minifyLayersUnnamed = self.minifyLayersUnnamedCheck:Value() SS_SVGImport.minifyGroups = self.minifyGroupsCheck:Value() SS_SVGImport.minifyPostOp = self.minifyPostOpCheck:Value() local _, extName = SS_SVGImport.filename:match"^(.*)%.([^%.]*)$" if (SS_SVGImport.filename ~= "") and (extName == "svg") and os.rename(SS_SVGImport.filename, SS_SVGImport.filename) then if (SS_SVGImport.filename ~= SS_SVGImport.fileMru[1]) then local j = SS_SVGImport.FILE_MRU_MAX for i = 2, SS_SVGImport.FILE_MRU_MAX do if (SS_SVGImport.filename == SS_SVGImport.fileMru[i]) then j = i break end end for i = j, 2, -1 do SS_SVGImport.fileMru[i] = SS_SVGImport.fileMru[i -1] end SS_SVGImport.fileMru[1] = SS_SVGImport.filename end end -- Save Prefs iWant end -- ------------- -- Prefs Dialog -- ------------- function SS_SVGImportPrefsDialog:new(moho) local d = LM.GUI.SimpleDialog("Panel Options", SS_SVGImportPrefsDialog) local l = d:GetLayout() l:PushH() l:PushV() d.browseFirstCheck = LM.GUI.CheckBox("Browse First", self.PREF_BROWSE_FIRST) l:AddChild(d.browseFirstCheck, LM.GUI.ALIGN_LEFT) d.debugModeCheck = LM.GUI.CheckBox("Debug info", self.PREF_DEBUG_MODE) l:AddChild(d.debugModeCheck, LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- d.hideDialogCheck = LM.GUI.CheckBox("Hide me *", self.PREF_HIDE_DIALOG) l:AddChild(d.hideDialogCheck, LM.GUI.ALIGN_LEFT) l:Pop() l:PushV() l:AddChild(LM.GUI.StaticText(" - Browse for files, before showing dialog"), LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.StaticText(" - Show SVG import Info / Error output"), LM.GUI.ALIGN_LEFT) l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ---- l:AddChild(LM.GUI.StaticText(" - Dont show dialog! Restart/Reload Moho to reset"), LM.GUI.ALIGN_LEFT) l:Pop() l:Pop() return d end function SS_SVGImportPrefsDialog:UpdateWidgets() self.browseFirstCheck:SetValue(SS_SVGImport.browseFirst) self.browseFirstCheck:Enable(not SS_SVGImport.hideDialog) self.hideDialogCheck:SetValue(SS_SVGImport.hideDialog) self.debugModeCheck:SetValue(SS_SVGImport.debugMode) end function SS_SVGImportPrefsDialog:HandleMessage(msg) if (msg == self.PREF_BROWSE_FIRST) then SS_SVGImport.browseFirst = self.browseFirstCheck:Value() elseif (msg == self.PREF_DEBUG_MODE) then SS_SVGImport.debugMode = self.debugModeCheck:Value() elseif (msg == self.PREF_HIDE_DIALOG) then SS_SVGImport.hideDialog = self.hideDialogCheck:Value() self.browseFirstCheck:Enable(not SS_SVGImport.hideDialog) end end
SS - SVG Import
Listed
Author: simplsam
View Script
Script type: Button/Menu
Uploaded: Oct 26 2022, 17:08
Last modified: Nov 14 2022, 09:20
Script Version: 01.12 #5211
Improved SVG vector import -- Layer Grouping, Masking, Colors, Adobe Illustrator & Affinity Designer friendly !! (ver 1.12 for Moho 12.5 & 13.5 only)
Note: Use with Moho 12.5 and 13.5 Only, as Moho 14 has this tool already built-in (File > Import > SVG File...)
The SVG Import tool is a Moho addon that allows you to reliably and faithfully import Scalable Vector Graphics (SVG) assets into Moho - ready for animation and presentation.
The tool was developed as an upgrade/replacement to the existing built-in SVG import process, and provides a range of enhanced features to improve 3rd party SVG application compatibility and end-user productivity:
Enhanced Features:
- SVG Grouping & Elements translate to Moho Groups, Layers & Shapes - allowing you to retain naming & folder structures
- Vastly improved compatibility with Adobe Illustrator exported SVGs
- Support for Element & Group Transforms (translate, rotate, scale, matrix, skewX/Y)
- Handles CSS Class, Inline Style and Presentation attribute styling - with Hex #000000 / #000, Named and RGB/RGBA colors
- Full support for Masking with both ClipPath and Mask masks
- Provides support for Arcs, Rounded Rect’s, Use (referencing) and partial support for inline embedded SVGs
- Like-for-Like point count & positioning (no extraneous points)
- Multiple Image Sizing & Layer consolidation options during import
- Retain SVG element ID’s in Moho groups, layers and/or shapes
- Partial support for Gradients (Gradients are averaged to a single color)
- Respect/Ignore comments / comment blocks in the SVG files
- Provides a handy MRU feature for re-loading Most Recent Used imported files
- Dialog Options which determine the import workflow: Dialog > Browse > Load -or- Browse > Dialog > Load -or- Browse > Load
Video Demo
So … Why do you need the SVG Import tool?
The tool allows for a much greater range of vector based Characters, Props, Scenery and Artwork to be imported directly into Moho - allowing you to faithfully reproduce, manipulate and incorporate artwork from Illustrator, Inkscape, Affinity Designer, CorelDraw, Clip Studio Paint, Photopea + many other graphics/designer tools and SVG image repositories.
Some of the shortcomings of the built-in tool were its inability to fully support Grouping, Masking, CSS Styles, Short hex colors (#000), Use referencing and Arcs. Many of these features are used frequently in SVG imagery - which previously could lead to disfigured or discoloured artwork - or imported assets which failed to retain any grouping / layer structure or element naming.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Mashed Up example: car.svg image file imported with the default tool (Picasso would be proud). When the asset was imported with new SVG Import tool, the SVG image was more faithfully reproduced.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Layers & Naming example: monkey.svg image file imported with the default tool - which looks OK, but has no layers. When the asset was imported with the new SVG Import tool - the layer structure was retained.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Tool Options
- Specify the SVG Filename to be imported, or browse for a SVG file
- Use the Recently Used (MRU) feature to quickly select a previously imported file name. The delete button can be used to remove selected entries from the list
- Set Scaling to scale the size of the imported image asset relative to the Canvas screen size or relative to the original size of the asset (default: 50% Screen)
- Use Center to align the imported image to the middle-center location of the Canvas
- Use Consolidation to minify or reduce the number of Groups & Layers created in Moho (default)
- Select Vector Layers to combine adjacent vector layers into one (default)
- Select Group Layers to fold/ungroup Group Layers than only have one Vector layer in it - resulting in just a Vector Layer (default)
- Use Only Unnamed will avoid consolidating named Elements (i.e. Elements with SVG IDs)
- Post-operation reduction will aggressively reduce Groups and Layers after initial import. Good for getting absolute minimum number of layers (whilst respecting masking), but generally bad if you are planning character-type animation. The process can also be Slow
- Expand Group Tree will expand the imported layer group tree to show all groups & layers
- Use Reset to restore default settings. OK to load & process the specified SVG file. Cancel to Cancel
Tool Preferences
There is an additional Preference settings menu (top right) which allows you to:
- Browse for file First when the tool button is clicked. More like a traditional File-Open experience. The main Dialog will be shown after the File Browse
- Use Debug Info to show some limited process and error information during the import process
- Use Hide Me ** to completely hide the main dialog (on subsequent imports). The importer will behave even-more like a traditional File-Open-process experience.
** This would typically be used if your settings remain the same during the majority of your imports. You can cancel out of Hide Me mode by Restarting/Reloading Moho
Version:
- version: 01.12 MH12.5+ #521104
- release: 1.12
- by Sam Cogheil (SimplSam)
Notes & Limitations
The tool was designed to enhance the Moho SVG import capabilities whilst supporting most common SVG 1.1 features and respecting SVG grouping. The tool is not and never will be 100% SVG 1.1 compliant, and as such has some limitations:
Known Limitations:
- Filenames (and paths) with non-english characters may not be loadable. Rename or relocate the files to fix.
- SVG Document and Viewport dimensions are ignored
- Measurement Units are ignored (i.e. em, px, pt, cm, mm, in, %). All treated as pixels (px)
- Gradients are averaged to a single color (scripting limitation in MH13.5). Full Gradient color supported in Moho 14.
- No support for SVG animate or Filter effects
- Limited support for ‘stroke-linecap’. No support for ‘stroke-linejoin’ (platform limitation)
- Image, Text, Marker, Symbol, Pattern and ‘Hidden/Display/Hide’ keywords are not supported
- Limited SVG Syntax checking. SVG files should be valid before attempting import
Additional Notes:
- The tool has an external dependency on the XML 2 Lua parser (pure Lua)
- Compatible with MH12.5 - MH13.5
- Optimized for MH13.5
Changelog:
- 1.12 - Fix: Clipboard copy buffer
- 1.11 - Fix: Filled Polyline’s
- 1.10 - Add: Faux Gradient. Fix: line-width during Matrix scale. respect Unnamed during group & post-op reduce. un-default Unnamed option. +minor bugs.
Special Thanks to:
- Stan (and the team): MOHO Scripting – https://mohoscripting.com
- The friendly faces @ Lost Marble Moho forum – https://www.lostmarble.com/forum/
- XML 2 Lua parser – https://github.com/manoelcampos/xml2lua (Manoel Campos da Silva Filho / Paul Chakravarti)
- SVG documentation – https://developer.mozilla.org/en-US/docs/Web/SVG/ (Team Mozilla)
- SVG technical ref – https://www.w3.org/TR/SVG11/ (SVG Working Group / W3C Team)
- Arc to Path – https://github.com/BigBadaboom/androidsvg/…/utils/SVGAndroidRenderer.java (Paul LeBeau)
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: 3536
SS - SVG Import
Listed
Author: simplsam
View Script
Script type: Button/Menu
Uploaded: Oct 26 2022, 17:08
Last modified: Nov 14 2022, 09:20
Script Version: 01.12 #5211
Improved SVG vector import -- Layer Grouping, Masking, Colors, Adobe Illustrator & Affinity Designer friendly !! (ver 1.12 for Moho 12.5 & 13.5 only)
Note: Use with Moho 12.5 and 13.5 Only, as Moho 14 has this tool already built-in (File > Import > SVG File...)
The SVG Import tool is a Moho addon that allows you to reliably and faithfully import Scalable Vector Graphics (SVG) assets into Moho - ready for animation and presentation.
The tool was developed as an upgrade/replacement to the existing built-in SVG import process, and provides a range of enhanced features to improve 3rd party SVG application compatibility and end-user productivity:
Enhanced Features:
- SVG Grouping & Elements translate to Moho Groups, Layers & Shapes - allowing you to retain naming & folder structures
- Vastly improved compatibility with Adobe Illustrator exported SVGs
- Support for Element & Group Transforms (translate, rotate, scale, matrix, skewX/Y)
- Handles CSS Class, Inline Style and Presentation attribute styling - with Hex #000000 / #000, Named and RGB/RGBA colors
- Full support for Masking with both ClipPath and Mask masks
- Provides support for Arcs, Rounded Rect’s, Use (referencing) and partial support for inline embedded SVGs
- Like-for-Like point count & positioning (no extraneous points)
- Multiple Image Sizing & Layer consolidation options during import
- Retain SVG element ID’s in Moho groups, layers and/or shapes
- Partial support for Gradients (Gradients are averaged to a single color)
- Respect/Ignore comments / comment blocks in the SVG files
- Provides a handy MRU feature for re-loading Most Recent Used imported files
- Dialog Options which determine the import workflow: Dialog > Browse > Load -or- Browse > Dialog > Load -or- Browse > Load
Video Demo
So … Why do you need the SVG Import tool?
The tool allows for a much greater range of vector based Characters, Props, Scenery and Artwork to be imported directly into Moho - allowing you to faithfully reproduce, manipulate and incorporate artwork from Illustrator, Inkscape, Affinity Designer, CorelDraw, Clip Studio Paint, Photopea + many other graphics/designer tools and SVG image repositories.
Some of the shortcomings of the built-in tool were its inability to fully support Grouping, Masking, CSS Styles, Short hex colors (#000), Use referencing and Arcs. Many of these features are used frequently in SVG imagery - which previously could lead to disfigured or discoloured artwork - or imported assets which failed to retain any grouping / layer structure or element naming.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Mashed Up example: car.svg image file imported with the default tool (Picasso would be proud). When the asset was imported with new SVG Import tool, the SVG image was more faithfully reproduced.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Layers & Naming example: monkey.svg image file imported with the default tool - which looks OK, but has no layers. When the asset was imported with the new SVG Import tool - the layer structure was retained.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Tool Options
- Specify the SVG Filename to be imported, or browse for a SVG file
- Use the Recently Used (MRU) feature to quickly select a previously imported file name. The delete button can be used to remove selected entries from the list
- Set Scaling to scale the size of the imported image asset relative to the Canvas screen size or relative to the original size of the asset (default: 50% Screen)
- Use Center to align the imported image to the middle-center location of the Canvas
- Use Consolidation to minify or reduce the number of Groups & Layers created in Moho (default)
- Select Vector Layers to combine adjacent vector layers into one (default)
- Select Group Layers to fold/ungroup Group Layers than only have one Vector layer in it - resulting in just a Vector Layer (default)
- Use Only Unnamed will avoid consolidating named Elements (i.e. Elements with SVG IDs)
- Post-operation reduction will aggressively reduce Groups and Layers after initial import. Good for getting absolute minimum number of layers (whilst respecting masking), but generally bad if you are planning character-type animation. The process can also be Slow
- Expand Group Tree will expand the imported layer group tree to show all groups & layers
- Use Reset to restore default settings. OK to load & process the specified SVG file. Cancel to Cancel
Tool Preferences
There is an additional Preference settings menu (top right) which allows you to:
- Browse for file First when the tool button is clicked. More like a traditional File-Open experience. The main Dialog will be shown after the File Browse
- Use Debug Info to show some limited process and error information during the import process
- Use Hide Me ** to completely hide the main dialog (on subsequent imports). The importer will behave even-more like a traditional File-Open-process experience.
** This would typically be used if your settings remain the same during the majority of your imports. You can cancel out of Hide Me mode by Restarting/Reloading Moho
Version:
- version: 01.12 MH12.5+ #521104
- release: 1.12
- by Sam Cogheil (SimplSam)
Notes & Limitations
The tool was designed to enhance the Moho SVG import capabilities whilst supporting most common SVG 1.1 features and respecting SVG grouping. The tool is not and never will be 100% SVG 1.1 compliant, and as such has some limitations:
Known Limitations:
- Filenames (and paths) with non-english characters may not be loadable. Rename or relocate the files to fix.
- SVG Document and Viewport dimensions are ignored
- Measurement Units are ignored (i.e. em, px, pt, cm, mm, in, %). All treated as pixels (px)
- Gradients are averaged to a single color (scripting limitation in MH13.5). Full Gradient color supported in Moho 14.
- No support for SVG animate or Filter effects
- Limited support for ‘stroke-linecap’. No support for ‘stroke-linejoin’ (platform limitation)
- Image, Text, Marker, Symbol, Pattern and ‘Hidden/Display/Hide’ keywords are not supported
- Limited SVG Syntax checking. SVG files should be valid before attempting import
Additional Notes:
- The tool has an external dependency on the XML 2 Lua parser (pure Lua)
- Compatible with MH12.5 - MH13.5
- Optimized for MH13.5
Changelog:
- 1.12 - Fix: Clipboard copy buffer
- 1.11 - Fix: Filled Polyline’s
- 1.10 - Add: Faux Gradient. Fix: line-width during Matrix scale. respect Unnamed during group & post-op reduce. un-default Unnamed option. +minor bugs.
Special Thanks to:
- Stan (and the team): MOHO Scripting – https://mohoscripting.com
- The friendly faces @ Lost Marble Moho forum – https://www.lostmarble.com/forum/
- XML 2 Lua parser – https://github.com/manoelcampos/xml2lua (Manoel Campos da Silva Filho / Paul Chakravarti)
- SVG documentation – https://developer.mozilla.org/en-US/docs/Web/SVG/ (Team Mozilla)
- SVG technical ref – https://www.w3.org/TR/SVG11/ (SVG Working Group / W3C Team)
- Arc to Path – https://github.com/BigBadaboom/androidsvg/…/utils/SVGAndroidRenderer.java (Paul LeBeau)
The SVG Import tool is a Moho addon that allows you to reliably and faithfully import Scalable Vector Graphics (SVG) assets into Moho - ready for animation and presentation.
The tool was developed as an upgrade/replacement to the existing built-in SVG import process, and provides a range of enhanced features to improve 3rd party SVG application compatibility and end-user productivity:
Enhanced Features:
- SVG Grouping & Elements translate to Moho Groups, Layers & Shapes - allowing you to retain naming & folder structures
- Vastly improved compatibility with Adobe Illustrator exported SVGs
- Support for Element & Group Transforms (translate, rotate, scale, matrix, skewX/Y)
- Handles CSS Class, Inline Style and Presentation attribute styling - with Hex #000000 / #000, Named and RGB/RGBA colors
- Full support for Masking with both ClipPath and Mask masks
- Provides support for Arcs, Rounded Rect’s, Use (referencing) and partial support for inline embedded SVGs
- Like-for-Like point count & positioning (no extraneous points)
- Multiple Image Sizing & Layer consolidation options during import
- Retain SVG element ID’s in Moho groups, layers and/or shapes
- Partial support for Gradients (Gradients are averaged to a single color)
- Respect/Ignore comments / comment blocks in the SVG files
- Provides a handy MRU feature for re-loading Most Recent Used imported files
- Dialog Options which determine the import workflow: Dialog > Browse > Load -or- Browse > Dialog > Load -or- Browse > Load
Video Demo
So … Why do you need the SVG Import tool?
The tool allows for a much greater range of vector based Characters, Props, Scenery and Artwork to be imported directly into Moho - allowing you to faithfully reproduce, manipulate and incorporate artwork from Illustrator, Inkscape, Affinity Designer, CorelDraw, Clip Studio Paint, Photopea + many other graphics/designer tools and SVG image repositories.
Some of the shortcomings of the built-in tool were its inability to fully support Grouping, Masking, CSS Styles, Short hex colors (#000), Use referencing and Arcs. Many of these features are used frequently in SVG imagery - which previously could lead to disfigured or discoloured artwork - or imported assets which failed to retain any grouping / layer structure or element naming.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Mashed Up example: car.svg image file imported with the default tool (Picasso would be proud). When the asset was imported with new SVG Import tool, the SVG image was more faithfully reproduced.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Layers & Naming example: monkey.svg image file imported with the default tool - which looks OK, but has no layers. When the asset was imported with the new SVG Import tool - the layer structure was retained.
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Tool Options
- Specify the SVG Filename to be imported, or browse for a SVG file
- Use the Recently Used (MRU) feature to quickly select a previously imported file name. The delete button can be used to remove selected entries from the list
- Set Scaling to scale the size of the imported image asset relative to the Canvas screen size or relative to the original size of the asset (default: 50% Screen)
- Use Center to align the imported image to the middle-center location of the Canvas
- Use Consolidation to minify or reduce the number of Groups & Layers created in Moho (default)
- Select Vector Layers to combine adjacent vector layers into one (default)
- Select Group Layers to fold/ungroup Group Layers than only have one Vector layer in it - resulting in just a Vector Layer (default)
- Use Only Unnamed will avoid consolidating named Elements (i.e. Elements with SVG IDs)
- Post-operation reduction will aggressively reduce Groups and Layers after initial import. Good for getting absolute minimum number of layers (whilst respecting masking), but generally bad if you are planning character-type animation. The process can also be Slow
- Expand Group Tree will expand the imported layer group tree to show all groups & layers
- Use Reset to restore default settings. OK to load & process the specified SVG file. Cancel to Cancel
Tool Preferences
There is an additional Preference settings menu (top right) which allows you to:
- Browse for file First when the tool button is clicked. More like a traditional File-Open experience. The main Dialog will be shown after the File Browse
- Use Debug Info to show some limited process and error information during the import process
- Use Hide Me ** to completely hide the main dialog (on subsequent imports). The importer will behave even-more like a traditional File-Open-process experience.
** This would typically be used if your settings remain the same during the majority of your imports. You can cancel out of Hide Me mode by Restarting/Reloading Moho
Version:
- version: 01.12 MH12.5+ #521104
- release: 1.12
- by Sam Cogheil (SimplSam)
Notes & Limitations
The tool was designed to enhance the Moho SVG import capabilities whilst supporting most common SVG 1.1 features and respecting SVG grouping. The tool is not and never will be 100% SVG 1.1 compliant, and as such has some limitations:
Known Limitations:
- Filenames (and paths) with non-english characters may not be loadable. Rename or relocate the files to fix.
- SVG Document and Viewport dimensions are ignored
- Measurement Units are ignored (i.e. em, px, pt, cm, mm, in, %). All treated as pixels (px)
- Gradients are averaged to a single color (scripting limitation in MH13.5). Full Gradient color supported in Moho 14.
- No support for SVG animate or Filter effects
- Limited support for ‘stroke-linecap’. No support for ‘stroke-linejoin’ (platform limitation)
- Image, Text, Marker, Symbol, Pattern and ‘Hidden/Display/Hide’ keywords are not supported
- Limited SVG Syntax checking. SVG files should be valid before attempting import
Additional Notes:
- The tool has an external dependency on the XML 2 Lua parser (pure Lua)
- Compatible with MH12.5 - MH13.5
- Optimized for MH13.5
Changelog:
- 1.12 - Fix: Clipboard copy buffer
- 1.11 - Fix: Filled Polyline’s
- 1.10 - Add: Faux Gradient. Fix: line-width during Matrix scale. respect Unnamed during group & post-op reduce. un-default Unnamed option. +minor bugs.
Special Thanks to:
- Stan (and the team): MOHO Scripting – https://mohoscripting.com
- The friendly faces @ Lost Marble Moho forum – https://www.lostmarble.com/forum/
- XML 2 Lua parser – https://github.com/manoelcampos/xml2lua (Manoel Campos da Silva Filho / Paul Chakravarti)
- SVG documentation – https://developer.mozilla.org/en-US/docs/Web/SVG/ (Team Mozilla)
- SVG technical ref – https://www.w3.org/TR/SVG11/ (SVG Working Group / W3C Team)
- Arc to Path – https://github.com/BigBadaboom/androidsvg/…/utils/SVGAndroidRenderer.java (Paul LeBeau)
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: 3536