Blender-Pipeline/studio/story.py

808 lines
32 KiB
Python

# THIS FILE IS A PART OF VCStudio
# PYTHON 3
import os
import datetime
import json
from studio import checklist
#import checklist
def get_legacy(project_location):
# This function will read the .bos (Blender-Organizer Story) files. Primarily
# to convert them to .vcss (VCStudio Story) files.
# The concept is similar. But in order to work with them outside of reading
# and writting the file directly. I want to make a dictionary format with in
# the python. (Similarly to how I did with checklists and analytics)
# In the .bos file there was what I thought was clever at the time but
# ultimatly a dumb idea to make EVENTS and not scenes. The idea was that in
# a single event could be multiple scenes. But in reality it very rearly used
# and creating scenes with in events created a potential user error situation.
# So I want to get rid of all the EVENTS and read Scenes directly. Unless
# the event is empty of scenes. In which case to create a scene with the
# text of that event.
data = {
"fraction": 0.0, # Percentage of the Scenes finished.
"camera" : [0,0], # The position of where the user left
"selected": [], # List of selected items in the story editor
"active": None, # Active item.
"scenes" : {}, # List of scenes.
"arrows" : [], # List of connections. (A LIST. NOT DICT.)
"links" : [], # List of links to files or assets.
"markers": {}, # List of markers.
"events" : {} # List of frame like containers. (It will have similar)
} # function as events. But will have no text data with in
# it. It will be more like Blender's node editor's Frame.
# Even tho I want to change radically the idea of events. We still need to
# think in the events terms. Because we are parsing and old file. Funny how
# I will need to write this thing twice. For both file-types.
# I can't read it the way I read most other files. Because it's not line-
# based. But more something like HTML file. So it's going to be a little
# issue.
bos = open(project_location+"/pln/main.bos")
bos = bos.read()
cx, cy = 1, 1
if "</camera>" in bos:
camera = bos[bos.find("<camera>")+8:]
camera = camera[:camera.find("</camera>")]
camera = camera.split(",")
for num, val in enumerate(camera):
try:
camera[num] = float(val)
except:
camera[num] = 0.0
try:
cx = float(camera[2])
cy = float(camera[3])
except:
pass
print (cy, cx)
# Some stupid me made decision early in a story editor's life to use
# per-pixel X coordinates and per line Y coordinates. Which is something
# like 100 times the difference. Okay 50 ish is a default value for Y.
# I'm not going to do anything about it right now. Maximum a bit later.
camera = [camera[0], camera[1]] # I don't want scale to exist here.
data["camera"] = camera
# Events. They are very important. Now in the Blender-Organizer I used an
# HTML like format to mark shots and assets with in the text. But it's not
# a very efficient way of doing it. Because it requiered constant parsing
# of the text in real time. Which caused quite a noticable lag. And also
# every time I needed to read a part of it. I had to do parsing of the text.
# So I guess the VCSS file format will be designed to deal with this kind of
# thing. I'm actually more toward putting the text into separate files. Simple
# text documents. And making the VCSS a linking system.
if "</event>" in bos:
for event in bos.split("</event>")[:-1]:
event = event[event.find("<event>")+8:]
# Now we have text of the event. Let's parse out of it folowing
# stuff. We will need it if there are multiple scenes. Or when
# there are no scenes at all.
eventname = event[event.find('"')+1:event.replace('"'," ",1).find('"')]
# Let's parse the coordinates.
c = event[event.find('[')+1:event.find(']')]
c = c.split(",")
eventpositon = [float(c[0])*cx,float(c[2])*cy]
eventsize = [float(c[1])*cx,60.0]
# Now since we know the name of the event and the sizes. We can
# start parsing the scenes from the text with in the event.
eventtext = event[event.find(']')+2:-1]
# Now basically have to do the same exact thing with <scene> and
# later with <shot>, <item> to make all work.
# But first let's record a scene if it has no scene in it.
if not "<scene>" in eventtext:
if eventname in data["scenes"]:
eventname = eventname + "_copy"
data["scenes"][eventname] = {
"fraction":0.0, # Percentage
"position":eventpositon,
"size":eventsize,
"parent":"", # For when it's in a Frame (Event)
"shots":[[
"text_block",[["text", eventtext]]
]]
}
else:
# If there are more then 1 scene per event. We want to create
# an event, frame thing for them.
aos = eventtext.count("<scene>")
parent = "" #This will be it's name
if aos > 1:
parent = eventname
data["events"][eventname] = {
"position":eventpositon,
"size":[0,0]
}
# Now let's continue parsing the scenes.
for num, scene in enumerate(eventtext.split("</scene>")[:-1]):
scenename = scene[scene.find('"')+1:scene.replace('"'," ",1).find('"')]
scenename = scenename.replace(" ", "_")
scenetext = scene[scene.replace('"', " ", 1).find('"')+1:-1]
scenesize = [eventsize[0] / aos , eventsize[1]]
sceneposition = [eventpositon[0] + scenesize[0]*num,
eventpositon[1]]
data["scenes"][scenename] = {
"fraction":0.0, # Percentage
"position":sceneposition,
"size":scenesize,
"parent":parent, # For when it's in a Frame (Event)
"shots":[[
"text_block",[["text", scenetext]]
]]
}
# Parsing the text inside the scenes... OMG. Finding the SHOTS.
for scenename in data["scenes"]:
scenetext = data["scenes"][scenename]["shots"][0][1][0][1]
# Okay so we have both scene name and scene text. So it's
# time so send this data into the main data thingy, right?
# Wrong. Now we need to parse pointers to the shots, assets
# and other stuff. This file format gonna take a while.
# If you look into the history of this file on NotABug there
# Will be a version with a novel-long article here in the
# comments. I was toying with ideas of how to make stuff
# work. I will not change the whole system a bit from what
# I though there. (Using text pointers) and make it more like
# list of shot_block or text_block. Which both are just text.
# But one has a bit more metadata then the other. And inside
# which you can specify links, frases, images and simple text...
# So first of all we need to break the text up into shots.
shots = []
sa = scenetext.count("<shot>") # Shots amount
ts = scenetext # Copy of the scene text to butcher
for s in range(sa):
# If the first part of the scene is not a shot.
if not ts.startswith("<shot>"):
shots.append([
"text_block", [["text",ts[:ts.find("<shot>")]]]
])
# Now we need to erase the part from the ts.
ts = ts[ts.find("<shot>")+6:]
# Now we can parse the shot itself. We need the name.
if ts.count('"') > 1:
shotname = ts[ts.find('"')+1:ts.replace('"', " ", 1).find('"')]
ts = ts[ts.replace('"', " ", 1).find('"')+1:]
else:
shotname = "Unnamed"
# Put it also into the list.
shots.append([
"shot_block", shotname, [["text",ts[:ts.find("</shot>")]]]
])
# And erase it.
ts = ts[ts.find("</shot>")+7:]
shots.append([
"text_block", [["text",ts]]
])
# Now I want to get a fraction from a scene.
shotsfractions = []
for shot in shots:
if shot[0] == "shot_block":
# Let's see if it has a checklist.
if os.path.exists(project_location\
+"/rnd/"+scenename+"/"+shot[1]+"/shot.progress"):
check = checklist.get_list(project_location\
+"/rnd/"+scenename+"/"+shot[1]+"/shot.progress")
shotsfractions.append(check["fraction"])
else:
folder = project_location\
+"/rnd/"+scenename+"/"+shot[1]
try:
if len(os.listdir(folder+"/rendered")) > 0:
shotsfractions.append(1.0)
elif len(os.listdir(folder+"/test_rnd")) > 0:
shotsfractions.append(0.8)
elif len(os.listdir(folder+"/opengl")) > 0:
shotsfractions.append(0.6)
elif len(os.listdir(folder+"/storyboard")) > 0:
shotsfractions.append(0.4)
elif len(os.listdir(folder+"/extra")) > 0:
shotsfractions.append(0.2)
except:
shotsfractions.append(0.0)
# Writting parsed verions of the scenes. With all the shots
data["scenes"][scenename]["shots"] = shots
# Now that we have SHOTS Fractions we need a scene fraction.
try:
data["scenes"][scenename]["fraction"] = \
sum(shotsfractions) / len(shotsfractions)
except:
data["scenes"][scenename]["fraction"] = 0.0
# Now since we have shots and we have their factors. I'd like to do
# parsing of the text parts inside shots and texts between shots to
# find all the images, links and phrases.
for scenename in data["scenes"]:
for shotnum, shot in enumerate(data["scenes"][scenename]["shots"]):
# Our shot data could be either text_block ot shot_block
# and they are formatted a little bit differently.
if shot[0] == "shot_block":
text = shot[2][0][1]
else:
text = shot[1][0][1]
# Now keep in mind that similar stuff has to be done again
# when we saving the parsed text back
textblock = []
# This requires some extreme cleverness. Since I already hate
# this file format. WHY IS IT SO COMPLICATED?!
part = ""
skip = 0
for num, letter in enumerate(text):
if num >= skip:
if part.endswith("<item>"):
# putting the normal text in
if part[skip:].replace("<item>", ""):
textblock.append([
"text", part[skip:].replace("<item>", "")
])
# parsing the item
itempart = text[num:text[num:].find("</item>")+num]
link = itempart[itempart.find('"')+1:
itempart.replace('"', " ", 1).find('"')]
if link.startswith("/dev"):
link = link[4:]
itemtext = itempart[itempart.replace('"', " ", 1).find('"')+1:]
# puttin the link into the list
textblock.append([
"link", link, itemtext
])
# skiping to after the item
skip = text[num:].find("</item>")+6
part = ""
# Images. With the <>
elif part.endswith("<image>"):
# putting the normal text in
if part[skip:].replace("<image>", ""):
textblock.append([
"text", part[skip:].replace("<image>", "")
])
# parsing the item
link = text[num:text[num:].find("</image>")+num]
textblock.append([
"image", link
])
# skiping to after the item
skip = text[num:].find("</image>")+7
part = ""
# Images. With the [] (a little older version but still
# should be supported by default)
elif part.endswith("[image]"):
# putting the normal text in
if part[skip:].replace("[image]", ""):
textblock.append([
"text", part[skip:].replace("[image]", "")
])
# parsing the item
link = text[num:text[num:].find("[/image]")+num]
textblock.append([
"image", link
])
# skiping to after the item
skip = text[num:].find("[/image]")+7
part = ""
# Now let's figure out the frases. Because it has a
# text based thing in the beginning. Or maybe it's going
# to be anything that was before it? Hmm...
elif part.endswith(" - ["):
# putting the normal text in
if part[skip:].replace(" - [", ""):
textblock.append([
"text", part[skip:].replace(" - [", "")
])
# I designed the - [ thing to type conversation
# easier. Example:
# John - [Hello, World.]
# This would be John saying the words "Hello, World."
# but with the inclusion of <item> it became quite a
# problem. Because I would like John to be a link.
# It ended up looking something like:
#<itme>"/dev/chr/John"John</item> - [Hello, World.]
# This is a bit of a problem. Because any link that
# marked as <item> is already in a textblock list.
character = []
# If it's a link
if textblock[-1][0] == "link":
character = textblock[-1]
del textblock[-1]
# If it's just a text
elif textblock[-1][0] == "text":
character = textblock[-1][1]
character = character[character.rfind("\n")+1:]
if character:
textblock[-1][1] = textblock[-1][1].replace(character, "")
character = ["text", character]
# Now let's get the frase
frase = text[num:text[num:].find("]")+num]
# If any character. Put it into a textblock
if character:
textblock.append([
"frase", character, frase
])
# skiping to after the item
skip = text[num:].find("]")
part = ""
else:
part = part + letter
# LAST PART (WHY DIDN'T I THINK ABOUT ERLIER OMG)
textblock.append([
"text", part[skip:]
])
if shot[0] == "shot_block":
data["scenes"][scenename]["shots"][shotnum][2] = textblock
else:
data["scenes"][scenename]["shots"][shotnum][1] = textblock
# Okay this was event. LOL. This is a large function to parse them all.
# Crazy complex bastards. Anyway. It's over. And we have to only do the
# easy rest of it. Untill we will get to calculating the project. Because
# we need to filter out all scenes that are not in the main chain.
# Let's start with hard thing first. So to feel good when having to do the
# easy stuff in the end. ARROWS. (connections between scenes)
# To make it work. We need to fisrt of all earase all the stuff before
# the first arrow. Because <arrow> could be mentioned in the script it
# self. Similarly <image> is too. So it's good to complitelly clear bos
# out of anything that came before.
bos = bos[bos.rfind("</event>"):]
# From here everything is PER LINE BASED... YEAH!!!!
bos = bos.split("\n")
for line in bos:
# This is arrows...
if line.startswith("<arrow>"):
arrow = line[line.find("<")+7:line.rfind("</")].split(" --> ")
newarrow = []
for i in arrow:
i = i.split(",")
if i[0] != "-1":
newarrow.append([
"scene", i[1][1:-1]
])
else:
newarrow.append(
i[1][1:-1]
)
data["arrows"].append(newarrow)
# And this is links. Formerly known as Images. Initially the idea was to
# just store images. But later. I started adding assets like this as well.
elif line.startswith("<image>"):
stuff = line.split(",")
link = stuff[3][1:-1]
coordinates = []
try:
coordinates.append(float(stuff[0].replace("<image>", ""))*cx)
except:
coordinates.append(0.0)
try:
coordinates.append(float(stuff[1])*cy)
except:
coordinates.append(0.0)
# Lately the primary reason to use <image> was linking assets directly
# into story editor's space. But it was a hack. Basically I was
# linking a preview image. Preview.png or Preview.jpg from the
# renders of the asset. And the drawer of the images in the story-
# editor already knew to redirect the clicks to a different function.
# But for VCStudio I want to link to asset or a file.
linktype = "file"
if "/renders/Preview." in link:
linktype = "asset"
link = link[:link.rfind("/renders/Preview.")].replace("/dev", "")
data["links"].append([
linktype, link, coordinates, ""
])
# Markers. I nearly forgot about the markers. In the Story-Editor of
# Blender-Orgaznier they were something like markers in VSE in Blender.
# Vertical lines visible on every altitude.
elif line.startswith("<marker>"):
marker = line.replace("<marker>", "").replace("</marker>", "").split(",")
try:
markloc = float(marker[0])
except:
markloc = 0.0
markstring = marker[1].replace('"', "")
# I do want to do something quite interesting with markers in the
# VCStudio tho. I want them to be like little nodes with text while
# in the frame. And be like directional guides. Sticking to the
# edges of the screen when outside the screen. Something like items
# that are outside of the map in the videogames. So you can see
# which direction are they. For this markers will need to have
# both X and Y coordinates.
data["markers"][markstring] = [markloc, 0.0, ""]
# For now the Y is 0. Because we are reading from the old version.
# Okay we've got the Arrows data. Pretty much all the data. Now we just
# need to calculate the scenes fraction. For that we need to recreate the
# train of scenes. From START till END.
fractions = []
lastarrow = 'start'
for i in data["arrows"]:
if lastarrow == "end":
break
for arrow in data["arrows"]:
if arrow[0] == lastarrow:
lastarrow = arrow[1]
if arrow[1] != "end":
fractions.append(
data["scenes"][arrow[1][1]]["fraction"]
)
else:
break
# FINAL STUFF...
try:
data["fraction"] = sum(fractions) / len(fractions)
except:
data["fraction"] = 0.0
#save(project_location, data)
#data = load(project_location)
return data
def get_asset_data(win, name, force=False):
# This function will return a data of about the asset.
if name not in win.assets or force:
data = {
"fraction":0.0
}
try:
# Now let's get a fraction of the asset
if os.path.exists(win.project+"/ast/"+name+".blend"):
data["fraction"] = 1.0
else:
check = checklist.get_list(win.project+"/dev/"+name+"/asset.progress")
data["fraction"] = check["fraction"]
except:
pass
win.assets[name] = data
return win.assets[name]
def save(project, story):
# This is a save of the new VCStudio. Which is using a standard file format.
# so people could parse the data themselves. ( Or because I'm lazy )
try:
os.mkdir(project+'/pln/')
except:
pass
with open(project+'/pln/story.vcss', 'w') as fp:
json.dump(story, fp, sort_keys=True, indent=4)
def load(project):
# This is a convertion back from JSON data to out beloved python dict
try:
with open(project+'/pln/story.vcss') as json_file:
data = json.load(json_file)
except:
# If there is no file.
data = {
"fraction": 0.0, # Percentage of the Scenes finished.
"camera" : [0,0], # The position of where the user left
"selected": [], # List of selected items in the story editor
"active": None, # Active item.
"scenes" : {}, # List of scenes.
"arrows" : [], # List of connections. (A LIST. NOT DICT.)
"links" : [], # List of links to files or assets.
"markers": {}, # List of markers.
"events" : {} # List of frame like containers. (It will have similar)
} # function as events. But will have no text data with in
# it. It will be more like Blender's node editor's Frame.
# I forgot to update the scenes analytics lol.
for scenename in data["scenes"]:
shotsfractions = []
for shot in data["scenes"][scenename]["shots"]:
if shot[0] == "shot_block":
# Let's see if it has a checklist.
if os.path.exists(project\
+"/rnd/"+scenename+"/"+shot[1]+"/shot.progress"):
check = checklist.get_list(project\
+"/rnd/"+scenename+"/"+shot[1]+"/shot.progress")
shotsfractions.append(check["fraction"])
else:
folder = project\
+"/rnd/"+scenename+"/"+shot[1]
try:
if len(os.listdir(folder+"/rendered")) > 0:
shotsfractions.append(1.0)
elif len(os.listdir(folder+"/test_rnd")) > 0:
shotsfractions.append(0.8)
elif len(os.listdir(folder+"/opengl")) > 0:
shotsfractions.append(0.6)
elif len(os.listdir(folder+"/storyboard")) > 0:
shotsfractions.append(0.4)
elif len(os.listdir(folder+"/extra")) > 0:
shotsfractions.append(0.2)
except:
shotsfractions.append(0.0)
try:
data["scenes"][scenename]["fraction"] = \
sum(shotsfractions) / len(shotsfractions)
except:
data["scenes"][scenename]["fraction"] = 0.0
fractions = []
lastarrow = 'start'
for i in data["arrows"]:
if lastarrow == "end":
break
for arrow in data["arrows"]:
if arrow[0] == lastarrow:
lastarrow = arrow[1]
if arrow[1] != "end":
fractions.append(
data["scenes"][arrow[1][1]]["fraction"]
)
else:
break
# FINAL STUFF...
try:
data["fraction"] = sum(fractions) / len(fractions)
except:
data["fraction"] = 0.0
return data
def undo_record(win):
# This function will record undo of the story. The undo will be basically
# copies of the script JSON file as a whole. The /pln/story.vcss
# This function will not read the files. It's not nessesary. But it will
# make a copy of the whole thing. So I guess we need to implement some kind
# of limiter.
limit = int(win.settings["Undo_Limit"])
# Now let's advance the undo history 1 forward.
win.undo_index = min(len(win.undo_history)+1, limit)
# Let's read our file
f = open(win.project+'/pln/story.vcss')
win.undo_history.append(f.read())
# And now let's limit our selves
win.undo_history = win.undo_history[0-limit:]
# Following 2 functions are a big buggy at the moment. I will aprecieate if
# someone going to look at them. Maybe it's to do with studio/studio_storyLayer.py
# where I was testing them. But they work. Just not every time.
def undo(win):
# This function will do the undo. Tho this function will not nessesarily
# sens the Ctrl - Z by it self. It should be activated from any place
# in the UI.
# Let's move our index back 1 way.
win.undo_index = max(0, win.undo_index-1)
# Now let's read our data
d = win.undo_history[win.undo_index]
# And let's save the data back to the file
f = open(win.project+'/pln/story.vcss', "w")
f.write(d)
f.close()
# And let's refresh story
win.story = load(win.project)
def redo(win):
# This function will do the redo. Tho this function will not nessesarily
# sens the Ctrl - Y by it self. It should be activated from any place
# in the UI.
# Let's move our index back 1 way.
win.undo_index = min(len(win.undo_history)-1, win.undo_index+1)
# Now let's read our data
d = win.undo_history[win.undo_index]
# And let's save the data back to the file
f = open(win.project+'/pln/story.vcss', "w")
f.write(d)
f.close()
# And let's refresh story
win.story = load(win.project)