Image
ScriptName = 'WP_spineImport'

-- **************************************************
-- Import images from Spine Json files
-- Created by Maarten de Haas, Wigglepixel
-- **************************************************

--[[ ***** Licence & Warranty *****

    Copyright 2023 - Maarten de Haas / Wigglepixel

    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 Maarten de Haas, Wigglepixel 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:
    *    Lua JSON parser: https://github.com/rxi/json.lua (rxi)
]]

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

WP_spineImport = {}

local json

WP_spineImport.WP_UTILS_REQUIRED_VERSION = { 1, 3, 0 }


function WP_spineImport:Name()
    return 'Spine Import'
end

function WP_spineImport:Version()
    return '1.1.3'
end

function WP_spineImport:UILabel()
    return 'Spine.json Images Import'
end

function WP_spineImport:Description()
    return 'Import assets from default skin in Spine json file'
end

function WP_spineImport:Creator()
    return 'Maarten de Haas, Wigglepixel'
end

function WP_spineImport:ColorizeIcon()
    return false
end

-- **************************************************
-- Is Relevant / Is Enabled
-- **************************************************

function WP_spineImport:IsRelevant(moho)
    if (not _did_my_resources_package_path) then
        package.path = package.path .. ";" .. moho:UserAppDir() .. "/scripts/ScriptResources/?.lua;"
        _did_my_resources_package_path = true
    end
    json = json or require('json.json')
    
    return true
end

function WP_spineImport:IsEnabled(moho)
    return (moho.document:CurrentDocAction() == '')
end

-- **************************************************
-- GO
-- **************************************************

function WP_spineImport:Run(moho)
    local mohoDoc = moho.document

    local alertOnWarnings = false
    local warningsList = {}

    local imgExtSearchOrder = { 'png', 'jpg', 'jpeg' }
    
    local OSPathsUseForwardSlashes -- will be set after browsing path

    local spineObj
    
    -- CHECK UTILS VERSION
    local utilsVersionCheck = WP_utils:compareVersion(WP_spineImport.WP_UTILS_REQUIRED_VERSION, WP_utils.VERSION)
    if (utilsVersionCheck < 0) then
        WP_utils:alert("Halted. This script requires at least version "..WP_utils:ver2str(WP_spineImport.WP_UTILS_REQUIRED_VERSION).." of 'wp_utils'")
        return
    elseif (utilsVersionCheck > 0) then
        WP_utils:alert("Caution! This loaded 'wp_utils' is newer than the expected version "..WP_utils:ver2str(WP_spineImport.WP_UTILS_REQUIRED_VERSION))
    end

    -- ADD WARNING TO THE LIST 
    local function raiseWarning(msg)
        table.insert(warningsList, '- '..msg)
        if (alertOnWarnings) then WP_utils:alert(newMsg) end
    end

    -- SEARCHES BONE IN BONES OBJECT. RETURNS BONE IF FOUND, OR nil IF NOT FOUND
    local function findBone(name) 
        if (spineObj and spineObj.bones) then
            for _, bone in ipairs(spineObj.bones) do
                if (bone.name == name) then
                    return bone
                end
            end
        end
        return nil
    end

    -- CALCULATE TRANSFORMS AND SET PARENT BONE REFERENCES
    local function processBones() 
        if (spineObj and spineObj.bones) then
            local bones = spineObj.bones
            local parentBone

            for _, bone in ipairs(bones) do 
                bone.parentObj = nil -- will be set below

                -- TRANSLATION IN LOCAL SPACE
                if (bone.x == nil) then bone.x = 0 end
                if (bone.y == nil) then bone.y = 0 end
                
                -- POSITION IN WORLD COORDINATES (USED FOR CALCULATING POSITION OF IMAGES)
                bone.worldX = bone.x
                bone.worldY = bone.y

                -- LENGTH (UNDEPENDENT OF TRANSFORMATIONS)
                if (bone.length == nil) then bone.length = 0 end

                -- RELATIVE/LOCAL ROTATION (IN DEGREES)
                if (bone.rotation == nil) then bone.rotation = 0 end

                -- WORLD/ABSOLUTE ROTATION (IN DEGREES)
                bone.worldRotation = bone.rotation

                -- LOCAL SCALE (1.0 = SAME SCALING AS PARENT)
                if (bone.scaleX == nil) then bone.scaleX = 1 end
                if (bone.scaleY == nil) then bone.scaleY = 1 end
                if (bone.scaleX ~= bone.scaleY) then
                    raiseWarning("Independent X/Y bone scaling is not supported. Using scaleX as scale for bone '"..bone.name.."'.")
                end
                bone.scaleSingle = bone.scaleX
                bone.scaleY = bone.scaleX -- TEMP SOLUTION FOR IMAGE CALCULATIONS BECAUSE INDEPENDENT SCALING IS NOT SUPPORTED ON BONES

                -- WORLD/ABSOLUTE SCALE 
                bone.worldScaleX = bone.scaleX -- will be set below
                bone.worldScaleY = bone.scaleY -- will be set below

                -- LOCAL SHEAR (NOT SUPPORTED YET)
                if (bone.shearX == nil) then bone.shearX = 0 end
                if (bone.shearY == nil) then bone.shearY = 0 end
                if (bone.shearX ~= 0 or bone.shearY ~= 0) then
                    raiseWarning("Shear on bones is not supported. Ignoring Shear on bone '"..bone.name.."'.")
                end

                -- LOCAL TRANSFORMATION VECTOR (POSITION, ROTATION, SCALE)
                bone.locVec2 = LM.Vector2:new_local()
                bone.locVec2.x = bone.x -- will be set below
                bone.locVec2.y = bone.y -- will be set below

                -- WORLD TRANSFORMATION VECTOR (POSITION, ROTATION, SCALE)
                bone.worldVec2 = LM.Vector2:new_local()
                bone.worldVec2.x = bone.x -- will be set below
                bone.worldVec2.y = bone.y -- will be set below            

                if (bone.parent) then
                    parentBone = findBone(bone.parent)
                    if (parentBone) then
                        -- SET PARENT BONE OBJECT
                        bone.parentObj = parentBone

                        -- CALC LOCAL TRANSFORMATION VECTOR
                        if (parentBone.worldRotation ~= 0) then
                            bone.locVec2:Rotate(WP_utils:deg2rad(parentBone.worldRotation))
                        end
                        bone.locVec2.x = bone.locVec2.x * parentBone.scaleSingle
                        bone.locVec2.y = bone.locVec2.y * parentBone.scaleSingle

                        -- WORLD TRANSFORMATION VECTOR AND WORLD POSITION
                        bone.worldVec2:Set(parentBone.worldVec2 + bone.locVec2)
                        bone.worldX = bone.worldVec2.x
                        bone.worldY = bone.worldVec2.y

                        -- CALC WORLD ROTATION
                        bone.worldRotation = parentBone.worldRotation + bone.rotation

                        -- CALC WORLD SCALE
                        bone.worldScaleX = parentBone.worldScaleX * bone.worldScaleX 
                        bone.worldScaleY = parentBone.worldScaleY * bone.worldScaleY
                    end
                end
            end            
        end
    end

    -- CREATES LAYER WITH IMAGE
    local function addImgAsLayer(parentLayer, name, imgFile, posPx, rotDeg, scale, tintColStr)
        local layer = moho:LayerAsImage(moho:CreateNewLayer(MOHO.LT_IMAGE, false))

        layer:SetName(name)

        -- CONVERT FORWARD SLASHES TO BACKWARDS SLASHES ON WINDOWS, OTHERWISE 'REVEAL SOURCE IMAGE' ISN'T WORKING IN WINDOWS
        if (OSPathsUseForwardSlashes == false) then imgFile = imgFile:gsub('/', '\\') end
        layer:SetSourceImage(imgFile)

        if (posPx) then
            local layerTransVec3 = WP_utils:getMohoRelLayerTransVec3(moho, posPx.x, posPx.y)
            layer.fTranslation:SetValue(0, layerTransVec3)
        end

        if (rotDeg) then
            layer.fRotationZ:SetValue(0, WP_utils:deg2rad(rotDeg))
        end

        if (scale) then
            local scaleVec3 = LM.Vector3:new_local()
            scaleVec3:Set(scale.x, scale.y, 1)
            layer.fScale:SetValue(0, scaleVec3)
        end

        if (parentLayer) then
            moho:PlaceLayerInGroup(layer, parentLayer, true, false)
        end

        -- ADD TINT LAYER

        if (tintColStr) then
            local tintLayer = moho:DuplicateLayer(layer, true)
            tintLayer:SetName(name..'_tint')

            -- COLORIZE
            local colVec = spineHexColToMohoColVec(tintColStr)
            tintLayer.fLayerColor:SetValue(0, colVec)
            tintLayer.fLayerColorOn:SetValue(0, true)

            -- BLENDING MODE
            tintLayer:SetBlendingMode(MOHO.BM_MULTIPLY)
        end
    end

    
    
    -- RETURN SKIN OBJECT FROM SKINS ARRAY (ONLY FOR ARRAY FORMATTED SKINS)
    local function getSkinObjFromSkinsArr(skinsArr, skinName) 
        for _, value in ipairs(skinsArr) do 
            if (value.name ~= nil and value.name == skinName) then
                return value
            end
        end
        return nil
    end

    -- CONVERTS SPINE COLOR STRING TO MOHO COLOR VECTOR
    local function spineHexColToMohoColVec(hexStr)
        local rgbVec = LM.rgb_color:new_local()
        if (hexStr:len() == 8) then
            rgbVec.r = tonumber('0x'..hexStr:sub(1,2))
            rgbVec.g = tonumber('0x'..hexStr:sub(3,4))
            rgbVec.b = tonumber('0x'..hexStr:sub(5,6))
            rgbVec.a = tonumber('0x'..hexStr:sub(7,8))
            return rgbVec
        else
            return nil
        end
    end

    -- =========================================================================

    -- SETTINGS
    local debugMode = true
    local spineFileExt = '.json'
    local importScale = 1 -- set scale to scale up or down complete group
    local importBones = true -- set to true to import bones too
    local expandBoneGroupLayerAfterCreation = true -- if bone layer is expanded after creation
    local importImagesFromMeshes = false -- set to true to import images from meshes (caution! locations wont work because mesh UV's don't get imported)
    alertOnWarnings = true
    local showBoneLabels = false -- true to show label on each bone
    local cancelOnImageNotFound = false -- if set to true importer will cancel import when an image is not found. if false only a warning will be registered
    local alwaysCreateUniqueLayerNames = false -- checks through all layers if there is already a layer with that name. If so adds a postfix number


    -- GET SPINE FILE
    local spineFile = LM.GUI.OpenFile('Please Spine JSON File... (Wigglepixel Spine Import v'..WP_spineImport:Version()..')')
    if (spineFile ~= '') then
        local fileExt = string.match(spineFile, spineFileExt..'$')
        if (fileExt ~= '.json') then
            WP_utils:alert('Only Spine files with extension .json are alowed')
            return
        end
    else
        return
    end

    if (string.find(spineFile, '/')) then OSPathsUseForwardSlashes = true else OSPathsUseForwardSlashes = false end
    local absSpineFileBasePath = WP_utils:getPath(spineFile)
    local spineFilenameWithoutPath = WP_utils:getFilenameFromPath(spineFile)
    local spineFileBasename = WP_utils:trim(string.sub(spineFilenameWithoutPath, 1, -(string.len(spineFileExt) + 1)))
    spineObj = json.decode(WP_utils:readFile(spineFile))

    -- GET IMAGES PATH
    local absImagesPath = absSpineFileBasePath
    if (spineObj.skeleton and type(spineObj.skeleton.images) == 'string') then
        local imgPath = WP_utils:trim(spineObj.skeleton.images)
        if (imgPath ~= '') then
            if (imgPath:sub(-1, -1) == '/') then imgPath = imgPath:sub(1, -2) end

            if (imgPath:sub(1,1) == '.') then
                -- relative
                absImagesPath = (absImagesPath..'/'..imgPath):gsub('/./', '/') -- remove /./ same dir dot halfway path. throws issues on windows    
            else
                -- absolute
                absImagesPath = imgPath
            end
        end
    end
    absImagesPath = absImagesPath:gsub('\\', '/')-- all forward slash 

    -- GET BONES + SET REFERENCE TO PARENT BONES AND CALCULATE WORLD TRANSFORMS
    processBones()
    
    -- GET SKINS
    if (spineObj.skins == nil or type(spineObj.skins) ~= 'table') then
        WP_utils:alert('Halted. No or unrecognized skins object found in spine file. Didn\'t make any changes to your Moho file yet.')
        return
    end
    local skinsCount = WP_utils:arrLen(spineObj.skins)
    if (skinCount == 0) then
        WP_utils:alert('Halted. No skins found inside skins object in spine file. Didn\'t make any changes to your Moho file yet.')
        return
    end
    
    -- GET SKIN
    local skinName = 'default'
    local attachmentsObj 
    local skinsIsArrayFormatted = spineObj.skins[1] ~= nil -- new spine formatting for skins object
    if (skinsIsArrayFormatted) then
        local skinObj = getSkinObjFromSkinsArr(spineObj.skins, skinName)
        if (skinObj == nil) then
            WP_utils:alert('Halted. Skin not found inside skins object in spine file. Didn\'t make any changes to your Moho file yet.')
            do return end
        end
        attachmentsObj = skinObj.attachments
        if (attachmentsObj == nil) then
            WP_utils:alert('Halted. Skin is missing attachments object. Didn\'t make any changes to your Moho file yet.')
            do return end
        end
    else
        attachmentsObj = spineObj.skins[skinName]
    end

    -- GET SLOT NAMES AND SKIN DATA
    local assetsData = {}
    local layerName -- = slot
    local assetName -- = attachment
    local boneObj -- = bound bone object
    local assetType -- = spine attachment type 
    local localRelImgPath -- = relative path and basename for image file
    local assetIndex = 0
    local imgWorldVec2 = LM.Vector2:new_local() -- temp vector to calculate world position 
    
    for i, value in ipairs(spineObj.slots) do 
        -- SET BONE AS REFERENCE
        value.boneObj = findBone(value.bone)
        boneObj = value.boneObj

        -- GET SLOT NAME AS LAYER NAME
        layerName = value.name 
        if (type(layerName) ~= 'string' or layerName:len() == 0) then
            WP_utils:alert('Halted. Name for Layer/slot at position '..tostring(i)..' (one-based) is missing in spine file. Didn\'t make any changes to your Moho file yet.')
            return
        end 
        for i, value in ipairs(assetsData) do
            if (assetsData[i].layerName == layerName) then
                WP_utils:alert("Halted. Layer/slot '"..layerName.."' has duplicates in spine file. Didn't make any changes to your Moho file yet.")
                return
            end
        end
        
        -- GET ATTACHMENT NAME AS ASSETNAME (SPINE.JSON DOCUMENTATION SAYS: IF MISSING WE SHOULD ASSUME THERE IS NO ATTACHMENT)
        assetName = value.attachment
        if (assetName == nil) then
            raiseWarning("Skipping Layer/slot '"..layerName.."'. Attachment/image is missing in spine file. Either this slot is corrupted or is missing an attachment.")
        end

        -- IMAGE FILE (+ CHECK IF IT EXISTS)
        if (attachmentsObj[layerName] and attachmentsObj[layerName][assetName] and assetName ~= nil) then
            -- GET ATTACHMENT TYPE
            assetType = attachmentsObj[layerName][assetName].type
            if (assetType == nil) then assetType = 'region' end
                
            if (assetType == 'region' or (importImagesFromMeshes and assetType == 'mesh')) then
                assetIndex = assetIndex + 1

                localRelImgPath = assetName

                -- CREATE NEW ASSET ITEM
                assetsData[assetIndex] = {}        
                assetsData[assetIndex].index = assetIndex
                assetsData[assetIndex].layerName = layerName
                assetsData[assetIndex].assetName = assetName
                assetsData[assetIndex].imgOrgSize = { width = 0, height = 0 } -- original image size (unscaled and undeformed)
                assetsData[assetIndex].imgFile = ''

                assetsData[assetIndex].parentBoneObj = nil -- will be set later
                assetsData[assetIndex].boneObj = boneObj

                assetsData[assetIndex].imgPosPx = { x = 0, y = 0 } -- local position ofset of image in relation to bone
                assetsData[assetIndex].imgPosPxWorld = { x = 0, y = 0 } -- world position of image

                assetsData[assetIndex].boneRotDegWorld = 0 -- rotation of bound bone in world coordinates
                assetsData[assetIndex].imgRotDeg = 0 -- local rotation in relation to bone
                assetsData[assetIndex].imgRotDegWorld = 0 -- image rotation in total

                assetsData[assetIndex].boneScaleWorld = { x = 1, y = 1 } -- bone world scale
                assetsData[assetIndex].imgScale = { x = 1, y = 1 } -- local image scale
                assetsData[assetIndex].imgScaleWorld = { x = 1, y = 1 } -- world image scale (total scale)
            
                
                local props = attachmentsObj[layerName][assetName]

                -- GET BONE AND UPDATE POS, ROT, SCALE OF IMAGE
                if (boneObj) then
                    assetsData[assetIndex].parentBoneObj = boneObj.parentObj

                    assetsData[assetIndex].imgPosPxWorld.x = boneObj.worldX
                    assetsData[assetIndex].imgPosPxWorld.y = boneObj.worldY

                    assetsData[assetIndex].boneRotDegWorld = boneObj.worldRotation
                    assetsData[assetIndex].imgRotDegWorld = boneObj.worldRotation

                    assetsData[assetIndex].boneScaleWorld.x = boneObj.worldScaleX
                    assetsData[assetIndex].boneScaleWorld.y = boneObj.worldScaleY

                    assetsData[assetIndex].imgScaleWorld.x = boneObj.worldScaleX
                    assetsData[assetIndex].imgScaleWorld.y = boneObj.worldScaleY
                end
                
                if (props ~= nil) then
                    -- GET POSITION AND SIZE DATA                    
                    if (props.x ~= nil and type(props.x) == 'number') then 
                        assetsData[assetIndex].imgPosPx.x = props.x 
                    end

                    if (props.y ~= nil and type(props.y) == 'number') then
                        assetsData[assetIndex].imgPosPx.y = props.y
                    end 

                    if (props.width ~= nil and type(props.width) == 'number') then 
                        assetsData[assetIndex].imgOrgSize.width = props.width 
                    end 
                    if (props.height ~= nil and type(props.height) == 'number') then
                        assetsData[assetIndex].imgOrgSize.height = props.height 
                    end

                    if (props.rotation ~= nil and type(props.rotation) == 'number') then 
                        assetsData[assetIndex].imgRotDeg = props.rotation
                        assetsData[assetIndex].imgRotDegWorld = assetsData[assetIndex].imgRotDegWorld + props.rotation
                    end

                    if (props.scaleX ~= nil and type(props.scaleX) == 'number') then 
                        assetsData[assetIndex].imgScale.x = props.scaleX
                        assetsData[assetIndex].imgScaleWorld.x = assetsData[assetIndex].imgScaleWorld.x * props.scaleX 
                    end
                    if (props.scaleY ~= nil and type(props.scaleY) == 'number') then 
                        assetsData[assetIndex].imgScale.y = props.scaleY
                        assetsData[assetIndex].imgScaleWorld.y = assetsData[assetIndex].imgScaleWorld.y * props.scaleY 
                    end

                    if (props.color ~= nil and type(props.color) == 'string') then 
                        assetsData[assetIndex].tintColStr = props.color 
                    end       
                    if (props.path ~= nil and type(props.path) == 'string') then 
                        localRelImgPath = props.path 
                    end

                    -- CALC WORLD POS (USING ROTATED AND SCALED AXIS)
                    imgWorldVec2.x = props.x
                    imgWorldVec2.y = props.y        
                    if (boneObj.worldRotation ~= 0) then
                        imgWorldVec2:Rotate(WP_utils:deg2rad(boneObj.worldRotation))
                    end
                    imgWorldVec2.x = imgWorldVec2.x * boneObj.scaleX
                    imgWorldVec2.y = imgWorldVec2.y * boneObj.scaleY

                    imgWorldVec2:Set(boneObj.worldVec2 + imgWorldVec2)

                    assetsData[assetIndex].imgPosPxWorld.x = imgWorldVec2.x
                    assetsData[assetIndex].imgPosPxWorld.y = imgWorldVec2.y

                    -- GET IMAGE FILE AND CHECK IF IT EXISTS
                    if (props.sequence ~= nil) then
                        raiseWarning("Image sequences aren't supported. Only the first frame will be loaded for asset '"..assetName.."' (layer '"..layerName.."').")

                        -- GET IMAGE SEQUENCE FIRST FRAME INDEX
                        if (type(props.sequence.start) ~= 'number') then
                            WP_utils:alert('Halted. Missing image sequence \'Start\' property on asset "'..assetName..'". Didn\'t make any changes to your Moho file yet.')
                            return
                        end
                        local imgSeqStartIndex = props.sequence.start

                        -- GET IMAGE SEQUENCE DIGITS COUNT
                        if (type(props.sequence.digits) ~= 'number') then
                            WP_utils:alert('Halted. Missing image sequence \'Digits\' property on asset "'..assetName..'". Didn\'t make any changes to your Moho file yet.')
                            return
                        end
                        local imgSeqDigits = props.sequence.digits
                        if (imgSeqDigits < 1) then imgSeqDigits = 1 end

                        local imgSeqFrameNrStr = string.format('%0'..imgSeqDigits..'d', imgSeqStartIndex)
                        localRelImgPath = localRelImgPath..imgSeqFrameNrStr
                    end
                    assetsData[assetIndex].imgFile = absImagesPath .. '/'..localRelImgPath
                                    
                    -- SEARCH IMAGE FILE
                    local imgIsFound = false
                    for _, imgExt in ipairs(imgExtSearchOrder) do
                        imgIsFound = WP_utils:fileExists(assetsData[assetIndex].imgFile..'.'..imgExt) 
                        if (imgIsFound) then 
                            assetsData[assetIndex].imgFile = (assetsData[assetIndex].imgFile..'.'..imgExt)
                            break 
                        end
                    end
                    if (imgIsFound == false) then
                        local msg = "File '"..assetsData[assetIndex].imgFile.."' (+ .png/jpg/jpeg) not found (layer '"..layerName.."')."
                        if (cancelOnImageNotFound == true) then                            
                            WP_utils:alert("Halted. "..msg.." Didn\'t make any changes to your Moho file yet.")
                            return
                        else
                            raiseWarning(msg)
                        end
                    end
                else
                    WP_utils:alert('Halted. Missing props for attachment '..i..'. Didn\'t make any changes to your Moho file yet.')
                    return    
                end
            else
                raiseWarning("Skipping asset '"..assetName.."', because currently '"..assetType.."' assets aren't supported.")
            end
        end
    end


    -- --------------------------------------------------------------------------------------------------------------------
    -- ADD IMAGES AS LAYERS TO MOHO
    -- --------------------------------------------------------------------------------------------------------------------

    -- GOTO FRAME ZERO 
    if (moho.frame ~= 0) then moho:SetCurFrame(0) end

    -- PREP MULTI UNDO AND RAISE DIRTY FLAG
    mohoDoc:PrepMultiUndo()
    mohoDoc:SetDirty()

    -- SELECT TOP LAYER
    moho:SetSelLayer(mohoDoc:Layer(mohoDoc:CountLayers() -1), false, true)

    -- CREATE BONES LAYER
    local boneGroupLayer = moho:CreateNewLayer(MOHO.LT_BONE, true)
    local boneGroupLayerName = spineFileBasename.." ('"..skinName.."' skin)"
    if (WP_utils:layerExists(moho, boneGroupLayerName)) then 
        boneGroupLayerName = WP_utils:findNextFreeLayerName(moho, boneGroupLayerName) 
    end
    boneGroupLayer:SetName(boneGroupLayerName)
    boneGroupLayer = moho:LayerAsBone(boneGroupLayer)
    boneGroupLayer:Expand(expandBoneGroupLayerAfterCreation)

    local skeleton = boneGroupLayer:Skeleton()

    -- IMPORT SCALING
    if (importScale ~= 1) then
        local importScaleVec3 = LM.Vector3:new_local()
        importScaleVec3:Set(importScale, importScale, 1)
        boneGroupLayer.fScale:SetValue(0, importScaleVec3)
    end
    
    -- CREATE IMAGE LAYERS
    local layerName
    local autoRenameLayers = nil
    local docIsModified = false
    for i in ipairs(assetsData) do
        layerName = assetsData[i].layerName

        -- CHECK IF LAYERNAME EXISTS (ONLY IF SETTING IS ENABLED)
        if (alwaysCreateUniqueLayerNames) then
            if (WP_utils:layerExists(moho, layerName)) then
                if (autoRenameLayers == nil) then
                    autoRenameLayers = WP_utils:alert('Layer "'..layerName..'" already exists. Do you want to continue with auto-renaming new duplicate layers? (Hit Cancel to exit without making changes to your moho project).', true)
                    if (autoRenameLayers == 1) then
                        return nil
                    end
                end
                layerName = WP_utils:findNextFreeLayerName(moho, layerName)
            end
        end

        -- CREATE LAYER
        addImgAsLayer(
            boneGroupLayer,
            layerName, 
            assetsData[i].imgFile,
            assetsData[i].imgPosPxWorld,
            assetsData[i].imgRotDegWorld,
            assetsData[i].imgScaleWorld,
            assetsData[i].tintColStr)
    end

    -- SELECT GROUP LAYER
    moho:SetSelLayer(boneGroupLayer)


    -- --------------------------------------------------------------------------------------------------------------------
    -- ADD BONES TO MOHO
    -- --------------------------------------------------------------------------------------------------------------------

    if (importBones and spineObj and spineObj.bones) then
        local newBone
        local hasParent = false
        for i, bone in ipairs(spineObj.bones) do 
            hasParent = bone.parentObj ~= nil

            -- ===============================
            -- ADD BONE
            -- ===============================

            -- CREATE BONE
            newBone = skeleton:AddBone(0)
            newBone:SetName(bone.name)
            newBone:ShowLabel(showBoneLabels)
            
            -- UPDATE BONE DATA FOR CHILDREN
            bone.mohoBoneObj = newBone
            bone.mohoBoneId = skeleton:BoneID(newBone)

            if (hasParent == false) then
                --=====================================================
                --  NO PARENT
                --=====================================================

                -- SET PARENT
                newBone.fParent = -1
                newBone.fAnimParent:SetValue(0, -1)
                
                -- SET POSITION
                local bonePosMohoVec2  = WP_utils:getMohoRelLayerTransVec2(moho, bone.x, bone.y)
                newBone.fPos = bonePosMohoVec2
                newBone.fAnimPos:SetValue(0, bonePosMohoVec2)

                -- SET ROTATION
                local boneRotationMohoRad = WP_utils:deg2rad(bone.rotation)
                newBone.fAngle = boneRotationMohoRad
                newBone.fAnimAngle:SetValue(0, boneRotationMohoRad)

                -- CALCULATE MOHO SCALE
                newBone.fScale = bone.scaleSingle
                newBone.fAnimScale:SetValue(0, bone.scaleSingle)

                -- SET BONE LENGTH
                local boneLengthVec2 = WP_utils:getMohoLayerSizeVec2(moho, bone.length, 0)
                boneLengthVec2:Rotate(WP_utils:deg2rad(bone.rotation))
                local boneLengthMoho = boneLengthVec2:Mag() 
                newBone.fLength = boneLengthMoho * bone.scaleSingle
            else
                --=====================================================
                --  WITH PARENT
                --=====================================================

                -- SET PARENT
                newBone.fParent = bone.parentObj.mohoBoneId
                newBone.fAnimParent:SetValue(0, bone.parentObj.mohoBoneId)

                -- GET DATA FROM MOHO PARENT BONE
                local mohoParentBone = bone.parentObj.mohoBoneObj
                local parentBonePosVec2Moho = mohoParentBone.fAnimPos:GetValue(0)
                local parentBoneScale = mohoParentBone.fAnimScale:GetValue(0)

                -- SET BONE LENGTH
                local boneLengthVec2 = WP_utils:getMohoLayerSizeVec2(moho, bone.length, 0)
                boneLengthVec2:Rotate(WP_utils:deg2rad(bone.rotation))
                local boneLengthMoho = boneLengthVec2:Mag() 
                newBone.fLength = boneLengthMoho * parentBoneScale * bone.scaleSingle

                -- SET POSITION OF CHILD BONE
                local bonePosMohoVec2 = WP_utils:getMohoLayerSizeVec2(moho, bone.x * parentBoneScale, bone.y * parentBoneScale)
                newBone.fPos = bonePosMohoVec2
                newBone.fAnimPos:SetValue(0, bonePosMohoVec2)

                -- SET ROTATION OF CHILD BONE
                local boneRotationMohoRad = WP_utils:deg2rad(bone.rotation)
                newBone.fAngle = boneRotationMohoRad
                newBone.fAnimAngle:SetValue(0, boneRotationMohoRad)

                -- CALCULATE MOHO SCALE
                newBone.fScale = parentBoneScale * bone.scaleSingle
                newBone.fAnimScale:SetValue(0, parentBoneScale * bone.scaleSingle) 
            end

            -- UPDATE BONE TRANSFORM MATRICES (IS THIS NEEDED?)
            skeleton:UpdateBoneMatrix(bone.mohoBoneId)
        end
    end

    
    


    -- FINISHED
    local warningsCount = WP_utils:arrLen(warningsList)
    if (warningsCount == 0) then
        if (debugMode == false) then
            WP_utils:alert('Successfully Finished importing!')
        end
    else
        print('----------------------------------------------------------------------------------------------')
        print('WIGGLEPIXEL SPINE IMPORT v'..WP_spineImport:Version())
        print('')
        print('Completed with '..warningsCount..' warnings:')
        print('')
        for _, msg in ipairs(warningsList) do 
            print(msg)
        end
        print('----------------------------------------------------------------------------------------------')
        print('')
        if (debugMode == false) then
            WP_utils:alert('Completed with '..warningsCount..' warnings (see Lua Console for details)')
        end
    end
end

Icon
WP Import from Spine JSON
Listed

Script type: Tool

Uploaded: Oct 16 2023, 08:04

Last modified: Dec 10 2023, 07:28

Script Version: 1.1.3

Import Assets from Spine JSON (To import from Affinity, Krita, Spine, Photoshop, AfterEffects, Gimp etc.)
Image



IMPORT FROM AFFINITY DESIGNER OR PHOTO

Tutorial video on how to use the script to import Affinity (Photo/Designer) Layers as Image Layers with cropped images in Moho:




IMPORT FROM KRITA
Tutorial video on how to use the script to import Krita Layers as Image Layers with cropped images in Moho:



IMPORT FROM SPINE
Short tutorial video on how to use the script to import Region Attachments (images) and their transformations at the setup pose from Spine as Moho 14 Layers:




IMPORT FROM PHOTOSHOP, AFTEREFFECTS, GIMP ETC.
There are spine JSON export scripts for many programs. Look here for export scripts made by Esoteric Software (creator of Spine) for at least Photoshop, AfterEffects, Gimp etc. : https://github.com/EsotericSoftware/spine-scripts


----------------------------------------------------------------------------------------------------------
DISCLAIMER
----------------------------------------------------------------------------------------------------------
I've tried my best to make this plugin as stable and useful as possible. But please bare in mind; I'm sharing this script, which I've created for doing my own animation work, for others to use for free as I think this could help others too. But this script/this plugin is 'as is'. There might be an update in the future, but that's not garanteed and there is no active support. Nor a garantee (always backup first!). That said; I hope you enjoy it and it helps you with your animation work!

----------------------------------------------------------------------------------------------------------
VERSION HISTORY
----------------------------------------------------------------------------------------------------------

1.1.3 [current]
- Changed: Made compatible with newer utils file

1.1.2
- Changed: Made compatible with newer utils file
- Added: Retina icon

1.1.1
- Fixed: Typo

1.1.0
- Added: If there is bone data available it now imports bones and creates the same skeleton in Moho,  complete with transforms translation, rotation and scale*. Bone shearing will be ignored tho. *: Different scaling for X and Y isn't supported. If a bone has a different scale value for x and y then the x-value will be used as scale in Moho.
- Changed: Improve warnings output
- Changed: different scaleX and scaleY values in bones aren't supported. The image transformspath to calculate images from bones in the tree is now calculated accordingly to keep image transforms the same as the bone transformations.

1.0.0
- Initial release. Imports and transforms images.

----------------------------------------------------------------------------------------------------------
ABOUT THE CURRENT VERSION
----------------------------------------------------------------------------------------------------------

How to install the script in Moho?
* Click on the 'Download for Install Script command''-button below to download the zip-file
* Unzip the downloaded file
* Start Moho
* Open the Scripts menu --> choose 'Install Script' --> Browse through the unziped folder and choose the folder named 'wp_spine_import_for-moho-install-script-command' --> It should now install the script
* Once installation is done Moho shows a dialog saying 'Installation Complete!'
* Restart Moho (only after restarting the tool will become available)
* You should now see the tool with the spine logo in the tools panel. Ready to use!


Why I created this script?

1. Make exporting layers from Affinity way easier and quicker
I started this script because Affinity (Mainly Photo and Designer) (Affinity website) offers a great way to export all layers to individual cropped images on disc, together with a spine.json file containing all layer data, like positions of all images.
This makes it extremely easy and fast to import all layers and images directly into Spine. However, Moho so far couldn't open these spine.json files. So it took a long time to import all these individual images to moho layers and position all these layers in the right spot. By using this script this now is as easy in Moho as it is in Spine and it's now importing and positioning all images/layers via the spine.json file with a single click so we don't have to spend lots of time importing images anymore and can start rigging right away.

2. Make exporting layers from Krita way easier and quicker
There is also a very useful script for Krita (Krita Website) around to export Krita layers to spine.json. Although I didn't tested the outputed spine.json with that script yet, it should work just fine too. You can find it here: Krita-to-spine script download
Not shown in the tutorial video above, because I wonder if it's useful in Spine, but it's even possible to add special naming to Krita group layers that the export script recognises to automatically create bones etc.. See the krita-to-spine script readme (follow the download link) for more information.

3. Make importing images from spine.json possible too?
After having the import for Affinity-generated spine.json files working I wondered how far I could take this to import spine.json files generated by Spine itself (Esoteric Software/Spine website). These files are more complicated because Spine also has a complete skeleton system, like Mohos bones, and images inside spine files have a hierarchical tree structure with bones. But also all bones can have translations/offsets, rotations, scales and shear values. Even images bound to bones can have these transformations applied. And they all influence each other via parenting.

Besides this in Spine it's possible to tint images, which isn't currently possible in Moho.
Spine has more features, like multiple skeletons in a file, multiple skins per skeleton, meshes, several other types of attachments besides images and transforms. But these aren't supported by this script (yet?) and is way over the original use case for the script, which was to import all layers as images from Affinity.

All images are imported now and get the right transformations (except shears) in Moho, just like in Spine. And when it finds tinted images/slots in the spine.json file it generates extra reference tint-layers in Moho to have a similar effect. At the moment these aren't having exactly the same result/blending, so these will probably be tweaked later. But at least it imports. And I consider this a feature that won't be used by many people.

4. Importing from other software (Photoshop, After Effects, Gimp etc.)?
There are Spine JSON exporters around for Photoshop, Gimp and other software too. So basically any software you have and has an export script to export to Spine JSON fileformat is with this script now able to export to Moho. Esoteric Software, the creators of Spine, have a repository with some exporter scripts for several programs here: https://github.com/EsotericSoftware/spine-scripts
Image

What does the script do?
This first version generates a group with image layers. The group is transformed to a bone layer in moho, but doesn't contain bones yet. A later version will probably add an option to load the bones too, if the spine.json contains bone data.
The bone data in the spine.json is however used to determine the transformations of the images to position/rotate/scale them on the right spots in Moho.

How to use the script?
This is a new script! Backup your moho file before use just to be sure.
Hit the tool button, browse the spine.json file (only json files are supported)

How to export Affinity or Krita layers, or Spine content to spine.json?
See tutorial videos above!

Supported Moho version
Tested in Moho 14.x.

About support of spine.json features
Everything imports fine from files generated by Affinity
About more advanced spine features as generated by Spine:
- Only the default skin (skin named 'default'wink will currently be imported. Multi-skin is not supported.
- Imports region attachments (= image without a mesh). Images having a mesh will not be imported.
- Like spine the valid supported image file formats are png, jpg and jpeg (will search for these formats in that order, like Spine).
- Positions, rotations and scaling* of images and bones are calculated hierarchical through the bones to their final position.
- *) Different/Individual scaling for X and Y on bones isn't supported. If a bone has a unequal values for scaleX and scaleY then the scaleX value will be used as scale for that bone in Moho.
- Shear on bones aren't currently supported.
- Constraints aren't supported.
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: 621