Multi colour printing without using wipe or prime towers

I’ve marked this as a feature post. If you are interested in this, check my blog for the latest updates.

As I have promised on a couple of forums, this post will explain in detail the method I am developing to enable 3D printing of multi colour objects without resorting to the use of wipe or prime towers.

For those of you who haven’t been following my endeavours on any forums, here is a link to the video I posted on YouTube which shows the technique in action.

Now before we get too far into the details, I need to explain few things about myself. I was born in 1953 and left school/started work in 1969 at age 16. I learnt my maths using log tables and a slide rule. I’m a mechanical engineer by training but currently (career number 5) I have a small business make designing and building garden decks. What I know about computers, 3D printers, electronics, writing code etc is all self taught through a lot of internet searches and reading a few books and online tutorials so I’m no expert.

I started setting up this blog/web site on 4th January 2017 so I’m still finding may way around. Apart from a brief introductory post, this is the first blog post I have ever made. Therefore, the layout of this blog site may well change (and please excuse any typos)

That’s the excuses out of the way, now down to business…..

The rest of this post assumes that the reader has the tools and techniques available to generate or otherwise obtain a .gcode file for a multicoloured object. I plan to do a separate write up on the method I use, which involves separate .stl files (which I generate using OpenScad) and slic3r but that will have to wait.

There are a number of issues which have to be addressed when printing multicoloured objects and most of them revolve around what happens when switching from one filament to another. My first forays into multicolour printing were with a tricolour Mendel variant which used 3 separate hot ends. I found that there were 4 major drawbacks to this approach. Firstly it was very difficult to get all 3 nozzles at exactly the same height (within less than 0.1mm) and for this setting to be maintained in such a way that inactive nozzles didn’t drag across the printed part. Secondly, it was very difficult to set and maintain  X and Y offsets get the output from all 3 nozzles perfectly aligned. Thirdly, it was necessary to set unused nozzles to a lower standby temperature in order to prevent filament from oozing out and dragging on the print but this was never completely successful. Also, the waiting time for one nozzle to heat up and another to cool down significantly increased the print duration. Lastly, there was the problem of filament oozing from one nozzle as it cooled down and before it reached it’s standby temperature, or another nozzle as it heated up before it reached it’s active temperature.

All of the above problems can by overcome by using a single nozzle – i.e. a mixing hot end. For the purpose of this post, I will define a mixing hot end as one which has multiple filament inputs but a single output (nozzle). I am currently using a Diamond hot end but I am aware that there are other designs.

However, this approach generates a new problem which is that when switching between filaments, there is still some of the old filament left in the nozzle and/or mixing chamber which has to be purged before the next filament comes through. If the colour change coincides with a layer change, this is rarely an issue. Unless the object being printed is really small, what happens is that the purge occurs during the inner perimeter(s) so by the time the outer perimeters are being laid down, the purge has already been completed. Effectively the change over period is hidden inside the object and isn’t visible.

The real problem is when the colour change occurs within the same layer, as in the union flag example that is in the YouTube video, because there is nowhere to hide and any defects will be visible. The usual approach is to move the print head away from the main object, and print some sort of sacrificial tower or use some sort of wiping mechanism to purge out the old filament, then move back to the object and resume printing. This is wasteful both in terms of filament and time, and can lead to a less than satisfactory print finish if filament retraction and recovery are not fully optimised.

The technique I’m developing and which I am about to share here, simply moves the filament change point to a position earlier in the gcode file. The new position equates to the amount of filament that needs to be extruded in order to purge out the old. The method isn’t perfect because in theory, there is still a “transition” period where what is being extruded is a mixture of the old colour and the new. In practice, by observation I’d estimate this transition to be about 10% of the total “purge amount” and I have found that with the Diamond hot end the optimum purge amount is about 2.0 to 2.5mm of filament so there is around 0.2 to 0.25mm of “transition” filament which is hardly noticeable. Other designs of hot end may have larger mixing chambers, in which it may still be necessary to use some sort of wipe or prime tower. However, my technique could still be applied to take account of much of the purging so any further priming could be achieved by using a very much smaller tower or priming mechanism. One of my planned improvements is to build a small priming tower or similar mechanism into the post processing script, although as I have said earlier, the transition between one colour and the next with the Diamond hot end is really small and hardly noticeable.

As mentioned earlier, the only coding I know how to do is what I have been able to teach myself from internet searches, on-line tutorials and the odd book (some have been very odd). I have used Python just because I found it less of a pain to learn than some other languages that I’ve tried. I am aware that this might not be the best tool and I am deeply aware of my own shortcomings so I think the best approach is for me to post my work flow and explanation of what the code attempts to do. Others with more skill than I have can then use it as basis to write something more efficient (and probably less dangerous). I’ll make the python script available for those want to try it but please don’t blame me if it crashes your PC or worse.

So here goes.

The approach I’m taking is to leave the original file untouched and generate a new file with the colour change commands moved forward in the file but otherwise it’s an exact copy. This enables me to run the script several times on the same file but with a different “purge” setting in order to test and fine tune. The change from one colour to another in the gcode file is accomplished by selecting a new tool. It could be done by having a single tool and changing the mixing ratio but that’s not how slc3r works. So for the rest of this post colour changes will be referred to as tool changes. Note that the code could be adapted to look for M563 commands (mixing ratio changes) instead or as well as “Tn” (tool change) commands.

I haven’t yet figured out how to cut and paste the python text while retaining the font colours so I’ll change them manually – hopefully I won’t miss any. Comments are in red and start with a hash (#) String values are in green. My other comments are in blue to denote the fact that are not part of the script.

These first lines should be changed to suit the user. Hopefully the comments make them self explanatory.

sourceFile = “D:\RepRap\G Code Files Diamond hot end\Python test file.gcode” # The full path and file name of the file to be worked on
destinationFile=“D:\RepRap\G Code Files Diamond hot end\Python output.gcode” # the full path and file name of the destination (output) file
temporaryFile =“D:\RepRap\G Code Files Diamond hot end\Python temp.gcode” # A temporary file used to hold smallish sections of the original and which gets read into memory.
purgeAmount =2.5 # This is the amount of filament to be purged when a tool change is needed

The next thing I struggled with was how to detect the start of a print proper. Slic3r inserts all sorts of comments both before and after the actual print and I use quite a complex start gcode in my slic3r setting. In the end I decided to use flags in my start and end gcodes. So the last line of my start gcode is a comment “;begin” and the first line of my end gcode is the comment “;end”. The main reason for doing this is that the script simply looks for the text “T” to detect a tool change and as “T” can be found in comments and both pre and post print sections, I need to restrict the search.  

beginText=“begin” #string to look for to detect start of gcodes proper. This should be the last line of slicer start gcode
endText=“end” #string to look for to detect end of gcodes proper. This should be the first line of slicer start gcode
searchToolText=“T” # String to look for to detect a tool command (which is why we need to restrict the search to between “begin” and “end”
# Tool number could be anything after the “T” so we can’t can’t search for “Tnn” explicitly

Here are all the other variables that I have used. I know that I probably shouldn’t have declared them as global variables but at least they are all in this section so easy to find what they do. I also know that I’ve probably used a lot of unnecessary flags and that they could be boolean rather than integers etc etc.

beginFlag=0 # flag to detect if the word “begin” is present
endFlag=0 # flag to detect if the word “end” is present
#There is nothing to check that begin is indeed before end – maybe do this check too.
toolFlag=0 # flag to detect that tool changes are present
toolChanges=0 # number of tool changes in the file
currentTool=“T0” #default this to “T0”
lineText=“” # text read in from file
startLineNo=0 # the line number in the file immediately after the word “begin”
toolChangeLineNo=0 # the line number in the file where a tool change occurs

newToolPos=0 # the position where the tool command will be inserted
lastLine = 0 # the position of the last line in the temp file
lineToRead=0 # pointer to which line to read in the temp file
estart=0 # position in the line after the letter “E” which denotes the position of the start of digits for extrusion
estring=“” # the string of 7 digits following the letter E
EAmount=0.0 # estring converted to a number
totEAmount=0.0 # the sum of all EAmounts
EAmountNeeded=0 # the purge amount less the totEAmount
notEnoughFlag=0 # flag to set if the the totEAmount is less than needed for a full purge between tool changes

xStartPrev=0 # the position in the previous line where X is
xPrevString=“” # read the 7 characters after “X”
xPrevNum=0.00 # convert to number – First X value

xStartHere=0 # the position in this line where X is
xHereString=“” # read the 7 characters after “X”
xHereNum=0.0 # convert to number – Second X value

xMove=0 #xHereNum-xPrevNum

yStartPrev=0 # the position in the previous line where Y is
yPrevString=“” # read the 7 characters after “Y”
yPrevNum=0.0 # convert to number – First Y value

yStartHere=0 # the position in this line where Y is
yHereString=“” # read the 7 characters after “Y”
yHereNum=0.0 # convert to number – Second Y value

yMove=0 # yHereNum-yPrevNum

E1 = 0 # EAmount-EAmountNeeded
E2 = 0 # EAmountNeeded

X2 =0 # xHereNum
X1 =0 # PrevNum +(xMove/eAmount*E2)
Y2 =0 # yHereNum
Y1 =0 # yPrevNum +(yMove/eAmount*E2)

E1string=“” # used to build insertString1 – 1st E amount
E2string=“” # used to build insertString2 – 2nd E amount
X1string=“” # as above but X values
X2string=“”
Y1string=“” # as above but Y values
Y2string=“”

insertCheck=0 # used in loop to check lines are valid for inserting text
firstInsert=0 # modified from newToolPos if that position contains something other than a normal XYE move
secondInsert=0# as above

insertString1=“” # 1st (new) move string prior to tool change i.e. “G1 Xnnn.nnn Ynnn.nnn En.nnnnn”
insertString2=“” # then the tool change
insertString3=“” # then the second move string

changesDone=0 #number of tool changes done

So now we start the script proper. This section should all be self explanatory. I don’t know how to indent individual lines within a paragraph using this WordPress thingy so I’m having to do it by starting a new paragraph wherever the text needs to be indented.  Therefore there are what look like a lot of blank lines in what follows but Python relies on indentation for the code to work so it’s important that I show it.

# Start by reading through the file to check that “;begin” and “;end” are present
startfile=open(sourceFile,‘r’) # Open file as read only
numLines=sum(1 for line in (startfile))

startfile.seek(0)# set to start of file (just in case)
print (“Checking File…”)
for x in range (0,numLines):

lineText=(startfile.readline())
if endText in lineText:

endFlag=x
break # exit loop on first instance of “end”

for x in range (0,numLines):

lineText=(startfile.readline())
if beginText in lineText:

beginFlag=x
break # exit loop on first instance of “begin”

print (“Number of lines =”,numLines) # Just for info
if beginFlag>0:

print (“\”;begin\” text is present at line “,beginFlag) # Just for info – print a message to say that the search string was present.

else:

print (“There is no \”begin\” statement in the file”) # If not print a different message to say that the string wasn’t found…….
print (“Add \”begin\” to the end of the start gcode in Slicer”) # ….and tell the user what to do about it.
print (“And/Or edit the file and start again”)
startfile.close()
exit() pr# if there is no “begin” text string in the source file then end the script after printing the message and closing the file.

if endFlag>0:

print (“\”;end\” text is present at line “,endFlag) # comments as above
print(“”)

else:

print (“There is no \”end\” statement in the file”)
print (“Add \”end\” to the end of the start gcode in Slicer”)
print (“And/Or edit the file and start again”)
startfile.close()
exit()# if there is no “end” text string in the source file then end the script after printing the message and closing the file.

startfile.close() # I always like to close a file when an operation on it has finished – just in case.

This next bit is just for information really although the number of tool changes is used further on in the code and if there aren’t any tool changes found, it will print a message and exit. 

#so now read through and count the number of tool changes between begin and end
startfile=open(sourceFile,‘r’)
startfile.seek(0)

print (“Checking for tool changes…”)
for x in range (beginFlag,endFlag):

lineText=(startfile.readline())
if searchToolText in lineText:

toolFlag=toolFlag+1

toolChanges=toolFlag-1 # the first occurrence will be the first tool, so take 1 off to get the number of tool changes.
if toolChanges>0:

print (“There are “,toolChanges-1,“tool changes in the file”)
print(“”)
startfile.close()

else:

print (“There are no tool changes in this file”)
print (“So no changes will be made.”)
startfile.close()
exit()

startfile.close()

Now I read through the file and copy everything up to the first tool change into the output file

# Now read through the file and copy everything up to the first tool change
startfile=open(sourceFile,‘r’) # open source file again for reading
startfile.seek(0) # make sure we start at the beginning
endfile = open(destinationFile,‘w’)# open the destination file for writing
print (“Reading file and writing lines up to first tool change”)
print(“”)
#start by copying everything up to the begin statement
for x in range (0,numLines):

lineText=(startfile.readline())#read line from input file
# uncomment the following line with caution – it really slows things down.
#print (lineText)
endfile.write(lineText) # copy line to output file

if beginText in lineText: #is there a tool string present in this line?

startLineNo=x # if so, set the variable StartLineNo to x which will be the text file line number

break #end the loop when the first tool line number has been reached

# end of for loop

endfile.close()

# So now check for first instance of a tool (check gcode file for a “T”)
# also, append all the lines up to this point to the end file
# Start by opening the end file again in append mode

endfile = open (destinationFile, ‘a’) # open again in append mode

for x in range (startLineNo,numLines): # miss out the first lines from 0 to StartLine No

lineText=(startfile.readline())#read line from input file
# uncomment the following line with caution – it really slows things down.
#print (lineText)
endfile.write(lineText) # copy line to output file but this time we are amending it to the end of the file
if searchToolText in lineText: #is the string present in this line?
currentTool=lineText # if so, set the variable CurrentToolText to whatever this line holds (string value)
toolChangeLineNo=x
endfile.close()
break #exit loopif

endfile.close()

# *********** Now it gets tricky************

What I do next is copy chunks of the source file between tool changes into a temporary file. The reason for doing so is that I wanted to read this file into a list so that I can work on it. Maybe I could read the entire file into memory but potentially it could be a huge file and gobble up all the PCs resources. David Crocker (DC42 of Duet fame) has said that if he were doing this in firmware, he would have two streams – the original and the output with the a kind of buffer that would hold just the number of commands that make up the purge amount, rather than this approach which holds all the commands between one tool change and the next before they are written to the output file. Thinking about it, that would be a far better approach but the following is how it is now. So, to continue…

(Oh by the way, there are quite a few commented out print statements in what follows which were only used for debugging)

# Read lines from latest tool to next tool and stick them in a temporary file

for changesDone in range (1,toolChanges):# repeat the rest of this until we reach the end of the file

print (“Doing tool change “,changesDone)
totEAmount=0 # reset to zero
newToolPos=0 # reset to zero

tempfile = open(temporaryFile, ‘w’) # This will create a file if it doesn’t exist and/or overwrite.
print (“Writing to temporary file……”)
for x in range (toolChangeLineNo,numLines): # miss out the first lines from 0 to StartLine No

#print (“Tool change line number = “,toolChangeLineNo)
lineText=(startfile.readline())#read line from input file

# uncomment the following line with caution – it really slows things down.
#print (LineText)
tempfile.write(lineText) # copy line to temporary output file
if searchToolText in lineText: #is the string present in this line?

currentTool=lineText # if so, set the variable CurrentTool (Text) to whatever this line holds (string value)
toolChangeLineNo=x # so update the pointer to the current line number…
break #… and exit loop

if x==numLines: # if we reach the end of the file (we should never get here)
print (“No more tool changes”) # print message, close files and quit

tempfile.close()
endfile.close()
startfile.close()
quit()

#end of x loop – still in changesDone loop

tempfile.close()# Close the file for writing to

#now read the whole tempfile into a list so we can work on it
f=open(temporaryFile,“r”)
templines =f.readlines()
f.close()
lastLine =(len(templines)-1) # Len is the number of lines in the file but the last line will be 1 less because the first line is indexed as 0.
#print (templines[lastline]) # just for debugging
print(“Calculating new tool position”)
#Start at the end of the file and work backwards
# read the 7 characters after the “E” in a line
notEnoughFlag=0 # Reset flag (start at zero)
for j in range(1,lastLine): # set range to the number of lines in the file so that it can’t go back beyond that

lineToRead=lastLine-j # read the line
#print (templines[lineToRead]) # for debugging only – comment out later
if “E” in templines[lineToRead]: # check that the line does actually have an E value

estart=templines[lineToRead].find(“E”) # find the position in the line where E is
estring=templines[lineToRead][estart+1:estart+8] # read the 7 string characters after the E
#print(estring) # just for debugging – comment out later
EAmount=float(estring)# convert the string to a number
#print (EAmount) # just for debugging – comment out later
totEAmount=totEAmount+EAmount # add this amount to the previous amount to get a total
#print (totEAmount) # just for dbugging – comment out later
if totEAmount > purgeAmount:

newToolPos=lineToRead #Set the new tool position to lastline -j (the point in the line where the sum of all the extruder commands excedes the purge amount).
#print(“new tool position “,j)
break #exit the loop

if j==lastLine: # if we go all the way back

newToolPos=lineToRead # then there aren’t enough moves to purge fully so set new tool pos to the earliest position
print (“WARNING Not enough extrusion between tool changes to fully purge”)
notEnoughFlag=1

j=j+1

# so now we have the position for the tool where adding the Eamount from the line after excedes the amount needed to purge
# but it’s counting backwards from the end
print(“Splitting Gcode line at insert point”)
# so I want to use only the amount needed, therefore I have to split the line
#the amount needed will be the ToteAmount minus the purgeAmount
EAmountNeeded = totEAmount-purgeAmount
#get the x an y totals for the line that needs to be split

# ************Trap for the line not being a normal XYE move **************
# ************If it isn’t go back and keep going back until we find one **************

insertCheck=0
secondInsert=newToolPos
if notEnoughFlag==1: # skip the next bit if flag set

insertCheck=0

while insertCheck ==0:

if “G1 X” in templines[secondInsert]:

insertCheck=1
break

else:
secondInsert=secondInsert-1
insertCheck=0

# Now get the x value from this line

xStartHere=templines[secondInsert].find(“X”) # the position in this line where X is
xHereString=templines[secondInsert][xStartHere+1:xStartHere+8] # read the 7 characters after “X”
xHereNum=float(xHereString) # convert to number – Second X value

# and do the same for the y value
yStartHere=templines[secondInsert].find(“Y”) # the position in this line where Y is
yHereString=templines[secondInsert][yStartHere+1:yStartHere+8] # read the 7 characters after “Y”
yHereNum=float(yHereString) # convert to number – Second Y value

#Now do it all again for the previous line

insertCheck=0
firstInsert=newToolPos-1 #take 1 away to get the previous line
if notEnoughFlag==1: # skip the next bit if flag set

insertCheck=0

while insertCheck ==0:

if “G1 X” in templines[firstInsert]:

insertCheck=1
break

else:
firstInsert=firstInsert-1
insertCheck=0

xStartPrev=templines[firstInsert].find(“X”) # the position in the previous line where X is
xPrevString=templines[firstInsert][xStartPrev+1:xStartPrev+8] # read the 7 characters after “X”
xPrevNum=float(xPrevString) # convert to number – First X value

yStartPrev=templines[firstInsert].find(“Y”) # the position in the previous line where Y is
yPrevString=templines[firstInsert][yStartPrev+1:yStartPrev+8] # read the 7 characters after “Y”
yPrevNum=float(yPrevString) # convert to number – First Y value

#Now take one from the other
xMove=xHereNum-xPrevNum
yMove=yHereNum-yPrevNum

# Now split the values

E1 = EAmount-EAmountNeeded
E2 = EAmountNeeded

X2 = xHereNum
X1 = xPrevNum +(xMove/EAmount*E2)
Y2 = yHereNum
Y1 = yPrevNum +(yMove/EAmount*E2)

# Now convert the values back to strings

E1string=“{:.5f}”.format(E1)# 5 decimal places
#print (E1string)
E2string=“{:.5f}”.format(E2)
#print (E2string)
X1string=“{:.3f}”.format(X1)#3 decimal places
#print (X1string)
X2string=“{:.3f}”.format(X2)
#print (X2string)
Y1string=“{:.3f}”.format(Y1)
#print (Y1string)
Y2string=“{:.3f}”.format(Y2)
#print (Y2string)

#build strings to insert
insertString1=“G1 X”+X1string+” Y”+Y1string+” E”+E1string+”\n” # 1st (new) move prior to tool change
insertString2=currentTool # then the tool change
insertString3=“G1 X”+X2string+” Y”+Y2string+” E”+E2string+”\n” # then the second (new) move

#print (insertString1)
#print (insertString2)
#print (insertString3)

#determine the point where the lines need to be inserted
insertPoint=lastLine-j
print (“Appending lines to output file”)
#now open the end file and append this lot to it.
endfile = open(destinationFile,’a’)
for x in range (0,insertPoint):

endfile.write(templines[x])

endfile.write(insertString1)
endfile.write(insertString2)
endfile.write(insertString3)
for x in range (insertPoint+1,lastLine):

endfile.write(templines[x])

endfile.close()
print (“Tool change “,changesDone,“done”)
print(“”)
changesDone=changesDone+1 # so go back and do it all again for the next tool change

# End of changes Done loop

print(changesDone-1,“Tool changes moved”)
print(“”)
if notEnoughFlag==1: # skip the next bit if flag set
print(“WARNING there was insufficient filament extruded to fully purge between at least one of the tool changes”)
endfile.close

# So now all that remains is to append the rest of the source file to the output file

print (“Copying remaining lines to output file…”)
print(“”)
endfile = open(destinationFile,’a’)
for x in range (toolChangeLineNo,numLines):

lineText=(startfile.readline())#read line from input file
endfile.write(lineText)

startfile.close()
endfile.close()
print (“Done..”)

And that my friends is it!!

I don’t suppose anyone will want to copy and paste all the above code so I’ve uploaded the file to my Google drive.

Edit. 24th Jan 2017. I have removed the link to the Python script because there were a number of bugs – see later posts.

8 thoughts on “Multi colour printing without using wipe or prime towers

  1. Nice idea. Did you ever get to writing the post you referred to for how to make/prepare a file for multi-colored? It seems like a lot of work to make multiple STLs for one design, ie. one per color. I have a diamond (clone) hotend working now, but now need to see about how to get it actually multi-color…

    Like

    1. Hi Jon,

      The only way I know how to do it is by using multiple STL files as per this post https://somei3deas.wordpress.com/2017/01/09/making-a-multicolour-gcode-file/.
      AFAIK (but I could be wrong), all slicers need separate stl files so that you can assign a different tool to for each colour. I have downloaded one or two .amf files from thingiverse but when I load these into Slic3R, I see that the amf file contains a number of STL files so essentially the same thing.
      CAD design isn’t my strong point but I guess it depends on the design package that you use. I can only use OpenScad which doesn’t generate amf file natively. I guess some other design packages may have the ability to generate multiple STLs which are combined in a single amf file. At least, that’s how I assume the amf models that I’ve downloaded from thingiverse have been created.
      Ian

      Like

  2. Hi Ian,
    do you think this could be integrated into reprap firmware?
    Something you could set like retraction length?
    Only call it purge length?

    Like

  3. This did actually make it on to the Duet firmware wish list and David (DC42) thought that he could implement it. However, later testing that I did revealed other issue to do with holding “unused” filaments” at print temperature for extended periods of time which meant that some additional purging might still be needed. On that basis, the technique has less merit than my initial testing indicated so I told David to “hold fire”. The best one could hope for is to reduce the amount of purge needed or to implement this technique for certain designs but it doesn’t work for every print. The problem is that if you need to purge the tool anyway, then it needs to happen at the original tool change point, immediately before the new tool comes into play. So it isn’t easy to both move the tool change point forward in the gcode file and yet still do an additional purge at the original tool change position (because you’ve just moved it), nor is there much benefit. Basically, the technique works but only within certain parameters (time between switching filaments, type of filament etc) so it isn’t a universal fix. I still use it from time to time but only on certain prints. For other designs, I resort to different purge mechanisms.
    Ian

    Like

  4. I understand the problem of cooking up unused filament in the hotend. Is PETG as prone to this as PLA?
    For longer prints I always print one small block with all colors combined to prevent cooking.
    As long as you have enough infill and inner perimeter you can purge the actual color there.
    For bigger prints, the small block with all colors could probably be put into infill. But you would need another script to check that.
    Probably something coming in the next few years, considering there are more and more multimaterial printers outthere.
    If Reprap Firmware intergrated something like that I’d switch from repetier and by a duet for my main printer.

    Like

    1. Hi,

      I found PETG to be far more tolerant to being held at print temperature, The problem with PLA is that it starts to Hydrolyse (go more and more runny) over time. So when you come to print with it, the first few mm all comes out as a big runny blob. For sure, keeping it moving avoids most of the problems and I often use a mixing ratio that uses 2 or 5% of some filaments along with 90 to 95% if the main one. Depending on the colours involved, this small amount of mixing can be hardly noticeable.

      Like

Leave a comment