# SkinDesigner: A Plugin for Building Skin Design (GPL) started by Santiago Garay

# This file is part of SkinDesigner.
# 
# Copyright (c) 2017, Santiago Garay <sgaray1970@gmail.com> 
# SkinDesigner is free software; you can redistribute it and/or modify 
# it under the terms of the GNU General Public License as published 
# by the Free Software Foundation; either version 3 of the License, 
# or (at your option) any later version. 
# 
# SkinDesigner is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with SkinDesigner; If not, see <http://www.gnu.org/licenses/>.
# 
# @license GPL-3.0+ <http://spdx.org/licenses/GPL-3.0+>
# Skin Generator

"""
Use this component to generate a panelized design solution in the scene.
_
_
To add more panels bays in the construction, simply zoom into the component and hit the lowest "+" sign that shows up on the input side.  To remove panels from the construction, zoom into the component and hit the lowest "-" sign that shows up on the input side.
-
    Args:
        _acivate: A boolean to turn on/off the SkniGenerator solution.
        _skinSurfaceList: List of grasshopper surfaces or IDs of scene surfaces where panels will be mapped.
        Surfaces should have a Z straight linear direction in one of its domains to work.
        skinParameters: A list of optional parameter/value pairs generated by the SkinParamter component. 
        designFunctions: A list of DesignFunction objects created by the LayoutFunction and PanelFunction components.
        postProcFunctions:  A list of PostProcFucntion objects generated by PostProcess type components.
        panelBay_1: (panelBay_2, panelBay_3,....) A Panel or a list of Panel objects ( if Panel_Bay component is ised)
        to be used by the SkinGenerator solution. By adding inputs, more panel objects can be added.These input numbers (IDs) are used to reference this panel or panel bay in some design functions.
    Returns:
        postProcData: Any custom data that might be generated by the PostProcess Functions connected to SkinGenerator. 
        skinBayData: A database with panel placement infomation generated by the SkinGenerator solution.
        skinPanelData: A database with panel types generated by the SkinGenerator solution.
"""

ghenv.Component.Name = "SkinDesigner_SkinGenerator"
ghenv.Component.NickName = 'SkinGenerator'
ghenv.Component.Message = 'VER 0.1.16\nSep_22_2017'
ghenv.Component.Category = "SkinDesigner"
ghenv.Component.SubCategory = "01 | Construction"
try: ghenv.Component.AdditionalHelpFromDocStrings = "2"
except: pass

# automnatically set the right input names and types (when using + icon) 
numInputs = ghenv.Component.Params.Input.Count
accessList = ghenv.Component.Params.Input[0].Access.list
accessItem = ghenv.Component.Params.Input[0].Access.item
 
for input in range(numInputs):
    access = accessList
    if input == 0: inputName = '_activate'; access = accessItem
    elif input == 1: inputName = 'skinParameters' ; access = accessList    
    elif input == 2: inputName = '_skinSurfaceList' ; access = accessList
    elif input == 3: inputName = 'designFunctions'; access = accessList   
    elif input == 4: inputName = 'postProcFunctions'; access = accessList   
    else: inputName = 'panelBay_' + str(input-4)

    ghenv.Component.Params.Input[input].NickName = inputName
    ghenv.Component.Params.Input[input].Name = inputName
    ghenv.Component.Params.Input[input].Access = access
    
ghenv.Component.Attributes.Owner.OnPingDocument()

import Grasshopper.Kernel as gh
import rhinoscriptsyntax as rs
import Rhino
import scriptcontext as sc
from types import *
import random
import copy
import math

SGLibPanel = sc.sticky["SGLib_Panel"]
SGLibSkin = sc.sticky["SGLib_Skin"]
SGLibDesignFunction = sc.sticky["SGLib_DesignFunction"]

#GLOBAL PARAMETERS-------------------------------------------------------
INSTANCE_ID = str(ghenv.Component.InstanceGuid).rsplit("-",1)[1]
SKIN_NAME = INSTANCE_ID

DEFAULT_OFFSET_LEVEL = 0.0          #Offset distance of first panel at segments.Use list for different dimensions at each segment 
DEFAULT_OFFSET_PATH = 0.0           #Offset in elevation from path to be considered bottom of panel. Any value > 0 creates custom panel. 

DEFAULT_SKIN_WRAP = True            #Wrap at corners or create custom corner panels
SKIN_WRAP = True

RESET_BAY_AT_POINTS = True  #Start new bay at new segment

FLAT_MODE = False           #Low geometry mode
DRAW_MODE = "DEFAULT"       #"LADYBUG", "DEFAULT" 

DEFAULT_BAY_LIST = None     #default bays used in skin - 'None' will use all panel bays connected

SIZE_FROM_BAY_NUM = 0       # bay index to use to obtain grid size
FLOOR_HEIGHT = 0            #To set a floor height different than the panel height, otherwise
                            #uses height  of panel index provideded.

MIN_PANEL_WIDTH = .1        #if surface cell width is below this number it will be ignored and panel won't be created.
MIN_PANEL_HEIGHT = .1       #if surface cell height is below this number will be ignored and panel won't be created.

RANDOM = random.Random()    #Global Random generator object

GENERATE_PANELS_ONLY = False

RESET_DF_AFTER_SURFACE = False #Reset Design FUnctions between surfaces

#Internal global variables
warningData = []
PanelBay_List = []


def CheckInputs():
    
    global warningData

    fatalError = False
    if _skinSurfaceList == [] or _skinSurfaceList == [None]: 
        warningData.append("Empty skinSurfaceList input - Unable to run")
        fatalError = True
    else:
        
        #-----skin parameters format check -----------------------------------------
        if skinParameters <> []:
            result = []
            for action in skinParameters: 
                if action : 
                    try:
                        glVar = action.split("=", 1)
                        if len(glVar) <> 2: raise UserWarning
                        action = "global " + glVar[0] + "\n\r" + action
                        codeObj= compile(action,'<string>','exec')
                        eval(codeObj)
                    except:
                        result.append(action)
            if result <> [] : warningData.append("Invalid skinParameters: " + str(result))
            
        #-----design functions-----------------------------------------
        if designFunctions <> []:
            for index, df in enumerate(designFunctions):
                if df.__class__.__name__ not in ["LayoutDesignFunction", "PanelDesignFunction"]:
                    warningData.append("Invalid Design Function #"+str(index+1)+": "+str(df))
                    designFunctions[index] = None
            try: 
                while True: designFunctions.remove(None)
            except: pass
            
        #-----post processing parameters -----------------------------------------
        if postProcFunctions <> []:
            for index, ppf in enumerate(postProcFunctions):
                if ppf.__class__.__name__ not in ["PP_LadybugFunction"]:
                    warningData.append("Invalid Post Processing Function #"+str(index+1)+": "+str(ppf))
                    postProcFunctions[index] = None
            try: 
                while True: postProcFunctions.remove(None)
            except: pass
            
        #-----panel bay inputs -----------------------------------------                    
        global PanelBay_List
        numInputs = ghenv.Component.Params.Input.Count
        #Load panel bays from inputs to PanelBay_List
        for input in range(numInputs):
            item = ghenv.Component.Params.Input[input]
            if "panelBay_" in item.Name and item.VolatileDataCount > 0: 
                pList = []
                for i in range(item.VolatileDataCount) : 
                    if item.VolatileData.get_DataItem(i):
                        panel = item.VolatileData.get_DataItem(i).Value
                        if not isinstance(panel, SGLibPanel): 
                            warningData.append("Invalid Panel #"+str(i)+" in " + item.Name+" - process stopped")
                            fatalError = True
                            break
                        pList.append(panel)
                if pList : PanelBay_List.append(pList)
                
        
        #Create a default panel if no panel or panel bay provided
        if PanelBay_List == []: PanelBay_List.append([SGLibPanel()])
        
        #---Skin Parameters value check ----------------------------------------
        global DEFAULT_BAY_LIST
        if DEFAULT_BAY_LIST <> None:
            for bayNum in DEFAULT_BAY_LIST: 
                if bayNum not in range(1, len(PanelBay_List)+1):
                    warningData.append("Invalid defaultBayList value "+str(DEFAULT_BAY_LIST)+" in Skin Paramaters - process stopped")
                    fatalError = True; break
        
    return fatalError

#---------------------------------------------------------------------------------------------------------
#SKIN GENERATION SECTION
#---------------------------------------------------------------------------------------------------------


def SkinGenerator(myPanelBays, designFunctions):

    #Create Skin matrix (grid with panel bay dimensions)
    SkinList = []  #Skin class instances (one per polysurface)
    # assign skin name to panel
    for panelBay in myPanelBays:
        for panel in panelBay: panel.SetPanelProperty("SkinParentName", SKIN_NAME)
    
    for df in designFunctions: df.Reset() #reset values in design functions
        
        
    #-----Skin Vertices Matrix creation data ----------------------------------- 
    numBay = 0
    if DEFAULT_BAY_LIST <> None: numBay = DEFAULT_BAY_LIST[0]-1
    
    dblFloorToFloor = myPanelBays[numBay][0].GetPanelProperty("PanelHeight") #get bay height from first panel
    dblBayWidth = 0
    for panel in myPanelBays[numBay]:
        dblBayWidth += panel.GetPanelProperty("PanelWidth")#get bay width from adding up its panel widths
        
    #-----Skin generation data -----------------------------------------------------------------------------
    PanelTypes = {}
    BayData = {'BayCounter':[], 'PanelIndices':[], 'BayIndices':[]}
    BayData['BayCounter'] = myPanelBays + [0 for x in myPanelBays] #bay counters in format [list of bays, list of  counters]

    #Parameter/valuse list used to transfer data from Skin Generator component to Skin object 
    skinParams = [["OFFSET_LEVEL", DEFAULT_OFFSET_LEVEL], ["OFFSET_PATH", DEFAULT_OFFSET_PATH], ["SKIN_WRAP", DEFAULT_SKIN_WRAP],\
        ["RESET_BAY_AT_POINTS", RESET_BAY_AT_POINTS], ["FLAT_MODE", FLAT_MODE], ["DRAW_MODE", DRAW_MODE], ["BAY_LIST", DEFAULT_BAY_LIST],\
        ["MIN_PANEL_WIDTH", MIN_PANEL_WIDTH], ["MIN_PANEL_HEIGHT", MIN_PANEL_HEIGHT], ["RANDOM_OBJECT", RANDOM],\
        ["DESIGN_FUNCTIONS", designFunctions], ["GENERATE_PANELS_ONLY", GENERATE_PANELS_ONLY], ["FLOOR_HEIGHT", FLOOR_HEIGHT] ]

    global warningData
    #Create Panel Matrix and Panel Blocks one surface object at a a time
    for index, objSkinSurface in enumerate(_skinSurfaceList):
        #create skin object
        if objSkinSurface == None : 
            warningData.append("Invalid Surface object #"+str(index+1)+": "+str(objSkinSurface)); continue
        tmpSkin = SGLibSkin(SKIN_NAME+"_"+str(index), objSkinSurface, myPanelBays, designFunctions)
        # check if successful
        checkSurface = tmpSkin.GetProperty("SKIN_SURFACE_TYPE")
        if checkSurface <> "<type 'Brep'>" : 
            warningData.append("Invalid Surface object #"+str(index+1)+": "+str(objSkinSurface)); continue
        #load default parameters
        for param in skinParams: tmpSkin.SetProperty(param[0], param[1])
        #load paramters from surface object (specified in name)
        tmpSkin.LoadSurfaceProperties()
        #generate panel vertex matrix
        tmpSkin.GeneratePanelMatrix(dblBayWidth, dblFloorToFloor)
        #generate panel blocks
        PanelTypes, BayData = tmpSkin.GeneratePanelBlocks(PanelTypes, BayData) 
        
        SkinList.append(tmpSkin)
        skinWarningData = tmpSkin.GetProperty("WARNING_DATA")
        if skinWarningData <> []:
            skinName = tmpSkin.GetProperty("SKIN_NAME")
            for warning in skinWarningData: ghenv.Component.AddRuntimeMessage(gh.GH_RuntimeMessageLevel.Warning, str("Skin "+skinName+" Internal Warning:\n\r" +warning))    
        if RESET_DF_AFTER_SURFACE:        
            for df in designFunctions: df.Reset() #reset values in design functions
                

    return PanelTypes, BayData


#-----------------------------------------------------------------------------------------

#----------------------------------------------------------------------------------------
#Set Draw mode for panel bays(and their panels) provided
#----------------------------------------------------------------------------------------
def SetPanelsProperty(panelBayList, strPanelProperty, value):
    
    if strPanelProperty == "DrawMode":
        for panelBay in panelBayList:
            for panel  in panelBay :panel.SetDrawMode(value)
    else:
        for panelBay in panelBayList:
            for panel  in panelBay :panel.SetPanelProperty(strPanelProperty, value)

    
#---CLEAN UP SECTION--------------------------------------------------------------------------------
#----------------------------------------------------------------------------------------
#Delete Panels and its block instances 
#----------------------------------------------------------------------------------------
def DeletePanelObjects(skinName):
    if not sc.sticky[skinName] : return
    for group in sc.sticky[skinName].values() :
        for item in group :
            if isinstance(item[0], SGLibPanel):
                item[0].DeleteBlockCopies() #Delete blocks first if blocks created   
                item[0].HideAll() #same as deleting all panel objects




#---RUN---------------------------------------------------------------------------------------------------

#init
sc.doc = Rhino.RhinoDoc.ActiveDoc
rs.EnableRedraw(False)
unitSystem = sc.doc.ModelUnitSystem
_UNIT_COEF = 1
if unitSystem == Rhino.UnitSystem.Feet: _UNIT_COEF = 3.28084

# -------Check inputs ----------------------------------------------------------------
fatalError = CheckInputs()


# Initialize skin variables to the sticky dict
if "Panel_Data"+INSTANCE_ID+SKIN_NAME not in sc.sticky: sc.sticky["Panel_Data"+INSTANCE_ID+SKIN_NAME] = []
if "count" not in sc.sticky : sc.sticky["count"] = 0

#-----Delete previous skin if present

if "Skin_Name"+INSTANCE_ID in sc.sticky : 
    oldSkinName = sc.sticky["Skin_Name"+INSTANCE_ID]
    DeletePanelObjects("Panel_Data"+INSTANCE_ID+oldSkinName)

sc.sticky["Skin_Name"+INSTANCE_ID] = SKIN_NAME

skinPanelData = []
skinBayData = []
sc.sticky["count"] += 1


# -------Execute Panel Skin Comamands  -----------------------------------------------
#PanelBay_List = [] 

# Reset values
if not _activate:
    sc.sticky["count"] = 0

    #Clean up Panels data
    sc.sticky["Panel_Data"+INSTANCE_ID+SKIN_NAME] = []
    
    
#  If activated , store paramters and run skin generator  

elif not fatalError and PanelBay_List <> []: 
    
    print "> Skin Designer Log: Generator "+SKIN_NAME
    
    SetPanelsProperty(PanelBay_List, "DrawMode", 'DEFAULT')
    SetPanelsProperty(PanelBay_List, "LB_ShadeThreshold", 0.1 * _UNIT_COEF)
    #Run Post Proc. functions if available
    for ppf in postProcFunctions:
        ppf.PP_SetPanelsProperties(PanelBay_List)

    #------Run Skin Generator-----------------------------------
    
    skinPanelData, skinBayData = SkinGenerator(PanelBay_List, designFunctions)
    #Store in memory (for cleanup in next iteration)
    sc.sticky["Panel_Data"+INSTANCE_ID+SKIN_NAME] = skinPanelData

#-----Post Processing Run Section------------------------------------------------------------------

# init
postProcData = []
if "PostProc_Data" not in sc.sticky : sc.sticky["PostProc_Data"] = []

#Delete previous ladybug objects if created
#DeletePostProcObjects()
#sc.sticky["PostProc_Data"] = []
# ---- Post Processing functions-------------------------------------------------
if _activate and _skinSurfaceList != [] and PanelBay_List != []:
    if  postProcFunctions :       
        for ppFunc in postProcFunctions:
            postProcData.append(eval("ppFunc."+ppFunc.RunString()))
            
#-----Wrap up--------------------------------------------------------------------------
if warningData <> []: 
    for warning in warningData: ghenv.Component.AddRuntimeMessage(gh.GH_RuntimeMessageLevel.Warning, str(warning))
print sc.sticky["count"]
if skinPanelData : skinPanelData = skinPanelData.values()
if skinBayData :skinBayData = skinBayData.values()
rs.EnableRedraw(True)
sc.doc = ghdoc
