import re
import random
import math
import datetime

# -- Required for the Cura wrapper --
from ..Script import Script     # Cura plugin support

from time import sleep
import threading

from UM.Logger import Logger    # Write to Cura Log
from UM.Message import Message  # Progress bar
from PyQt6.QtCore import QCoreApplication   # Keep gui alive
from UM.Qt.QtApplication import QtApplication  # Check if cura has closed
# ------------------------------------


# Python 2.7 vs 3 compatibility
try:
    xrange
except NameError:
    xrange = range



# Main Class - Imported by Cura
# ==============================
class Woodgrain_Cura(Script):
    """
    This is a script that adds "texture" (thanks to temperature gradients), so as to get horizontal stripes that "look like wood".
  
    """        

    # 
    #   - Used in generating wood texture
    # =================================================
    class Perlin:

        def __init__(self, tile_dimension=256):
            self.tile_dimension = tile_dimension
            self.perm = [None] * 2 * tile_dimension

            permutation = []
            for value in xrange(tile_dimension): permutation.append(value)
            random.shuffle(permutation)

            for i in xrange(tile_dimension):
                self.perm[i] = permutation[i]
                self.perm[tile_dimension + i] = self.perm[i]

        @staticmethod
        def fade(t):
            return t * t * t * (t * (t * 6 - 15) + 10)

        @staticmethod
        def lerp(t, a, b):
            return a + t * (b - a)

        @staticmethod
        def grad(hash_code, x, y, z):
            # CONVERT LO 4 BITS OF HASH CODE INTO 12 GRADIENT DIRECTIONS.
            h = hash_code & 15
            if h < 8:
                u = x
            else:
                u = y
            if h < 4:
                v = y
            else:
                if h == 12 or h == 14:
                    v = x
                else:
                    v = z
            if h & 1 == 0:
                first = u
            else:
                first = -u
            if h & 2 == 0:
                second = v
            else:
                second = -v
            return first + second

        def noise(self, x, y, z):
            # FIND UNIT CUBE THAT CONTAINS POINT.
            X = int(x) & (self.tile_dimension - 1)
            Y = int(y) & (self.tile_dimension - 1)
            Z = int(z) & (self.tile_dimension - 1)
            # FIND RELATIVE X,Y,Z OF POINT IN CUBE.
            x -= int(x)
            y -= int(y)
            z -= int(z)
            # COMPUTE FADE CURVES FOR EACH OF X,Y,Z.
            u = self.fade(x)
            v = self.fade(y)
            w = self.fade(z)
            # HASH COORDINATES OF THE 8 CUBE CORNERS
            A = self.perm[X] + Y
            AA = self.perm[A] + Z
            AB = self.perm[A + 1] + Z
            B = self.perm[X + 1] + Y
            BA = self.perm[B] + Z
            BB = self.perm[B + 1] + Z
            # AND ADD BLENDED RESULTS FROM 8 CORNERS OF CUBE
            return self.lerp(w, self.lerp(v,
                self.lerp(u, self.grad(self.perm[AA], x, y, z), self.grad(self.perm[BA], x - 1, y, z)),
                self.lerp(u, self.grad(self.perm[AB], x, y - 1, z), self.grad(self.perm[BB], x - 1, y - 1, z))),
                self.lerp(v,
                    self.lerp(u, self.grad(self.perm[AA + 1], x, y, z - 1), self.grad(self.perm[BA + 1], x - 1, y, z - 1)),
                    self.lerp(u, self.grad(self.perm[AB + 1], x, y - 1, z - 1), self.grad(self.perm[BB + 1], x - 1, y - 1, z - 1))))

        def fractal(self, octaves, persistence, x, y, z, frequency=1):
            value = 0.0
            amplitude = 1.0
            total_amplitude = 0.0
            for octave in xrange(octaves):
                n = self.noise(x * frequency, y * frequency, z * frequency)
                value += amplitude * n
                total_amplitude += amplitude
                amplitude *= persistence
                frequency *= 2
            return value / total_amplitude



    # Controls the settings available in the "Extensions > Post Processing > Modify G-Code" dialog
    # =============================
    def getSettingDataString(self):
        # Note that "version 2" does not refer to this code, but possibly the version of the cura plugin system(?) 
        return """{
            "name": "Woodgrain Effect",
            "key": "Woodgrain",
            "metadata": {},
            "version": 2,
            "settings":
            {
                "grainSize":
                {
                    "label": "Average wood grain size",
                    "description": "Make it larger for slower change in texture, 3 mm is a good value",
                    "type": "float",
                    "value": "3",
                    "minimum_value": "0",
                    "unit": "mm"
                },
                "minTemp":
                {
                    "label": "Mininum Temperature",
                    "description": "It depends on your filament, but Laywoo-D3 should be fine with 190. Clogging is more likely to happen with low values.",
                    "type": "int",
                    "value": "190",
                    "minimum_value": "0",
                    "minimum_value_warning": "180",
                    "maximum_value_warning": "250",
                    "unit": "C"
                },
                "maxTemp":
                {
                    "label": "Maximum Temperature",
                    "description": "It depends on your filament, but Laywoo-D3 withstands 240 well. Warning though, because when it is too hot or left to stay too long, it may clog the nozzle with solid carbon.",
                    "type": "int",
                    "value": "240",
                    "minimum_value": "0",
                    "minimum_value_warning": "180",
                    "maximum_value_warning": "250",
                    "unit": "C"
                },
                "firstTemp":
                {
                    "label": "First layer temperature",
                    "description": "The first layer temperature can be set manually so it sticks like you need it to the bed. Leave it to zero if you want it to be computed like the other layers.",
                    "type": "int",
                    "value": "200",
                    "minimum_value": "0",
                    "minimum_value_warning": "180",
                    "maximum_value_warning": "250",
                    "unit": "C"
                },
                "maxUpward":
                {
                    "label": "Maximum upward temperature variation",
                    "description": "Some printer firmwares like that of the BFB may pause to reach temperatures suddenly rised by more than 10°C. This setting caps the maximum positive increase between two changes; else set it at zero for most other firmwares like Marlin.",
                    "type": "int",
                    "value": "0",
                    "minimum_value": "0",
                    "unit": "C"
                },
                "maxDownward":
                {
                    "label": "Maximum downward temperature variation",
                    "description": "",
                    "type": "int",
                    "value": "0",
                    "minimum_value": "0",
                    "unit": "C"
                },
                "spikinessPower":
                {
                    "label": "Spikiness",
                    "description": "Default is a balanced set of dark and light (1.0). With higher values (eg 2 or 3), the dark stripes will be made sparser. You can get the opposite effect with value between 0 and 1 (eg. 0.5 will generate fatter dark bands, convenient for filament that get lighter with temperature)",
                    "type": "float",
                    "value": "1.0",
                    "minimum_value": "0.001",
                    "unit": ""
                },
                "zOffset":
                {
                    "label": "zOffset",
                    "description": "Vertical shift of the variations, as shown at the end of the gcode file.",
                    "type": "float",
                    "value": "0",
                    "unit": "mm"
                },
                "scanForZHop":
                {
                    "label": "scanForZHop",
                    "description": "Lines to scan ahead for Z-Hop. Max 5, 0 to disable.",
                    "type": "int",
                    "value": "5",
                    "minimum_value": "0",
                    "maximum_value": "5",
                    "unit": ""
                }
            }
        }"""



    # The .execute method in run by cura when the user saves the gcode file
    #   - this is our code entry point
    # =======================
    def execute(self, data):
        Logger.log("d", "[Woodgrain Effect] Begin processing")

        # Show the progress bar
        self.progress_bar = Message(title="Apply Woodgrain Effect", text="This may take several minutes, please be patient.\n\n",
                                    lifetime=0, dismissable=False, progress=-1)
        self.progress_bar.show()

        # Start the processing thread
        self._locks = {}
        self._locks["metadata"] = threading.Lock()
        self._locks["output"] = threading.Lock()

        self.progress = (-1,0)
        self.output_gcode=[]

        self.apply_woodgrain_thread = threading.Thread(target=self.apply_woodgrain, args=(data,))
        self.apply_woodgrain_thread.start()

        # Keep the GUI responsive while we wait, even though this script blocks the UI thread
        GUI_UPDATE_FREQUENCY = 50           # as used in cura source
        PROGRESS_CHECK_INTERVAL = 1000      # milliseconds

        update_period = 1 / GUI_UPDATE_FREQUENCY
        updates_per_check = int(GUI_UPDATE_FREQUENCY * (PROGRESS_CHECK_INTERVAL / 1000))
        
        # Wait until the processing thread is done
        while True:
            for i in range(0, updates_per_check):
                QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
                sleep(update_period)
            
            # Grab an update on the progress
            self._locks["metadata"].acquire()
            progress = self.progress
            self._locks["metadata"].release()

            # Update progress bar
            self.progress_bar.setProgress((progress[0] / progress[1]) * 100)    # float(100) means complete

            # Check if Cura is still open.. 
            # If it's not, this loop will just run forever
            main_window = QtApplication.getInstance().getMainWindow()  
            if main_window is None:
                return None     #close out the loop

            #Check if we're done
            if progress[0] >= progress[1]:
                self.apply_woodgrain_thread.join()
                break

        # Wrap things up and pass the modified gcode back to cura
        Logger.log("d", "[Woodgrain Effect] End processing. " + str(progress[1]) + " iterations performed")
        self.progress_bar.hide()
        return self.output_gcode



    # Gotta do the real work in a seperate thread, to keep the GUI from freezing up and the user from panicking
    # =======================
    def apply_woodgrain(self, data):
        lines = []

        # Get the appropriate eol character for unix / windows
        if "\r\n" in data[0]:
            eol = "\r\n"
        else:
            eol = "\n"

        # Deconstruct the gcode
        #   - One layer may have more than one command. To be safe we pull everything apart
        for layer in data:
            gcode_line = layer.split(eol)
            for line in gcode_line:
                lines.append(line)  #This is now our main source of data

        # Get the parameters from the "Extensions > Post Processing > Modify G-Code" dialog
        #   - Method is not defined here, but rather imported from Cura's "..Script" module.
        #==========================================
        minTemp = int(self.getSettingValueByKey("minTemp"))
        maxTemp = int(self.getSettingValueByKey("maxTemp"))
        firstTemp = int(self.getSettingValueByKey("firstTemp"))
        grainSize = float(self.getSettingValueByKey("grainSize"))
        maxUpward = int(self.getSettingValueByKey("maxUpward"))
        maxDownward = int(self.getSettingValueByKey("maxDownward"))
        zOffset = float(self.getSettingValueByKey("zOffset"))
        scanForZHop = int(self.getSettingValueByKey("scanForZHop"))
        spikinessPower = float(self.getSettingValueByKey("spikinessPower"))
        tempCommand = 'M104'
        skipStartZ = 0


        # (method for identifying gcode commands, not related to cura wrapper)
        def get_value(gcode_line, key, default=None):
            if not key in gcode_line or (';' in gcode_line and gcode_line.find(key) > gcode_line.find(';')):
                return default
            sub_part = gcode_line[gcode_line.find(key) + 1:]
            m = re.search('^[0-9]+\.?[0-9]*', sub_part)
            if m is None:
                return default
            try:
                return float(m.group(0))
            except:
                return default


        def get_z(line, default=None):
            # Support G0 and G1 "move" commands
            if line.startswith(";WoodGraph:"):
                return default
            if get_value(line, 'G') == 0 or get_value(line, 'G') == 1:
                return get_value(line, 'Z', default)
            else:
                return default

        # Limit the number of changes for helicoidal/Joris slicing method
        minimumChangeZ = 0.1

        # Find the total height of the object (minus optional additional Z-hops)
        maxZ = 0
        thisZ = 0

        # Note: data source is now lines array, instead of old f.readlines
        for line in lines:
            thisZ = get_z(line)
            if thisZ is not None:
                if maxZ < thisZ:
                    maxZ = thisZ

        #First pass generates the noise curve. We will normalize it as the user expects to reach the min & max temperatures
        perlin = self.Perlin()


        def perlin_to_normalized_wood(z):
            banding = 3
            octaves = 2
            persistence = 0.7
            noise = banding * perlin.fractal(octaves, persistence, 0, 0, (z + zOffset) / (grainSize * 2))
            noise = (noise - math.floor(noise))  # normalized to [0,1]
            noise = math.pow(noise, spikinessPower)
            return noise


        # Generate normalized noises, and then temperatures (will be indexed by Z value)
        noises = {}
        # first value is hard encoded since some slicers do not write a Z0 at the first layer!
        noises[0] = perlin_to_normalized_wood(0)
        pendingNoise = None
        formerZ = -1
        for line in lines:
            thisZ = get_z(line, formerZ)

            if thisZ > 2 + formerZ:
                formerZ = thisZ
            # noises = {}  # some damn slicers include a big negative Z shift at the beginning, which impacts the min/max range
            elif abs(thisZ - formerZ) > minimumChangeZ and thisZ > skipStartZ:
                formerZ = thisZ
                noises[thisZ] = perlin_to_normalized_wood(thisZ)

        # normalize built noises
        noisesMax = noises[max(noises, key=noises.get)]
        noisesMin = noises[min(noises, key=noises.get)]
        for z, v in noises.items():
            noises[z] = (noises[z] - noisesMin) / (noisesMax - noisesMin)


        def noise_to_temp(noise):
            return minTemp + noise * (maxTemp - minTemp)

        scanForZHop = int(scanForZHop)  # fix unicode error when using in range
        if scanForZHop > 5:
            scanForZHop = 5


        def z_hop_scan_ahead(index, z):
            if scanForZHop == 0:
                return False  # Do not scan ahead
            for i in range(scanForZHop):
                checkZ = get_z(lines[index + i], z)
                if checkZ < z:
                    return True  # Found z-hop
            return False  # Did not find z-hop


        # Drop-in replacement for old file writer
        #   - Stores data as a list of lines, terminated with eol
        # =============================================================
        class write_to_list:
            def __init__(self):
                self.content = ""
            def write(self, chars):
                self.content += (chars + eol)
            def get_data(self):
                list_output = []
                for line in self.content.split(eol):
                    list_output.append(line + eol)
                return list_output
        f = write_to_list()
        #==============================================================


        #
        # Now save the file with the patched M104 temperature settings
        #
        
        # Prepare a transposed ASCII-art temperature graph for the end of the file

        f.write(";woodified gcode, see graph at the end - [email protected] - generated on " +
                datetime.datetime.now().strftime("%Y%m%d-%H%M") + eol)
        warmingTempCommands = "M230 S0" + eol  # enable wait for temp on the first change
        t = firstTemp
        if t == 0:
            t = noise_to_temp(0)
        warmingTempCommands += ("%s S%i" + eol) % (tempCommand, t)
        # The two following commands depends on the firmware:
        warmingTempCommands += "M230 S1" + eol  # now disable wait for temp on the first change
        warmingTempCommands += "M116" + eol  # wait for the temperature to reach the setting (M109 is obsolete)
        f.write(warmingTempCommands)

        graphStr = ";WoodGraph: Wood temperature graph (from " + str(minTemp) + "C to " + str(
            maxTemp) + "C, grain size " + str(grainSize) + "mm, z-offset " + str(zOffset) + ", scanForZHop " + str(scanForZHop) + ")"
        if skipStartZ:
            graphStr += ", skipped first " + str(skipStartZ) + "mm of print"
        if maxUpward:
            graphStr += ", temperature increases capped at " + str(maxUpward)
        if maxDownward:
            graphStr += ", temperature decreases capped at " + str(maxDownward)
        graphStr += ":"
        graphStr += eol

        thisZ = -1
        formerZ = -1
        warned = 0

        postponedTempDelta = 0  # only when maxUpward is used
        postponedTempLast = None  # only when maxUpward is used
        skip_lines = 0
        total_length = len(lines) - 1   #For cura wrapper progress
        for index, line in enumerate(lines):

            # Cura wrapper - send progress back to gui
            # Todo - decrease frequency of this snippet?
            self._locks["metadata"].acquire()
            self.progress = (index, total_length)
            self._locks["metadata"].release()

            if "; set extruder " in line.lower():  # special fix for BFB
                f.write(line)
                f.write(warmingTempCommands)
                warmingTempCommands = ""
            elif "; M104_M109" in line:
                f.write(line)  # don't lose this remark!
            elif skip_lines > 0:
                skip_lines -= 1
            elif ";woodified" in line.lower():
                skip_lines = 4  # skip 4 more lines after our comment
            elif not ";woodgraph" in line.lower():  # forget optional former temp graph lines in the file
                if thisZ == maxZ:
                    f.write(line)  # no more patch, keep the important end scripts unchanged
                elif not "m104" in line.lower():  # forget any previous temp in the file
                    thisZ = get_z(line, formerZ)
                    if thisZ != formerZ and thisZ in noises and not z_hop_scan_ahead(index, thisZ):

                        if firstTemp != 0 and thisZ <= 0.5:  # if specified, keep the first temp for the first 0.5mm
                            temp = firstTemp
                        else:
                            temp = noise_to_temp(noises[thisZ])

                            # possibly cap temperature change upward
                            temp += postponedTempDelta
                            postponedTempDelta = 0
                            if (postponedTempLast is not None)\
                                    and (maxUpward > 0)\
                                    and (temp > postponedTempLast + maxUpward ):
                                postponedTempDelta = temp - (postponedTempLast + maxUpward)
                                temp = postponedTempLast + maxUpward
                            if (postponedTempLast is not None)\
                                    and (maxDownward > 0)\
                                    and (temp < postponedTempLast - maxDownward ):
                                postponedTempDelta = postponedTempLast - maxDownward - temp
                                temp = postponedTempLast - maxDownward
                            if temp > maxTemp:
                                postponedTempDelta = 0
                                temp = maxTemp
                            postponedTempLast = temp

                            f.write(("%s S%i" + eol) % (tempCommand, temp))

                        formerZ = thisZ

                        # Build the corresponding graph line
                        t = int(19 * (temp - minTemp) / (maxTemp - minTemp))
                        graphStr += ";WoodGraph: Z %03f " % thisZ
                        graphStr += "@%3iC | " % temp
                        graphStr += '#'*t + '.'*(20 - t)
                        graphStr += eol

                    f.write(line)
        f.write(graphStr + eol)

        # Fix incorrect values for first layer
        #   -   In testing, the script was not correctly setting the first layer temperatures for my prints
        #       As a bandaid, this snippet will manually hunt out the first M104 and fix it
        #       TODO: implement for temperature commands other than M104?
        self._locks["output"].acquire()
        first_layer_done = False
        for line in f.get_data():
            if not first_layer_done:
                if ";LAYER:0" in line:
                    first_layer_done = True
                elif "M104" in line and not ("M104 S" + str(firstTemp)) in line:
                    continue

            self.output_gcode.append(line)

        # Gcode now finalized, thread terminated
        # ============================================================
        self._locks["output"].release() 

Python Online Compiler

Write, Run & Share Python code online using OneCompiler's Python online compiler for free. It's one of the robust, feature-rich online compilers for python language, supporting both the versions which are Python 3 and Python 2.7. Getting started with the OneCompiler's Python editor is easy and fast. The editor shows sample boilerplate code when you choose language as Python or Python2 and start coding.

Taking inputs (stdin)

OneCompiler's python online editor supports stdin and users can give inputs to programs using the STDIN textbox under the I/O tab. Following is a sample python program which takes name as input and print your name with hello.

import sys
name = sys.stdin.readline()
print("Hello "+ name)

About Python

Python is a very popular general-purpose programming language which was created by Guido van Rossum, and released in 1991. It is very popular for web development and you can build almost anything like mobile apps, web apps, tools, data analytics, machine learning etc. It is designed to be simple and easy like english language. It's is highly productive and efficient making it a very popular language.

Tutorial & Syntax help

Loops

1. If-Else:

When ever you want to perform a set of operations based on a condition IF-ELSE is used.

if conditional-expression
    #code
elif conditional-expression
    #code
else:
    #code

Note:

Indentation is very important in Python, make sure the indentation is followed correctly

2. For:

For loop is used to iterate over arrays(list, tuple, set, dictionary) or strings.

Example:

mylist=("Iphone","Pixel","Samsung")
for i in mylist:
    print(i)

3. While:

While is also used to iterate a set of statements based on a condition. Usually while is preferred when number of iterations are not known in advance.

while condition  
    #code 

Collections

There are four types of collections in Python.

1. List:

List is a collection which is ordered and can be changed. Lists are specified in square brackets.

Example:

mylist=["iPhone","Pixel","Samsung"]
print(mylist)

2. Tuple:

Tuple is a collection which is ordered and can not be changed. Tuples are specified in round brackets.

Example:

myTuple=("iPhone","Pixel","Samsung")
print(myTuple)

Below throws an error if you assign another value to tuple again.

myTuple=("iPhone","Pixel","Samsung")
print(myTuple)
myTuple[1]="onePlus"
print(myTuple)

3. Set:

Set is a collection which is unordered and unindexed. Sets are specified in curly brackets.

Example:

myset = {"iPhone","Pixel","Samsung"}
print(myset)

4. Dictionary:

Dictionary is a collection of key value pairs which is unordered, can be changed, and indexed. They are written in curly brackets with key - value pairs.

Example:

mydict = {
    "brand" :"iPhone",
    "model": "iPhone 11"
}
print(mydict)

Supported Libraries

Following are the libraries supported by OneCompiler's Python compiler

NameDescription
NumPyNumPy python library helps users to work on arrays with ease
SciPySciPy is a scientific computation library which depends on NumPy for convenient and fast N-dimensional array manipulation
SKLearn/Scikit-learnScikit-learn or Scikit-learn is the most useful library for machine learning in Python
PandasPandas is the most efficient Python library for data manipulation and analysis
DOcplexDOcplex is IBM Decision Optimization CPLEX Modeling for Python, is a library composed of Mathematical Programming Modeling and Constraint Programming Modeling