PDA

Voir la version complète : PointeuseY



xs_yann
01/10/2014, 16h34
Salut à tous,

Je vous partage un petit exercice Python que j’ai fait, qui consiste à recoder La PointeuseX™ de Jean-Laurent en Python.

http://frenchcinema4d.fr/showthread.php?57408-Compteur-temps-pass%E9

Le principe de Jean-Laurent était de comparer à chaque exécution la minute courante avec celle de l’exécution précédente et en cas de différence (une ou plusieurs minutes écoulées), ajouter cette différence au compteur.

Le principe que j’ai utilisé est de calculer le temps écoulé entre chaque exécution (aussi court soit-il) et de l’ajouter au compteur.

http://www.xsyann.com/fc4d/pointeusey_gui.png

Valeur : La valeur courante du compteur (il est possible de la modifier manuellement).
Inactivité : Temps d’inactivité à partir duquel le compteur n’enregistre plus.
Limite de temps : Permet d’arrêter le compteur lorsque ce temps est atteint.
Compte à rebours : Si une limite de temps est définie, le compteur se décrémente au lieu de s’incrémenter.
Heures:Minutes : 2:30
Heures:Minutes:Secondes : 2:30:02
Total Minutes : 150
Total Heures : 2.5

Le code généré par le script se trouve dans un noeud Python d’un tag Xpresso, ce qui permet de récupérer facilement la sortie pour la connecter avec ce que l’on veut. Par défaut la sortie est connectée avec le nom de l’objet neutre pour voir le temps dans le gestionnaire d’objet ou pour l’ajouter à l’HUD.

http://www.xsyann.com/fc4d/pointeusey_hud.png

Exemple avec une spline Texte :

http://www.xsyann.com/fc4d/pointeusey_xpresso.png

http://www.xsyann.com/fc4d/pointeusey_extrude.png

La petite particularité est que tout est généré à partir d’un script, ce qui permet de ne pas dépendre d’un fichier c4d (comme la plupart des presets Xpresso). Le script créé l’objet neutre avec ses données utilisateur puis le tag Xpresso et le noeud Python avec son code dedans.

Voici le code :
import c4d

ID_OPERATOR_PYTHON = 1022471

_display_modes = ["DISPLAY_H", "DISPLAY_M", "DISPLAY_HM", "DISPLAY_HMS"]

NODE_CODE = """import c4d
from datetime import datetime, timedelta
import re

CONTAINER_ID = 1033688
ELAPSED_SEC = 100
ELAPSED_MICRO = 101

__ENUM_IDS__

_languages = ['fr', 'us']

_strings = {
# User Data
UD_HIDE: {'us':"Hide", 'fr':"Masquer"},
UD_VALUE: {'us':"Value", 'fr':"Valeur"},
UD_RESET: {'us':"Reset", 'fr':"Remise à zéro"},
UD_IDLE: {'us':"Idle", 'fr':"Inactivité"},
UD_TIME_LIMIT: {'us':"Time limit", 'fr':"Limite de temps"},
UD_COUNTDOWN: {'us':"Countdown", 'fr':"Compte à rebours"},
UD_DISPLAY: {'us':"Display", 'fr':"Affichage"},

# Cycles
(UD_DISPLAY, DISPLAY_H): {'us':"Total Hours", 'fr': "Total Heures"},
(UD_DISPLAY, DISPLAY_M): {'us':"Total Minutes", 'fr': "Total Minutes"},
(UD_DISPLAY, DISPLAY_HM): {'us':"Hours:Minutes", 'fr': "Heures:Minutes"},
(UD_DISPLAY, DISPLAY_HMS): {'us':"Hours:Minutes:Seconds", 'fr': "Heures:Minutes:Secondes"},

# Strings
'Show': {'us':"Show", 'fr': "Afficher"},
'Hide': {'us':"Hide", 'fr':"Masquer"},
'Finished': {'us':"Finished", 'fr':"Terminé"},
}

def get_default_language():
i = 0
while True:
lang = c4d.GeGetLanguage(i)
if lang is None:
break
if lang["default_language"]:
lang = lang["extensions"].lower()
return lang if lang in _languages else 'us'
i += 1

def tr(s):
lang = get_default_language().lower()
if s in _strings:
return _strings[s][lang]
return s

def translate_ud(obj, lang):
for desc, bc in obj.GetUserDataContainer():
if not desc[1].id in _strings:
continue
name = _strings[desc[1].id][lang]
bc[c4d.DESC_NAME] = name
bc[c4d.DESC_SHORT_NAME] = name
bc_cycle = bc[c4d.DESC_CYCLE]
if bc_cycle:
for k, v in bc_cycle:
bc_cycle[k] = _strings[(desc[1].id, k)][lang]
bc.SetContainer(c4d.DESC_CYCLE, bc_cycle)
obj.SetUserDataContainer(desc, bc)


_date = None
_total = None
_lang = None

class Duration(timedelta):

@staticmethod
def parse(s=""):
\"""Create timedelta from string.
\"""
regex = r'((?P<hours>\d+):)?((?P<minutes>\d+):?)(?P<seconds>\d+)?'
match_object = re.match(regex, str(s).replace(" ", ""))
if match_object is None:
return Duration()
args = dict((k, int(v)) for k, v in match_object.groupdict(default=0).items())
return Duration(**args)

def __add__(self, x):
y = super(Duration, self).__add__(x)
return Duration(days=y.days, seconds=y.seconds, microseconds=y.microseconds)

def __sub__(self, x):
y = super(Duration, self).__sub__(x)
return Duration(days=y.days, seconds=y.seconds, microseconds=y.microseconds)

def __str__(self):
return self.fmt("{th} : {m} : {s}")

def total_seconds(self, cast=int):
\"""Total seconds from timedelta.
\"""
return (self.microseconds + (self.seconds + self.days * 24 * 3600) * 10 ** 6) / cast(10 ** 6)

def fmt(self, fmt):
\"""String format.
\"""
seconds = self.total_seconds()
d = {'d': self.days}
d['h'], rem = divmod(self.seconds, 3600)
d['m'], d['s'] = divmod(rem, 60)
d['m'] = "{0:02d}".format(d['m'])
d['s'] = "{0:02d}".format(d['s'])
d['th'], d['tm'], d['ts'] = seconds / 3600, seconds / 60, seconds
d['thf'] = "{0:.1f}".format(seconds / 3600.0)
return fmt.format(**d)

def reset(default=Duration()):
global _date
global _total

_total = default
_date = datetime.now()
return default

def get_container(obj, container_id):
bc = obj[container_id]
if not bc:
bc = c4d.BaseContainer()
obj[container_id] = bc
return bc

def init(doc, controller, value):
\"""Init previous date and total elapsed time.
\"""
global _total
global _lang

if _lang is None:
container = get_container(doc, CONTAINER_ID)
_lang = get_default_language().lower()
translate_ud(controller, _lang)

if _total is None:
reset(value)
container = get_container(doc, CONTAINER_ID)
sec, micro = container[ELAPSED_SEC], container[ELAPSED_MICRO]
if sec is not None and micro is not None:
_total = Duration(seconds=sec, microseconds=micro)
elif str(_total) != str(value):
reset(value)

def get_delta():
\"""Return elapsed time from last execution.
\"""
global _date

now = datetime.now()
delta = now - _date
_date = now
return delta

def update(delta, idle, doc, controller):
\"""Update total and ouput.
\"""
global _total

if delta < idle or idle.total_seconds() == 0:
_total += delta

set_ud(controller, UD_VALUE, str(_total))
container = get_container(doc, CONTAINER_ID)
container[ELAPSED_SEC] = _total.total_seconds()
container[ELAPSED_MICRO] = _total.microseconds
doc.GetDataInstance().SetContainer(CONTAINER_ID, container)

def format_time(time_limit, display, countdown):
\"""Format output string.
\"""
global _total

formatted = ""
if _total >= time_limit and time_limit.total_seconds() != 0:
formatted = tr("Finished")
else:
total = _total
if countdown and time_limit.total_seconds() != 0:
total = time_limit - total
fmts = {DISPLAY_M: "{tm}", DISPLAY_H: "{thf}", DISPLAY_HM: "{th}:{m}", DISPLAY_HMS:"{th}:{m}:{s}"}
formatted = total.fmt(fmts[display])
return formatted

def get_ud(op, id_ud):
return op[c4d.ID_USERDATA, id_ud]

def set_ud(op, id_ud, value):
op[c4d.ID_USERDATA, id_ud] = value

def create_layer(obj, name):
layer = c4d.documents.LayerObject()
layer.SetName(name)
layer_root = doc.GetLayerObjectRoot()
layer.InsertUnder(layer_root)
obj.SetLayerObject(layer)
return layer

def hide(doc, obj):
layer = obj.GetLayerObject(doc)
if layer is None:
layer = create_layer(obj, "PointeuseY")
data = layer.GetLayerData(doc)
# Show / Hide Layer
data['manager'] = not data['manager']
layer.SetLayerData(doc, data)

# Switch button name
ud_bc = obj.GetUserDataContainer()
hide_bc, hide_desc = dict((desc[1].id, (bc, desc)) for desc, bc in ud_bc)[UD_HIDE]
names = [tr("Show"), tr("Hide")]
hide_bc[c4d.DESC_NAME] = names[data['manager']]
hide_bc[c4d.DESC_SHORT_NAME] = names[data['manager']]
obj.SetUserDataContainer(hide_desc, hide_bc)

def main():

global __OUTPUT__ # Output Node

controller = op.GetNodeMaster().GetOwner().GetObject()

value = Duration.parse(get_ud(controller, UD_VALUE))
idle = Duration.parse(get_ud(controller, UD_IDLE))
time_limit = Duration.parse(get_ud(controller, UD_TIME_LIMIT))
set_ud(controller, UD_IDLE, str(idle))
set_ud(controller, UD_TIME_LIMIT, str(time_limit))

countdown = get_ud(controller, UD_COUNTDOWN)
display = get_ud(controller, UD_DISPLAY)

if get_ud(controller, UD_RESET):
value = reset()
set_ud(controller, UD_RESET, False)

if get_ud(controller, UD_HIDE):
hide(doc, controller)
set_ud(controller, UD_HIDE, False)

init(doc, controller, value)
delta = get_delta()
update(delta, idle, doc, controller)
__OUTPUT__ = format_time(time_limit, display, countdown)
"""

def create_UD_container(obj, dtype, name, parent):
bc = c4d.GetCustomDataTypeDefault(dtype)
bc[c4d.DESC_NAME] = name
bc[c4d.DESC_SHORT_NAME] = name
bc[c4d.DESC_PARENTGROUP] = parent
return bc

def create_UD_default(obj, bc, default):
bc[c4d.DESC_DEFAULT] = default
ud = obj.AddUserData(bc)
obj[ud] = default
return ud

def create_UD_group(obj, name, parent=None):
bc = create_UD_container(obj, c4d.DTYPE_GROUP, name, parent)
return obj.AddUserData(bc)

def create_UD_string(obj, name, parent=None, default=""):
bc = create_UD_container(obj, c4d.DTYPE_STRING, name, parent)
return create_UD_default(obj, bc, default)

def create_UD_bool(obj, name, parent=None, default=False, gui=c4d.CUSTOMGUI_BOOL):
bc = create_UD_container(obj, c4d.DTYPE_BOOL, name, parent)
bc[c4d.DESC_CUSTOMGUI] = gui
return create_UD_default(obj, bc, default)

def create_UD_cycle(obj, name, parent=None, default=0, populate={}):
bc = create_UD_container(obj, c4d.DTYPE_LONG, name, parent)
bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_CYCLE
items = c4d.BaseContainer()
for i, item in populate.iteritems():
items.SetString(i, item)
bc[c4d.DESC_CYCLE] = items
return create_UD_default(obj, bc, default)

def create_userdata(null):
group = create_UD_group(null, "PointeuseY", c4d.DescID(0))
ud_ids = {}
ud_ids["UD_VALUE"] = create_UD_string(null, "", group, "0:00:00")[1].id
ud_ids["UD_RESET"] = create_UD_bool(null, "", group, gui=c4d.CUSTOMGUI_BUTTON)[1].id
ud_ids["UD_IDLE"] = create_UD_string(null, "", group, "0:05:00")[1].id
ud_ids["UD_TIME_LIMIT"] = create_UD_string(null, "", group, "3:00:00")[1].id
ud_ids["UD_COUNTDOWN"] = create_UD_bool(null, "", group)[1].id
items = dict((i, "dummy") for i, mode in enumerate(_display_modes))
ud_ids["UD_DISPLAY"] = create_UD_cycle(null, "", group, populate=items, default=3)[1].id
ud_ids["UD_HIDE"] = create_UD_bool(null, "", group, gui=c4d.CUSTOMGUI_BUTTON)[1].id
return ud_ids

def create_xpresso(null, ud_ids):
xpresso = c4d.BaseTag(c4d.Texpresso)
null.InsertTag(xpresso)
master = xpresso.GetNodeMaster()
python_node = master.CreateNode(master.GetRoot(), ID_OPERATOR_PYTHON, x=10, y=100)
formatted_port = python_node.AddPort(c4d.GV_PORT_OUTPUT, c4d.OUT_STRING, message=True)
python_node[c4d.GV_PYTHON_CODE] = generate_code(ud_ids, formatted_port.GetName(python_node))

null_node = master.CreateNode(master.GetRoot(), c4d.ID_OPERATOR_OBJECT, x=300, y=100)
null_node[c4d.GV_OBJECT_OBJECT_ID] = null
name_port = null_node.AddPort(c4d.GV_PORT_INPUT, c4d.ID_BASELIST_NAME)
name_port.Connect(formatted_port)
python_node.RemoveUnusedPorts()

def generate_code(ud_ids, output_name):
enum_ids = ""
for i, mode in enumerate(_display_modes):
enum_ids += "{0} = {1}\n".format(mode, i)
for k, v in ud_ids.iteritems():
enum_ids += "{0} = {1}\n".format(k, v)
return NODE_CODE.replace("__ENUM_IDS__", enum_ids).replace("__OUTPUT__", output_name)

def create_object(doc, object_type):
obj = c4d.BaseObject(object_type)
doc.InsertObject(obj)
doc.AddUndo(c4d.UNDO_NEW, obj)
doc.SetActiveObject(obj)
return obj

def main():
null = create_object(doc, c4d.Onull)
ud_ids = create_userdata(null)
create_xpresso(null, ud_ids)

c4d.EventAdd(c4d.EVENT_0)

if __name__=='__main__':
main()


http://xsyann.com/fc4d/PointeuseY.py


edit : J'avais oublié que Val l'avait déjà fais aussi, si vous voulez un chrono en mode plugin avec l'utilisation d'un vrai timer c'est ici : http://frenchcinema4d.fr/showthread.php?74574-Py-Timer

oli_d
01/10/2014, 20h35
Merci pour le partage, ton code est une vraie leçon de programmation.

Par contre sur la version française de c4d il plante, car la traduction de string est Chaîne, donc le code du noeud python plante sur la définition de ton port de sortie à cause de l'accent circonflexe ! (je sais pas s'il y a quelqu'un qui m'a compris là !)

Message d'erreur :

File "'Python'", line 126
global Chaîne # Output Node
^
SyntaxError: invalid syntax



Je continue à décortiquer, car j'apprends plein de trucs !

xs_yann
01/10/2014, 20h46
Ah mince, merci du retour oli_d.
Pourtant j'ai pensé à ce cas et j'ai injecté dynamiquement dans le code le nom du noeud que je récupère dans le script...
Malheureusement, le bug que j'ai posté dans le thread de César empêche de corriger ça.
Peut-être que je peux récupérer le nom du port automatiquement dans le noeud python, à voir.

edit :
Après avoir essayé de modifier manuellement le dictionnaire des globales, d'évaluer le nom du port, d'inspecter les membres de la classe port, d'utiliser un type Filename plutôt que String (ça fonctionne mais en français "Nom de Fichier" ça passe pas non plus en nom de variable), j'ai trouvé une solution qui consiste à appeler la fonction GvNode.AddPort avec message=True, je ne sais pas pourquoi mais le port s'appelle Output2 au lieu de String (ou Chaîne).


output_port = python_node.AddPort(c4d.GV_PORT_OUTPUT, c4d.OUT_STRING, message=True)

J'ai essayé aussi de changer le type du port Output1 de base (qui est en Real par défaut) avec GvNode.SetPortType, ça fonctionne mais que si la fonction AddPort est appelée avant avec le même id que SetPortType(), ce qui revient à créer un noeud pour rien qu'il faut supprimer après... :sweatdrop:


python_node.AddPort(c4d.GV_PORT_OUTPUT, c4d.OUT_STRING)
output_port = python_node.GetOutPort(0)
python_node.SetPortType(output_port, c4d.OUT_STRING)

valkaari
01/10/2014, 23h27
c'était presque trop facile quoi

miroof
01/10/2014, 23h38
Oh c'est nickel ça, merci pour les liens

xs_yann
02/10/2014, 15h37
J'ai ajouté un undo, un bouton afficher / masquer et une traduction français / anglais.

http://www.xsyann.com/fc4d/pointeusey_gui1.png

Le bouton 'Masquer' créé un calque et le masque dans le gestionnaire d'objet. L'idéal est de créer une entrée dans l'HUD en faisant un cliquer-glisser du nom de l'objet neutre sur la vue. Cela permet, en plus d'afficher le temps, de pouvoir retrouver facilement l'objet masqué en cliquant dessus.

Le bout de code qui change dynamiquement le nom d'un bouton en DU :


state = 0
ud_bc = obj.GetUserDataContainer()
hide_bc, hide_desc = dict((desc[1].id, (bc, desc)) for desc, bc in ud_bc)[BUTTON_ID]
names = [tr("Show"), tr("Hide")]
hide_bc[c4d.DESC_NAME] = names[state]
hide_bc[c4d.DESC_SHORT_NAME] = names[state]
obj.SetUserDataContainer(hide_desc, hide_bc)

La traduction est gérée par une fonction qui récupère le langage courant et qui traduit les chaîne de caractères en fonction de ce langage. De cette façon les noms des DU générées et le nom dynamique du bouton sont traduits.

edit : J'ai modifié le code pour que le preset traduise automatiquement son interface (les données utilisateur) à chaque ouverture.