diff --git a/__init__.py b/__init__.py index d35e4e0..7752746 100644 --- a/__init__.py +++ b/__init__.py @@ -5,137 +5,9 @@ import time import sys #from . import ag -#from . import gl -#from . import et -#from . import scn -#from . import ui -#from . import cmd -#from . import com -#from . import loop from . import core +from . import aux +#from . import m +#from . import serial -class ProgressBar: - def __init__(self, width, total=1.0, unit="", prefix=None, suffix=None, show_percent=True, show_absolute=True, show_rate=True, show_etc=True, draw_on_update=True): - self.width = width - self.total = total - self.unit = unit - self.prefix = prefix - self.suffix = suffix - self.show_percent = show_percent - self.show_absolute = show_absolute - self.show_rate = show_rate - self.show_etc = show_etc - self.draw_on_update = draw_on_update - - self.percentage_printf = "%5.1f %%" - self.absolute_printf_total = "%d/%d %s," - self.absolute_printf = "%d %s," - self.rate_printf_total = "%d %s/s," - self.rate_printf = "%d %s/s" - self.etc_printf = "ETA %d s" - - self._current = 0.0 - self._ratio = 0.0 - self._rate = 0.0 - self._runner = 0.0 - - self._start = None - - self._longest_line = 0 - - @property - def current(self): - return self._current - - @current.setter - def current(self, value): - self._current = value - - self.update() - - if self.draw_on_update: - self.draw() - - def update(self): - if self._start is None: - self._start = time.time() - - dt = (time.time() - self._start) - - if dt > 0: - self._rate = self._current / dt - else: - self._rate = 0 - - if self.total is None: - self._ratio = None - self._etc = None - self._runner = (self._runner + 0.01) % 1.0 - - else: - self._ratio = self._current / self.total - self._etc = dt * (1 / self._ratio - 1) - - def draw(self, overdraw=0): - output = [] - - if self.prefix: - output.append(self.prefix) - - if self.show_percent and self.total is not None: - output.append(self.percentage_printf %(self._ratio * 100)) - - if self.total is None: - count_full = int(self._runner * (self.width - 5)) - count_empty = self.width - count_full - 5 - - output.append("[%s<=>%s]" %(" " * count_full, " " * count_empty)) - else: - count_full = int(self._ratio * (self.width - 3)) - count_empty = self.width - count_full - 3 - - output.append("[%s>%s]" %("=" * count_full, " " * count_empty)) - - if self.show_absolute: - if self.total is None: - output.append(self.absolute_printf %(self._current, self.unit)) # TODO et.Unit - else: - output.append(self.absolute_printf_total %(self._current, self.total, self.unit)) - - if self.show_rate: - if self.total is None: - output.append(self.rate_printf %(self._rate, self.unit)) - else: - output.append(self.rate_printf_total %(self._rate, self.unit)) - - if self.show_etc and self.total is not None: - output.append(self.etc_printf %(self._etc)) - - if self.suffix: - output.append(self.suffix) - - line = " ".join(output) - line_len = len(line) - - if line_len > self._longest_line: - self._longest_line = line_len - - sys.stdout.write("\r%s\r" %(" " * (self._longest_line + overdraw))) - sys.stdout.write(line) - sys.stdout.flush() - - def finish(self): - if self.total is not None: - self.current = self.total - - if not self.draw_on_update: - self.draw(overdraw=2) - - sys.stdout.write("\n") - sys.stdout.flush() - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - pass +_version = 2.999 diff --git a/aux.py b/aux.py new file mode 100644 index 0000000..8a93669 --- /dev/null +++ b/aux.py @@ -0,0 +1,8 @@ +#! /bin/env python3 +# encoding: utf-8 + +import sys + +from . import core + +_print = core.textui.Output("over", stream=sys.stderr) diff --git a/build-all.sh b/build-all.sh index 5a5fb11..da24863 100755 --- a/build-all.sh +++ b/build-all.sh @@ -3,7 +3,7 @@ for dir in ag core m serial do - echo "Building in ${dir}" + echo "Building Cython implementations in ${dir}" cd "$dir" ./build.sh cd .. diff --git a/core/TODO b/core/TODO deleted file mode 100644 index f5ae857..0000000 --- a/core/TODO +++ /dev/null @@ -1,2 +0,0 @@ - * sjednotit parsing cmdlajny a configu - * integrovat se zshellem diff --git a/core/__init__.py b/core/__init__.py index e69de29..2bdf440 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -0,0 +1,14 @@ +#! /bin/env python3 +# encoding: utf-8 + +from . import app +from . import aux +from . import file +from . import misc +from . import textui + +try: + from . import cython_types as types +except: + aux._print("unable to load C implementation, using python instead", textui.prefix.warn) + from . import python_types as types diff --git a/core/core.40-Main.pyx b/core/app.py similarity index 97% rename from core/core.40-Main.pyx rename to core/app.py index 1d9e532..0863692 100644 --- a/core/core.40-Main.pyx +++ b/core/app.py @@ -1,3 +1,14 @@ +#! /bin/env python3 +# encoding: utf-8 + +import sys + +# FIXME This very seriously needs to be heavily simplified and de-duplicated +# TODO same parser for cmdline and Config +# TODO zsh and bash integration + +# -------------------------------------------------- + def _parse(data, dtype): if dtype == "str": if data.startswith("\"") and data.endswith("\""): @@ -35,11 +46,15 @@ _over_help_texts = [ ("Other", ["When enabled in the program, an OverCore program offers two options: §B--§ghelp§/ and §B--§gover-help§/. The first describes the program and its options, including their types, current values and whether is their current value coming from defaults, config files or the command line. The second option displays the help you're reading right now. You may now guess which rank I hold in the Obvious Corps.", "As the brighter amongst you might have noticed, OverCore likes colors. A lot. Generally, I use blue for §bdata types§/, magenta for §mvalues§/, white and green for §B--§goptions§/ and reserve red and yellow for when §rshit hits§/ §ythe fan§/, where red letters usually tell you §Bwhy§/ and yellow ones tell you §Bwhat§/.", "And it's not just colors! I like money, too. Push donations to §B1sekErApM4zh35RFW7qGWs5Yeo9EYWjyV§/, or catch me somewhere and force me to accept cash."]) ] +# -------------------------------------------------- + def _output(text, indent=0, newlines=1): sys.stdout.write(render(paragraph(text, indent=indent), True)) sys.stdout.write(newlines * "\n") sys.stdout.flush() +# -------------------------------------------------- + def _print_help(main, help_texts, chapter=None, list_options=False): """ Displays a help text and then quits the program. @@ -131,6 +146,8 @@ _allowed_types = { "float": [int, float] } +# -------------------------------------------------- + def _verify_type(data, dtype, plural=False): if plural: if type(data) == list: @@ -150,6 +167,8 @@ def _verify_type(data, dtype, plural=False): else: return False +# -------------------------------------------------- + class CfgAccessor: """ Provides convenient access to configuration options' values. @@ -171,6 +190,8 @@ class CfgAccessor: else: self.main._print("option §r%s§/ doesn't exist" %(opt_name), prefix.fail, exc=GeneralError) +# -------------------------------------------------- + class Main: """ Application backbone. Provides: @@ -607,6 +628,8 @@ class Main: def help_over(self, *dummy): _print_help(self, _over_help_texts) +# -------------------------------------------------- + class Option: def __init__(self, name, dtype, default, description, short_name, plural, use_cfg_file, callback, hidden): """ diff --git a/core/aux.py b/core/aux.py new file mode 100644 index 0000000..f02d845 --- /dev/null +++ b/core/aux.py @@ -0,0 +1,8 @@ +#! /bin/env python3 +# encoding: utf-8 + +import sys + +from . import textui + +_print = textui.Output("over.core", stream=sys.stderr) diff --git a/core/core.00-header.pyx b/core/core.00-header.pyx deleted file mode 100644 index f04a32a..0000000 --- a/core/core.00-header.pyx +++ /dev/null @@ -1,17 +0,0 @@ -import os -import re -import sys -import time -import struct -import termios -import fcntl - -_version = 2.1 - -class GeneralError(Exception): - """General string error. Thrown around a lot these days.""" - def __init__(self, description=None): - self.description = description - - def __str__(self): - return self.description diff --git a/core/core.10-text.pyx b/core/core.10-text.pyx deleted file mode 100644 index 472cd32..0000000 --- a/core/core.10-text.pyx +++ /dev/null @@ -1,175 +0,0 @@ -# def textfilter(text, set=None): -# """ -# str text text to filter -# dict set {to_replace: replacement} - -# Text filter that replaces occurences of to_replace keys with their respective values. - -# Defaults to filtering of 'bad' characters if no translational dictionary is provided. -# """ - -# if not set: -# set = badchars - -# for to_replace in set.keys(): -# text = text.replace(to_replace, set[to_replace]) - -# return text - -# def cut(text, size, end=0, colors=True): -# """ -# str text text to cut -# int size how many chars to return, >=0 -# int end return chars from 0 = left, 1 = right -# bool colors skip color tags -# """ - -# out = "" - -# strlen = len(text) - -# if end == 0: -# ptr = 0 # go from left -# else: -# ptr = strlen - 1 - -# while size: -# if end == 0: -# if colors and text[ptr] == "<" and strlen - ptr >= 4 and text[ptr+1] == "C" and text[ptr+3] == ">": # we have a tag -# out += text[ptr:ptr+4] -# ptr += 4 -# else: -# out += text[ptr] -# ptr += 1 -# size -= 1 -# else: -# if colors and text[ptr] == ">" and ptr >= 4 and text[ptr-2] == "C" and text[ptr-3] == "<": # we have a tag -# out = text[ptr-3:ptr+1] + out -# ptr -= 4 -# else: -# out = text[ptr] + out -# ptr -= 1 -# size -= 1 - -# # reached end ? -# if (end == 0 and ptr == strlen) or (end == 1 and ptr == -1): -# break - -# return out - -# def strtrim(text, length, mode=0, dots=True, colors=True): -# """ -# str text text to trim -# int length desired length, >1 -# int mode -1 = cut chars from the left, 0 = from the middle, 1 = from the right -# bool dots add an ellipsis to the point of cutting -# bool colors dont count color tags into length; also turns the ellipsis blue -# """ - -# if len(text) <= length: -# return text - -# if length <= 3: -# dots = False - -# if dots: -# length -= 3 - -# if mode == -1: -# if dots: -# return "..." + cut(text, length, 1, colors) -# else: -# return cut(text, length, 1, colors) - -# elif mode == 0: -# if length%2 == 1: -# part1 = cut(text, length/2+1, 0, colors) -# else: -# part1 = cut(text, length/2, 0, colors) - -# part2 = cut(text, length/2, 1, colors) - -# if dots: -# part1 += "..." - -# return part1 + part2 - -# else: -# if dots: -# return cut(text, length, 0, colors) + "..." -# else: -# return cut(text, length, 0, colors) - - -def paragraph(text, width=0, indent=0, prefix=None, stamp=None): - """ - str text text to format - int width required line length; if 0, current terminal width will be used; - if negative, current terminal width minus supplied amount will be used - int indent how many spaces to indent the text with - str prefix prefix each line with it; not counted in width (offsets the lines) - str stamp placed over the first line's indent (stretching the indent if necessary) - - Formats text into an indented paragraph that fits the terminal and returns it. - Correctly handles color tags. - """ - - #words = [x.strip() for x in text.split() if x.strip()] - words = text.split() - - term_width = get_terminal_size()[1] - - if not width: - width = term_width - elif width < 0: - width += term_width - - lines = [] # individual lines go in this buffer - - first = True - while words: - if indent: - if first and stamp: - lines.append([stamp + " "*(indent-1-len(stamp))]) - else: - lines.append([" "*(indent-1)]) # first word = indent minus one space (that's gonna get back while joining) - - first = False - else: - lines.append([]) - - while words: - word_added = False - - if len(re.sub("§.", "", ' '.join(lines[-1]))) + len(re.sub("§.", "", words[0])) + 1 <= width: - lines[-1].append(words.pop(0)) - word_added = True - - elif not word_added and len(lines[-1]) == 1 and indent: - # no word added and just the indent's in here = word's too long -> screw indent - # we might try to keep at least a part of the indent - if possible - len_word = len(re.sub("§.", "", words[0])) - if len_word < width: - lines[-1] = [" "*(width - len_word - 1), words.pop(0)] - else: - lines[-1] = [words.pop(0)] - word_added = True - break - - elif not word_added and not lines[-1] and not indent: - # no word added, empty line = word's too long -> screw indent - lines[-1] = [words.pop(0)] - word_added = True - break - - else: - break - - lines_tmp = [] - for line in lines: - if prefix: - line.insert(0, prefix) - - lines_tmp.append(' '.join(line)) # put words together - - return '\n'.join(lines_tmp) # put lines together diff --git a/core/core.20-Output.pyx b/core/core.20-Output.pyx deleted file mode 100644 index a5580b1..0000000 --- a/core/core.20-Output.pyx +++ /dev/null @@ -1,153 +0,0 @@ -class prefix: - info = (" ", "INFO") - debug = (" §b?§/", "§bDEBG§/") - start = ("§B>>>§/", "§BEXEC§/") - exec = start - warn = (" §y#§/", "§yWARN§/") - fail = ("§r!!!§/", "§rFAIL§/") - done = (" §g*§/", "§gDONE§/") - -colortags = { - "§r": "\x1b[31;01m", - "§g": "\x1b[32;01m", - "§y": "\x1b[33;01m", - "§b": "\x1b[34;01m", - "§m": "\x1b[35;01m", - "§c": "\x1b[36;01m", - "§B": "\x1b[01m", - "§/": "\x1b[39;49;00m" -} - -def render(text, colors=True): - """ - Processes text with color tags and either - removes them (with colors=False) or replaces - them with terminal color codes. - """ - - text = str(text) - - if colors: - tags = re.findall('§[^§]', text) - - for tag in tags: - try: - text = text.replace(tag, colortags[tag]) - except KeyError: - pass - else: - text = re.sub('§[^§]', '', text) - - # unescape actual paragraphs - text = re.sub('§§', '§', text) - - return text - -def char_length(string): - """ - Returns the length of a string minus all formatting tags. - """ - - plain_string = render(string, colors=False) - - return len(plain_string) - -class Output: - """ - Text UI output renderer. - - Prints messages to the stdout with optional eye candy - like colors and timestamps. - - Usage: - >>> from over import Output, prefix - >>> say = Output("test", timestamp=True) - >>> say("system initialized") - [2013-02-28 16:41:28] INFO -- test, system initialized - >>> say("system is FUBAR", prefix.fail) - [2013-02-28 16:41:46] FAIL -- test, system is FUBAR - >>> say("I just realized this will not work", prefix.fail, timestamp=False) - !!! I just realized this will not work - - TODO initialize with target output streams (which only need to implement .write) - """ - - def __init__(self, name, default_suffix="\n", timestamp=False, colors=True, default_prefix=prefix.info, tb=False): - self.name = name - self.timestamp = timestamp - self.colors = colors - self.default_prefix = default_prefix - self.default_suffix = default_suffix - self.tb = tb - - def __call__(self, text, prefix=None, suffix=None, indent=0, timestamp=None, colors=None, display_name=True, exc=None, tb=None): - if prefix is None: - prefix = self.default_prefix - - if type(prefix) is str: - prefix = (prefix, prefix) - - if suffix is None: - suffix = self.default_suffix - - if timestamp is None: - timestamp = self.timestamp - - if colors is None: - colors = self.colors - - if tb is None: - tb = self.tb - - output = [] - - # [2012-11-11 16:52:06] INFO -- ahoj - if timestamp: - output.append(time.strftime('[%Y-%m-%d %H:%M:%S] ')) - output.append(prefix[1]) - output.append(" -- ") - elif prefix: - output.append(prefix[0]) - output.append(" ") - - if display_name and self.name: - output.append("%s, " %(self.name)) - - #output.append(paragraph(str(text), indent=indent)) - output.append(str(text)) - - if suffix: - output.append(suffix) - - output = "".join(output) - - sys.stdout.write(render(output, colors)) - sys.stdout.flush() - - if exc: - if exc is True: - if tb: - raise - else: - if sys.exc_info()[0]: - self.__call__("unhandled exception %s raised" %(sys.exc_info()[0].__name__), timestamp=True) - #sys.exit(1) - raise - else: - if tb: - raise exc(render(text, colors=False)) - else: - self.__call__("unhandled exception %s raised" %(exc.__name__), timestamp=True) - raise exc - #sys.exit(1) - -def get_terminal_size(): - """ - Returns current terminal's (rows, cols). - """ - - terminal = sys.stdout.fileno() - try: - return struct.unpack("HHHH", fcntl.ioctl(terminal, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0))) - except IOError: - return (40, 80) diff --git a/core/core.30-File.pyx b/core/core.30-File.pyx deleted file mode 100644 index 5edc436..0000000 --- a/core/core.30-File.pyx +++ /dev/null @@ -1,51 +0,0 @@ -class File: - """ - A binary r/w file container that transparently abstracts file descriptors away. You just read and write data. - - Nonexistent files will be created, including any directories if necessary. - """ - - # def __init__(self, path, str encoding="utf-8"): - def __init__(self, path, encoding="utf-8"): - self.encoding = encoding - self._print = Output("over.File", timestamp=True) - - if path[0] == "~": - self.path = os.path.join(os.getenv("HOME"), path[2:]) - else: - self.path = path - - if not os.path.isfile(self.path): - if os.path.exists(self.path): - self._print("path §r%s§/ exists but is not a file" %(self.path), prefix.fail, exc=GeneralError) - else: - dirname = os.path.dirname(self.path) - - if dirname and not os.path.isdir(dirname): - self._print("creating directory §B%s§/" %(dirname), prefix.start) - os.makedirs(dirname) - - # create the file - touch(self.path) - - @property - def data(self): - """ - Reads the file and returns the contents. - """ - - fd = open(self.path, encoding=self.encoding) - data = fd.read() - fd.close() - - return data - - @data.setter - def data(self, data): - """ - Writes data into the file. - """ - - fd = open(self.path, "w", encoding=self.encoding) - fd.write(data) - fd.close() diff --git a/core/core.50-misc.pyx b/core/core.50-misc.pyx deleted file mode 100644 index d4070c5..0000000 --- a/core/core.50-misc.pyx +++ /dev/null @@ -1,340 +0,0 @@ -# class Autoloader: -# """ -# Python module autoloader. To activate, call et.autoloader.register() from python shell. -# Try not to rely on it as it'll betray you when there's more than one new module in a single command. -# """ - -# def __init__(self): -# self.original_handler = sys.excepthook - -# def register(self, deact=False): -# if not deact: -# sys.excepthook = self -# else: -# sys.excepthook = self.original_handler - -# def __call__(self, exctype, value, trace): -# if exctype == NameError: -# module_name = textfilter(value.args[0], {"name '": "", "' is not defined": ""}) -# #retval = _exec("import %s" % module_name, trace) - -# command_locals = trace.tb_frame.f_locals -# command_globals = trace.tb_frame.f_globals -# try: # retval relates to import success only -# exec "import %s" % module_name in command_locals, command_globals -# retval = True -# output("Autoloaded module §g%s§/" %(module_name), 0, timestamp=True) -# exec trace.tb_frame.f_code in command_locals, command_globals -# retval = True -# except ImportError: -# retval = False -# except: -# traceback.print_exc() - -# if exctype != NameError or not retval: -# traceback.print_exception(exctype, value, trace) - -# def wait(seconds_target, text="", minutes=True, progress_bar=True, countdown=False): -# """ -# int seconds how long to wait -# str text display this text before the timer -# bool minutes use mm:ss ? -# bool progress_bar display a progress bar ? TODO -# bool countdown count down instead of up - -# Wait clock. Don't use it on larger scales, i.e. hours, as it accumulates little error every tick -# and you may end up waiting extra minutes or even dead. - -# Returns True if zero is reached, False if interrupted. -# """ - -# seconds = 0 - -# try: -# while seconds <= seconds_target: -# if countdown: -# seconds_use = seconds_target - seconds -# else: -# seconds_use = seconds - -# if minutes: -# mins = str(seconds_use/60).zfill(2) -# secs = str(seconds_use%60).zfill(2) - -# mins_target = str(seconds_target/60).zfill(2) -# secs_target = str(seconds_target%60).zfill(2) - -# time_tag = "%s:%s / %s:%s" %(mins, secs, mins_target, secs_target) -# else: -# time_tag = "%s / %s" %(seconds_use, seconds_target) - -# prog_bar = " " -# if progress_bar: -# # determine how much space we have to crap up... I mean use -# # 13 = 8 spaces at the end, 5 for ' [>] ' -# width_max = get_terminal_size()[1] - len(re.sub("§.", "", text)) - len(time_tag) - 20 -# left_side = int((seconds/float(seconds_target)) * width_max) -# right_side = width_max - left_side - -# prog_bar = " [%s>%s] " %('='*left_side, ' '*right_side) - -# output(text+prog_bar+time_tag+' \r', 0, newline=False) - -# if not seconds == seconds_target: -# time.sleep(1) - -# seconds += 1 - -# except (EOFError, KeyboardInterrupt): -# output(text+prog_bar+time_tag+' ', 3) -# return False - -# else: -# output(text+prog_bar+time_tag+' ', 2) -# return True - -# def progress(part, width=None, before="", after="", show_percent=True, newline=False): -# """ -# Display a progress bar. - -# float part 0.0 to 1.0 -# int width width in cols, defaults to terminal width -# str before text displayed before the progress bar -# str after text displayed after the progress bar -# bool show_percent display percent after the bar (but before the after part) -# bool newline append a \n -# """ - -# if not width: -# width = get_terminal_size()[1] - -# width_bar = width - 2 - 2 - 1 -# # a b c -# # a = space on each side of the terminal -# # b = [ and ] -# # c = > - - -# if before: -# width_bar -= 1 + len(before) -# before = before + " " - -# if after: -# width_bar -= 1 + len(after) -# after = " " + after - -# if show_percent: -# width_bar -= 5 -# percent = "%s%% " %(str(int(round(part*100))).rjust(3)) - -# if not newline: -# r_part = "\r" -# else: -# r_part = "" - -# output(" %s%s[%s>%s]%s %s" %(before, percent, "="*int(part*width_bar), " "*(int((1-part)*width_bar)), after, r_part), newline=newline) - -# class Unit: -# _prefixes = ( ("Y", 24), ("Z", 21), ("E", 18), ("P", 15), ("T", 12), ("G", 9), ("M", 6), ("k", 3), ("", 0), -# ("m", -3), ("μ", -6), ("n", -9), ("p", -12), ("f", -15), ("a", -18), ("z", -21), ("y", -24)) -# #("c", -2), - -# def __init__(self, initializer, force_unit=None, dimension=1, space=True): -# self.space = space -# self.dimension = dimension - -# if type(initializer) == str: # init from text -# integer, decimal, unit = re.findall("(\d+)[.,]*(\d*)\s*(\D+)", initializer)[0] -# self.value = float("%s.%s" %(integer, decimal)) - -# if len(unit) >= 2: -# prefix = self._prefix_to_multiplier(unit[0]) - -# if prefix != None: -# self.value *= 10**(prefix*self.dimension) -# self.unit = unit[1:] -# else: -# self.unit = unit -# else: -# self.unit = unit - -# if force_unit: -# self.unit = force_unit - -# else: # init from float, str unit and int dimension -# self.value = float(initializer) -# self.unit = force_unit - -# def _prefix_to_multiplier(self, prefix): -# for p, mul in self._prefixes: -# if p == prefix: -# return mul - -# return None - -# def __repr__(self): -# value = self.value - -# if value < 0: -# sign = -1 -# value *= -1 -# else: -# sign = 1 - -# if value == 0.0: -# e = 0 -# else: -# e = round(math.log(value, 10), 6) - -# for prefix, mul in self._prefixes: -# if mul*self.dimension <= e: -# break - -# if self.unit: -# unit = self.unit -# else: -# unit = "" - -# if self.space: -# space = " " -# else: -# space = "" - -# return "%.2f%s%s%s" %(sign*value/10**(mul*self.dimension), space, prefix, unit) - -# by ephemient@stackoverflow.com -def touch(fname, times=None): - with open(fname, 'a'): - os.utime(fname, times) - -def console(environment): - """ - Opens up a Python console. - - Typical usage: over.console(locals()) - """ - - import code, readline, rlcompleter - - readline.parse_and_bind("tab: complete") - - _print = Output("over.core", default_suffix=".\n", timestamp=True, tb=False) - _print("opening Python console", prefix.start) - - c = code.InteractiveConsole(environment) - c.interact(banner="") - -class enum: - """ - Emulates a C++-like enum type. - - Based on a py2 enum function by Alec Thomas and acjohnson55. - """ - - def __init__(self, name, words, start=0): - self.typename = name - self.reverse_enums = dict(enumerate(words, start)) - self.enums = dict((value, key) for key, value in self.reverse_enums.items()) - - def name(self, value): - if value in self.reverse_enums: - return self.reverse_enums[value] - else: - raise AttributeError("No attribute of %s has a value of %s." %(self, value)) - - def __getattr__(self, aname): - if aname in self.enums: - return self.enums[aname] - else: - raise AttributeError("%s not in %s." %(aname, self)) - - def __repr__(self): - return "" %(self.typename) - -class map: - def __init__(self, source=None): - """ - source is a zipped list: [(key, value), (key, value), ...] - """ - - if source: - self.keys, self.vals = zip(*source) - else: - self.keys = [] - self.vals = [] - - def __get_index(self, key): - if key in self.keys: - return self.keys.index(key) - else: - return None - - def __getitem__(self, key): - i = self.__get_index(key) - - if i is None: - raise KeyError(key) - else: - return self.vals[i] - - def __setitem__(self, key, val): - i = self.__get_index(key) - - if i is None: - self.keys.append(key) - self.vals.append(val) - else: - self.vals[i] = val - - def __contains__(self, item): - return item in self.keys - - def index(self, item): - return self.keys.index(item) - - def sort(self, key=None): - tmp_keys = self.keys - tmp_vals = self.vals - - self.keys = [] - self.vals = [] - - for K, V in sorted(zip(tmp_keys, tmp_vals), key=key): - self.keys.append(K) - self.vals.append(V) - - @property - def items(self): - return zip(self.keys, self.vals) - - def __len__(self): - return len(self.vals) - - def __repr__(self): - pairs = [] - - for i in range(len(self.keys)): - pairs.append("%s: %s" %(repr(self.keys[i]), repr(self.vals[i]))) - - return "<{%s}>" %(", ".join(pairs)) - -def batch_gen(data, batch_size): - """ - by rpr (stackoverflow) - """ - - for i in range(0, len(data), batch_size): - yield data[i:i+batch_size] - -class ndict(dict): - def __init__(self, *args, **kwargs): - dict.__init__(self, *args, **kwargs) - - def __getattr__(self, name): - if name in self: - return self[name] - else: - raise AttributeError("'ndict' object has no attribute '%s'" %(name)) - - def __setattr__(self, name, value): - self[name] = value diff --git a/core/core.51-import.pyx b/core/core.51-import.pyx deleted file mode 100644 index b62dea8..0000000 --- a/core/core.51-import.pyx +++ /dev/null @@ -1,31 +0,0 @@ -import imp - -def import_module(path): - """ - Imports a python file as a module. The path can be relative or absolute. - - Based on the work of Yuval Greenfield released into the public domain. - """ - - # remove the .py suffix - mod_dn = os.path.dirname(path) - mod_fn = os.path.basename(path) - - if mod_fn.endswith(".py"): - mod_name = mod_fn[:-3] - else: - # packages for example - mod_name = mod_fn - - fd = None - - try: - data = imp.find_module(mod_name, [mod_dn]) - module = imp.load_module(mod_name, *data) - fd = data[0] - - finally: - if fd is not None: - fd.close() - - return module diff --git a/core/file.py b/core/file.py new file mode 100644 index 0000000..67e8e5c --- /dev/null +++ b/core/file.py @@ -0,0 +1,87 @@ +#! /bin/env python3 +# encoding: utf-8 + +import os + +from .aux import _print +from .textui import prefix + +# -------------------------------------------------- + +class File: + """ + A binary r/w file container that abstracts file descriptors away. You just read and write data. + + Nonexistent files will be created, including any directories if necessary. + """ + + def __init__(self, path, encoding=None): + """ + encoding which encoding to use when opening files; None means binary (raw) + """ + + self.encoding = encoding + + if path[0] == "~": + self.path = os.path.join(os.getenv("HOME"), path[2:]) + else: + self.path = path + + if not os.path.isfile(self.path): + if os.path.exists(self.path): + _print("path §y%s§/ exists but §ris not a file§/" %(self.path), prefix.fail) + raise RuntimeError + + else: + dirname = os.path.dirname(self.path) + + if dirname and not os.path.isdir(dirname): + _print("creating directory §B%s§/" %(dirname)) + os.makedirs(dirname) + + # create the file + touch(self.path) + + @property + def data(self): + """ + Reads the file and returns the contents. + """ + + if self.encoding: + fd = open(self.path, encoding=self.encoding) + else: + fd = open(self.path, "rb") + + data = fd.read() + fd.close() + + return data + + @data.setter + def data(self, data): + """ + Writes data into the file. + """ + if self.encoding: + fd = open(self.path, "w", encoding=self.encoding) + else: + fd = open(self.path, "wb") + + fd.write(data) + fd.close() + + def __repr__(self): + return "over.core.File(%s %s)" %(self.path, self.encoding if self.encoding else "raw") + +# -------------------------------------------------- + +def touch(fname, times=None): + """ + Sets a filename's atime and mtime. + + times is a tuple of (atime, mtime) and defaults to "now". + """ + + with open(fname, 'a'): + os.utime(fname, times) diff --git a/core/misc.py b/core/misc.py new file mode 100644 index 0000000..af5bfbf --- /dev/null +++ b/core/misc.py @@ -0,0 +1,65 @@ +#! /bin/env python3 +# encoding: utf-8 + +import imp +import os + +# -------------------------------------------------- + +def import_module(path): + """ + Imports a python file as a module. The path can be relative or absolute. + + Based on the work of Yuval Greenfield released into the public domain. + """ + + # remove the .py suffix + mod_dn = os.path.dirname(path) + mod_fn = os.path.basename(path) + + if mod_fn.endswith(".py"): + mod_name = mod_fn[:-3] + else: + # packages for example + mod_name = mod_fn + + fd = None + + try: + data = imp.find_module(mod_name, [mod_dn]) + module = imp.load_module(mod_name, *data) + fd = data[0] + + finally: + if fd is not None: + fd.close() + + return module + +# -------------------------------------------------- + +def batch_gen(data, batch_size): + """ + Split data (a sequence) into sequences batch_size elements long. + """ + + for i in range(0, len(data), batch_size): + yield data[i:i+batch_size] + +# -------------------------------------------------- + +def console(environment, tab_completion=False): + """ + Opens up a Python console. + + environment is a dictionary typically returned by locals() or similar + tab_completion enables completion of object names using TAB, however note + that this will make pasting formatted snippets of code difficult + """ + + import code, readline, rlcompleter + + readline.parse_and_bind("tab: complete") + + c = code.InteractiveConsole(environment) + c.interact(banner="") diff --git a/core/python_types.py b/core/python_types.py new file mode 100644 index 0000000..5c4f226 --- /dev/null +++ b/core/python_types.py @@ -0,0 +1,151 @@ +#! /bin/env python3 +# encoding: utf-8 + +# -------------------------------------------------- + +class ndict(dict): + """ + A dictionary superclass whose keys are exposed as attributes. + + >>> d = ndict() + >>> d.alpha = 1 + >>> d["alpha"] + 1 + >>> d["beta"] = 42 + >>> d.beta + 42 + >>> d + {'alpha': 1, 'beta': 42} + """ + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + + def __getattr__(self, name): + if name in self: + return self[name] + else: + raise AttributeError("'ndict' object has no attribute '%s'" %(name)) + + def __setattr__(self, name, value): + self[name] = value + +# -------------------------------------------------- + +class map: + """ + An ordered dict. + """ + + def __init__(self, source=None): + """ + Initialize the map. + + The source is a sequence of (key, value) tuples. + + TODO fixme bug + >>> map(((1, 2, 3, 4), ("a", "b", "c", "d"))) + """ + + print(repr(source)) + + if source: + self.keys, self.vals = zip(*source) + else: + self.keys = [] + self.vals = [] + + def __get_index(self, key): + if key in self.keys: + return self.keys.index(key) + else: + return None + + def __getitem__(self, key): + i = self.__get_index(key) + + if i is None: + raise KeyError(key) + else: + return self.vals[i] + + def __setitem__(self, key, val): + i = self.__get_index(key) + + if i is None: + self.keys.append(key) + self.vals.append(val) + else: + self.vals[i] = val + + def __contains__(self, item): + return item in self.keys + + def index(self, item): + return self.keys.index(item) + + def sort(self, key=None): + tmp_keys = self.keys + tmp_vals = self.vals + + self.keys = [] + self.vals = [] + + for K, V in sorted(zip(tmp_keys, tmp_vals), key=key): + self.keys.append(K) + self.vals.append(V) + + @property + def items(self): + return zip(self.keys, self.vals) + + def __len__(self): + return len(self.vals) + + def __repr__(self): + pairs = [] + + for i in range(len(self.keys)): + pairs.append("%s: %s" %(repr(self.keys[i]), repr(self.vals[i]))) + + return "<{%s}>" %(", ".join(pairs)) + +# -------------------------------------------------- + +class enum: + """ + Emulates a C++-like enum type. + + Based on a py2 enum function by Alec Thomas and acjohnson55. + """ + + def __init__(self, name, words, start=0): + """ + Initializes the enum. + + name is used for __repr__ only + words is a sequence of enum keys + start is the numerical value of the first key + """ + + self.typename = name + self.reverse_enums = dict(enumerate(words, start)) + self.enums = dict((value, key) for key, value in self.reverse_enums.items()) + + def name(self, value): + if value in self.reverse_enums: + return self.reverse_enums[value] + else: + raise AttributeError("no %s has a value of %s" %(self, value)) + + def __getattr__(self, aname): + if aname in self.enums: + return self.enums[aname] + else: + raise AttributeError("%s not in %s" %(aname, self)) + + def __repr__(self): + return "" %(self.typename) + +# -------------------------------------------------- + diff --git a/core/textui.py b/core/textui.py new file mode 100644 index 0000000..49e2199 --- /dev/null +++ b/core/textui.py @@ -0,0 +1,475 @@ +#! /bin/env python3 +# encoding: utf-8 + +import math +import re +import sys +import time + +# -------------------------------------------------- + +class ProgressBar: + """ + An animated progress bar. + + TODO derive Wait() from this + """ + + def __init__(self, width, top, unit, reverse=False): + """ + width width of the "widget" including all text + top the 100% value + unit name of the unit to display + reverse True to expand the progress bar from right to left + """ + + self.width = width + self.value = 0 + self.top = top + self.unit = unit + self.reverse = reverse + + self.old_len = 0 + self.t_start = None + + def draw(self): + if not self.t_start: + self.t_start = time.time() + + if self.old_len: + sys.stderr.write("\b" * self.old_len) + + transferred = Unit(self.value, self.unit) + dt = time.time() - self.t_start + + if dt > 0: + speed = self.value / dt + else: + speed = 0.0 + + speed = Unit(speed, "%s/s" %(self.unit)) + + ratio = self.value / self.top + pb_done = "=" * int(self.width * ratio) + pb_rem = " " * int(self.width * (1 - ratio)) + symbol = ">" + + if self.reverse: + symbol = "<" + pb_done, pb_rem = pb_rem, pb_done + + text = "%s [%s%s%s] %s" %(transferred, pb_done, symbol, pb_rem, speed) + + sys.stderr.write(text) + + current_len = len(text) + tail = self.old_len - current_len + self.old_len = current_len + + if tail > 0: + sys.stderr.write(" " * tail) + sys.stderr.write("\b" * tail) + + sys.stderr.flush() + + def update(self, value): + self.value = value + + self.draw() + +# -------------------------------------------------- + +class Unit: + """ + A object that represents numbers and units in human-readable form. + + TODO use significant digits instead of rigid order boundaries + TODO float superclass? + TODO base_2 prefixes (Ki, Mi, ...) + """ + + _prefixes = ( + ("Y", 24), ("Z", 21), ("E", 18), ("P", 15), ("T", 12), ("G", 9), ("M", 6), ("k", 3), + ("h", 2), ("D", 1), ("", 0), ("d", -1), ("c", -2), ("m", -3), ("μ", -6), ("n", -9), + ("p", -12), ("f", -15), ("a", -18), ("z", -21), ("y", -24) + ) + + def __init__(self, + value, unit=None, dimension=1, + use_prefixes="YZEPTGMkmμnpfazy", format="%.2f pU", + logarithmic=False, log_base=10 + ): + """ + value the numerical value of the variable (int or float) + unit the symbol to use, if any (str or None) + dimension the dimensionality of the value (1, 2, 3, ...) + use_prefixes which multiplier prefixes to use + - the default "YZEPTGMkmμnpfazy" omits "c" for centi- and "D" for deca- + format use printf notation for value (e.g. %010.5f), p for prefix and U for unit + logarithmic when True, log_10 prefixes are used + log_base logarithm base value + + note that deca- is correctly rendered as "da", the "D" is used in use_prefixes only + """ + + self.value = float(value) + self.unit = unit if unit else "" + self.dimension = dimension + self.use_prefixes = use_prefixes + self.format = format + self.logarithmic = logarithmic + self.log_base = log_base + + if self.logarithmic and (self.value < 0 or self.log_base < 0): + raise ValueError("math domain error (negative values can't be represented in dB)") + + def __str__(self): + if self.value == 0.0: + e = 0 + else: + e = round(math.log(abs(self.value), 10), 6) + + if self.logarithmic: + prefix = "dB" + value = math.log(self.value, self.log_base) * 10 + + else: + for prefix, mul in self._prefixes: + if prefix in self.use_prefixes and mul*self.dimension <= e: + break + + value = self.value / 10**(mul*self.dimension) + + output = self.format %(value) + output = output.replace("p", prefix if prefix != "D" else "da") # deca- handler + output = output.replace("U", self.unit) + + return output + + def __repr__(self): + return "over.core.textui.Unit(%s)" %(self) + +# -------------------------------------------------- + +def paragraph(text, width=0, indent=0, prefix=None, stamp=None): + """ + str text text to format + int width required line length; if 0, current terminal width will be used; + - if negative, current terminal width minus supplied amount will be used + int indent how many spaces to indent the text with + str prefix prefix each line with it; not counted in width (offsets the lines) + str stamp placed over the first line's indent (stretching the indent if necessary) + + Formats text into an indented paragraph that fits the terminal and returns it. + Correctly handles color tags. + """ + + #words = [x.strip() for x in text.split() if x.strip()] + words = text.split() + + term_width = get_terminal_size()[1] + + if not width: + width = term_width + elif width < 0: + width += term_width + + lines = [] # individual lines go in this buffer + + first = True + while words: + if indent: + if first and stamp: + lines.append([stamp + " "*(indent-1-len(stamp))]) + else: + lines.append([" "*(indent-1)]) # first word = indent minus one space (that's gonna get back while joining) + + first = False + else: + lines.append([]) + + while words: + word_added = False + + if len(re.sub("§.", "", ' '.join(lines[-1]))) + len(re.sub("§.", "", words[0])) + 1 <= width: + lines[-1].append(words.pop(0)) + word_added = True + + elif not word_added and len(lines[-1]) == 1 and indent: + # no word added and just the indent's in here = word's too long -> screw indent + # we might try to keep at least a part of the indent - if possible + len_word = len(re.sub("§.", "", words[0])) + if len_word < width: + lines[-1] = [" "*(width - len_word - 1), words.pop(0)] + else: + lines[-1] = [words.pop(0)] + word_added = True + break + + elif not word_added and not lines[-1] and not indent: + # no word added, empty line = word's too long -> screw indent + lines[-1] = [words.pop(0)] + word_added = True + break + + else: + break + + lines_tmp = [] + for line in lines: + if prefix: + line.insert(0, prefix) + + lines_tmp.append(' '.join(line)) # put words together + + return '\n'.join(lines_tmp) # put lines together + +# -------------------------------------------------- + +# def strtrim(text, length, mode=0, dots=True, colors=True): +# """ +# str text text to trim +# int length desired length, >1 +# int mode -1 = cut chars from the left, 0 = from the middle, 1 = from the right +# bool dots add an ellipsis to the point of cutting +# bool colors dont count color tags into length; also turns the ellipsis blue +# """ + +# if len(text) <= length: +# return text + +# if length <= 3: +# dots = False + +# if dots: +# length -= 3 + +# if mode == -1: +# if dots: +# return "..." + cut(text, length, 1, colors) +# else: +# return cut(text, length, 1, colors) + +# elif mode == 0: +# if length%2 == 1: +# part1 = cut(text, length/2+1, 0, colors) +# else: +# part1 = cut(text, length/2, 0, colors) + +# part2 = cut(text, length/2, 1, colors) + +# if dots: +# part1 += "..." + +# return part1 + part2 + +# else: +# if dots: +# return cut(text, length, 0, colors) + "..." +# else: +# return cut(text, length, 0, colors) + +# -------------------------------------------------- + +# def textfilter(text, set=None): +# """ +# str text text to filter +# dict set {to_replace: replacement} + +# Text filter that replaces occurences of to_replace keys with their respective values. + +# Defaults to filtering of 'bad' characters if no translational dictionary is provided. +# """ + +# if not set: +# set = badchars + +# for to_replace in set.keys(): +# text = text.replace(to_replace, set[to_replace]) + +# return text + +# -------------------------------------------------- + +# def cut(text, size, end=0, colors=True): +# """ +# str text text to cut +# int size how many chars to return, >=0 +# int end return chars from 0 = left, 1 = right +# bool colors skip color tags +# """ + +# out = "" + +# strlen = len(text) + +# if end == 0: +# ptr = 0 # go from left +# else: +# ptr = strlen - 1 + +# while size: +# if end == 0: +# if colors and text[ptr] == "<" and strlen - ptr >= 4 and text[ptr+1] == "C" and text[ptr+3] == ">": # we have a tag +# out += text[ptr:ptr+4] +# ptr += 4 +# else: +# out += text[ptr] +# ptr += 1 +# size -= 1 +# else: +# if colors and text[ptr] == ">" and ptr >= 4 and text[ptr-2] == "C" and text[ptr-3] == "<": # we have a tag +# out = text[ptr-3:ptr+1] + out +# ptr -= 4 +# else: +# out = text[ptr] + out +# ptr -= 1 +# size -= 1 + +# # reached end ? +# if (end == 0 and ptr == strlen) or (end == 1 and ptr == -1): +# break + +# return out + +# -------------------------------------------------- + +class prefix: + info = (" ", "INFO") + debug = (" §b?§/", "§bDEBG§/") + start = ("§B>>>§/", "§BEXEC§/") + exec = start + warn = (" §y#§/", "§yWARN§/") + fail = ("§r!!!§/", "§rFAIL§/") + done = (" §g*§/", "§gDONE§/") + +colortags = { + "§r": "\x1b[31;01m", + "§g": "\x1b[32;01m", + "§y": "\x1b[33;01m", + "§b": "\x1b[34;01m", + "§m": "\x1b[35;01m", + "§c": "\x1b[36;01m", + "§B": "\x1b[01m", + "§/": "\x1b[39;49;00m" +} + +def render(text, colors=True): + """ + Processes text with color tags and either + removes them (with colors=False) or replaces + them with terminal color codes. + """ + + text = str(text) + + if colors: + tags = re.findall('§[^§]', text) + + for tag in tags: + try: + text = text.replace(tag, colortags[tag]) + except KeyError: + pass + else: + text = re.sub('§[^§]', '', text) + + # unescape actual paragraphs + text = re.sub('§§', '§', text) + + return text + +# -------------------------------------------------- + +def char_length(string): + """ + Returns the length of a string minus all formatting tags. + """ + + plain_string = render(string, colors=False) + + return len(plain_string) + +# -------------------------------------------------- + +class Output: + """ + Text UI output renderer. + + Prints messages to the stdout with optional eye candy + like colors and timestamps. + + Usage: + >>> from over import Output, prefix + >>> say = Output("test", timestamp=True) + >>> say("system initialized") + [2013-02-28 16:41:28] INFO -- test, system initialized + >>> say("system is FUBAR", prefix.fail) + [2013-02-28 16:41:46] FAIL -- test, system is FUBAR + >>> say("I just realized this will not work", prefix.fail, timestamp=False) + !!! I just realized this will not work + + TODO use a generic format string + """ + + def __init__(self, name, timestamp=True, colors=True, default_prefix=prefix.info, default_suffix=".\n", stream=sys.stdout): + self.name = name + self.timestamp = timestamp + self.colors = colors + self.default_prefix = default_prefix + self.default_suffix = default_suffix + self.stream = stream + + def __call__(self, text, prefix=None, suffix=None, indent=0, timestamp=None, colors=None, display_name=True): + if prefix is None: + prefix = self.default_prefix + + if type(prefix) is str: + prefix = (prefix, prefix) + + if suffix is None: + suffix = self.default_suffix + + if timestamp is None: + timestamp = self.timestamp + + if colors is None: + colors = self.colors + + output = [] + + # [2012-11-11 16:52:06] INFO -- ahoj + if timestamp: + output.append(time.strftime('[%Y-%m-%d %H:%M:%S] ')) + output.append(prefix[1]) + output.append(" -- ") + elif prefix: + output.append(prefix[0]) + output.append(" ") + + if display_name and self.name: + output.append("%s, " %(self.name)) + + #output.append(paragraph(str(text), indent=indent)) + output.append(str(text)) + + if suffix: + output.append(suffix) + + output = "".join(output) + + self.stream.write(render(output, colors)) + self.stream.flush() + +# -------------------------------------------------- + +def get_terminal_size(): + """ + Returns current terminal's (rows, cols). + """ + + terminal = sys.stdout.fileno() + try: + return struct.unpack("HHHH", fcntl.ioctl(terminal, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0))) + except IOError: + return (40, 80) diff --git a/m/__init__.py b/m/__init__.py index e69de29..0418d0c 100644 --- a/m/__init__.py +++ b/m/__init__.py @@ -0,0 +1,28 @@ +#! /bin/env python3 +# encoding: utf-8 +# +# Part of Project Overwatch + +import sys + +try: + from .cython_m import mat4, vec3 +except ImportError: + print("!!! [%s] unable to load native implementation, using python instead" %(__name__), file=sys.stderr) + from .python_m import mat4, vec3 + +del sys + +class MathError(Exception): + def __init__(self, description): + self.description = description + + def __str__(self): + return self.description + +class GeneralError(Exception): + def __init__(self, description): + self.description = description + + def __str__(self): + return self.description \ No newline at end of file diff --git a/m/build.sh b/m/build.sh new file mode 100755 index 0000000..a1cc611 --- /dev/null +++ b/m/build.sh @@ -0,0 +1,63 @@ +#! /bin/zsh +# encoding: utf-8 + +setopt extendedglob + +function die() { + echo "\n\n>>> Failed during ${1}, aborting." + exit 1 +} + +function translate() { + echo -n "Translating from Python to C: " + + first=1 + + for item in ${@} + do + if [ $first -eq 1 ]; then + echo -n "${item}" + first=0 + else + echo -n ", ${item}" + fi + + cython -3 ${item}.pyx -o ${item}.c || die "translating" + done + + echo "." +} + +function combobulate() { + name="$1" + + echo -n > "$name.pyx" + + for part in src/$name.*.pyx + do + # echo "Combobulating $part..." + echo "###############################################################################" >> "$name.pyx" + echo "# Combobulated from file $part" >> "$name.pyx" + echo "###############################################################################\n" >> "$name.pyx" + cat "$part" >> "$name.pyx" + echo >> "$name.pyx" + done +} + +CFLAGS=(-Wall -pedantic -std=c99 -fPIC) +LFLAGS=(-shared) + +combobulate core +ln -s src/interface/* . + +# call Cython +translate core + +echo -n "Compiling and linking: core" +gcc $CFLAGS -I/usr/include/python3.3 -pthread -c core.c || die "compilation" +gcc $LFLAGS -L/usr/lib -lpython3.3 core.o -o core.so || die "linking" +rm -f core.{c,o} +echo "." + +# remove temporary sources +rm -f *.pyx *.pxd diff --git a/m/cython_m.pyx b/m/cython_m.pyx new file mode 100644 index 0000000..031dfe8 --- /dev/null +++ b/m/cython_m.pyx @@ -0,0 +1,473 @@ +#! /bin/env python3 +# encoding: utf-8 + +""" +Vector And Matrix Math + +TODO some nice description +""" + +from libc.stdlib cimport malloc, realloc, free +from libc.stdint cimport uint8_t, uint16_t, uint64_t +from libc.math cimport sin, cos, sqrt + +class MathError(Exception): + def __init__(self, description): + self.description = description + + def __str__(self): + return self.description + +class GeneralError(Exception): + def __init__(self, description): + self.description = description + + def __str__(self): + return self.description + +cdef class mat4: + """ + A float 4x4 matrix. + + All arrays are column-major, i.e. OpenGL style: + + 0 4 8 12 + 1 5 9 13 + 2 6 10 14 + 3 7 11 15 + + The matrix implements stacking useful for graphics. + """ + + def __cinit__(mat4 self): + to_alloc = 16 * sizeof(float) + self.stack = malloc(to_alloc) + + if not self.stack: + raise MemoryError("Unable to malloc %d B for mat4." %(to_alloc)) + + self.m = self.stack + self.size = 1 + + def _debug(mat4 self): + print("--- self.stack = %d" %(self.stack)) + print("--- self.m = %d (+%d)" %(self.m, self.m - self.stack)) + print("--- self.size = %d" %(self.size)) + + def __init__(mat4 self, *args): + """ + Create a ma4t. + + Accepts any number of parameters between 0 and 16 to fill the + matrix from the upper left corner going down (column-wise). + """ + + length = len(args) + + if length == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + length = len(args) + + if length > 16: + raise MathError("Attempt to initialize a mat4 with %d arguments." %(length)) + + self.load_from(args) + + def __dealloc__(mat4 self): + free(self.stack) + + def __getstate__(mat4 self): + state = [] + + for i in range(self.m - self.stack + 16): + state.append(self.stack[i]) + + return state + + def __setstate__(mat4 self, state): + length = len(state) + matrices = length//16 + + if not matrices*16 == length: + raise GeneralError("mat4 __setstate__ got %d floats as a state" %(length)) + + self.m = self.stack + + slot_full = False + for start in range(0, length, 16): + if slot_full: + self.push() + slot_full = False + + self.load_from(state[start:start+16]) + slot_full = True + + def __getitem__(mat4 self, int i): + if i > 16 or i < 0: + raise IndexError("element index out of range(16)") + + return self.m[i] + + def __setitem__(self, int i, value): + if i > 16 or i < 0: + raise IndexError("element index out of range(16)") + + self.m[i] = value + + def push(mat4 self): + """ + Push the current matrix into the stack and load up an empty one (a zero matrix) + """ + + # self.m points to the current matrix + # self.stack points to the first matrix + # self.size how many matrices are allocated + + # ensure there's room for one more + cdef unsigned int used = 1 + (self.m - self.stack) / 16 + cdef unsigned int empty = self.size - used + cdef float *tmp + + if not empty: + self.size += 1 + to_alloc = self.size * 16 * sizeof(float) + tmp = realloc(self.stack, to_alloc) + + if tmp: + self.stack = tmp + else: + raise MemoryError("Unable to malloc %d B for mat4." %(to_alloc)) + + # advance the pointer to the new one + self.m = self.stack + 16 * used + + # at this point there's at least enough space for one matrix + # copy the old matrix into the new one + cdef uint8_t i + cdef float *old_m = self.m - 16 + for i in range(16): + self.m[i] = old_m[i] + + def pop(mat4 self): + """ + Pop a matrix from the stack. + """ + + if self.m == self.stack: + raise IndexError("pop from an empty stack") + + self.m -= 16 + + def get_list(mat4 self): + L = [] + + for i in range(16): + L.append(self.m[i]) + + return L + + def load_from(mat4 self, L): + """ + Fill the current matrix from a either a list of values, column-major, + or another matrix. This method doesn't modify the stack, only the + current matrix is read and modified. + + If the number of values isn't 16, it will be padded to 16 by zeros. + If it's larger, GeneralError will be raised. + """ + + if isinstance(L, mat4): + L = L.get_list() + length = 16 + else: + length = len(L) + + if length > 16: + raise GeneralError("supplied list is longer than 16") + + for i in range(16): + if i < length: + self.m[i] = L[i] + else: + self.m[i] = 0.0 + + def zero(mat4 self): + """Fill the matrix with zeroes.""" + + for i in range(16): + self.m[i] = 0.0 + + def identity(mat4 self): + """Make the matrix an identity.""" + + self.zero() + + self.m[0] = 1.0 + self.m[5] = 1.0 + self.m[10] = 1.0 + self.m[15] = 1.0 + + def transpose(mat4 self): + """Transpose the matrix.""" + + cdef float tmp + + tmp = self.m[1] + self.m[1] = self.m[4] + self.m[4] = tmp + + tmp = self.m[2] + self.m[2] = self.m[8] + self.m[8] = tmp + + tmp = self.m[3] + self.m[3] = self.m[12] + self.m[12] = tmp + + tmp = self.m[7] + self.m[7] = self.m[13] + self.m[13] = tmp + + tmp = self.m[11] + self.m[11] = self.m[14] + self.m[14] = tmp + + tmp = self.m[6] + self.m[6] = self.m[9] + self.m[9] = tmp + + def invert(mat4 self): + """Invert the matrix.""" + + cdef float tmp[16] + cdef float det + + tmp[0] = self.m[5]*self.m[10]*self.m[15] - self.m[5]*self.m[11]*self.m[14] - self.m[9]*self.m[6]*self.m[15] + self.m[9]*self.m[7]*self.m[14] + self.m[13]*self.m[6]*self.m[11] - self.m[13]*self.m[7]*self.m[10] + tmp[4] = -self.m[4]*self.m[10]*self.m[15] + self.m[4]*self.m[11]*self.m[14] + self.m[8]*self.m[6]*self.m[15] - self.m[8]*self.m[7]*self.m[14] - self.m[12]*self.m[6]*self.m[11] + self.m[12]*self.m[7]*self.m[10] + tmp[8] = self.m[4]*self.m[9]*self.m[15] - self.m[4]*self.m[11]*self.m[13] - self.m[8]*self.m[5]*self.m[15] + self.m[8]*self.m[7]*self.m[13] + self.m[12]*self.m[5]*self.m[11] - self.m[12]*self.m[7]*self.m[9] + tmp[12] = -self.m[4]*self.m[9]*self.m[14] + self.m[4]*self.m[10]*self.m[13] + self.m[8]*self.m[5]*self.m[14] - self.m[8]*self.m[6]*self.m[13] - self.m[12]*self.m[5]*self.m[10] + self.m[12]*self.m[6]*self.m[9] + + det = self.m[0]*tmp[0] + self.m[1]*tmp[4] + self.m[2]*tmp[8] + self.m[3]*tmp[12] + + # epsilon pulled straight out of Uranus + if det < 0.00005 and det > -0.00005: + print("det=%.1f" %(det)) + return + + tmp[1] = -self.m[1]*self.m[10]*self.m[15] + self.m[1]*self.m[11]*self.m[14] + self.m[9]*self.m[2]*self.m[15] - self.m[9]*self.m[3]*self.m[14] - self.m[13]*self.m[2]*self.m[11] + self.m[13]*self.m[3]*self.m[10] + tmp[5] = self.m[0]*self.m[10]*self.m[15] - self.m[0]*self.m[11]*self.m[14] - self.m[8]*self.m[2]*self.m[15] + self.m[8]*self.m[3]*self.m[14] + self.m[12]*self.m[2]*self.m[11] - self.m[12]*self.m[3]*self.m[10] + tmp[9] = -self.m[0]*self.m[9]*self.m[15] + self.m[0]*self.m[11]*self.m[13] + self.m[8]*self.m[1]*self.m[15] - self.m[8]*self.m[3]*self.m[13] - self.m[12]*self.m[1]*self.m[11] + self.m[12]*self.m[3]*self.m[9] + tmp[13] = self.m[0]*self.m[9]*self.m[14] - self.m[0]*self.m[10]*self.m[13] - self.m[8]*self.m[1]*self.m[14] + self.m[8]*self.m[2]*self.m[13] + self.m[12]*self.m[1]*self.m[10] - self.m[12]*self.m[2]*self.m[9] + tmp[2] = self.m[1]*self.m[6]*self.m[15] - self.m[1]*self.m[7]*self.m[14] - self.m[5]*self.m[2]*self.m[15] + self.m[5]*self.m[3]*self.m[14] + self.m[13]*self.m[2]*self.m[7] - self.m[13]*self.m[3]*self.m[6] + tmp[6] = -self.m[0]*self.m[6]*self.m[15] + self.m[0]*self.m[7]*self.m[14] + self.m[4]*self.m[2]*self.m[15] - self.m[4]*self.m[3]*self.m[14] - self.m[12]*self.m[2]*self.m[7] + self.m[12]*self.m[3]*self.m[6] + tmp[10] = self.m[0]*self.m[5]*self.m[15] - self.m[0]*self.m[7]*self.m[13] - self.m[4]*self.m[1]*self.m[15] + self.m[4]*self.m[3]*self.m[13] + self.m[12]*self.m[1]*self.m[7] - self.m[12]*self.m[3]*self.m[5] + tmp[14] = -self.m[0]*self.m[5]*self.m[14] + self.m[0]*self.m[6]*self.m[13] + self.m[4]*self.m[1]*self.m[14] - self.m[4]*self.m[2]*self.m[13] - self.m[12]*self.m[1]*self.m[6] + self.m[12]*self.m[2]*self.m[5] + tmp[3] = -self.m[1]*self.m[6]*self.m[11] + self.m[1]*self.m[7]*self.m[10] + self.m[5]*self.m[2]*self.m[11] - self.m[5]*self.m[3]*self.m[10] - self.m[9]*self.m[2]*self.m[7] + self.m[9]*self.m[3]*self.m[6] + tmp[7] = self.m[0]*self.m[6]*self.m[11] - self.m[0]*self.m[7]*self.m[10] - self.m[4]*self.m[2]*self.m[11] + self.m[4]*self.m[3]*self.m[10] + self.m[8]*self.m[2]*self.m[7] - self.m[8]*self.m[3]*self.m[6] + tmp[11] = -self.m[0]*self.m[5]*self.m[11] + self.m[0]*self.m[7]*self.m[9] + self.m[4]*self.m[1]*self.m[11] - self.m[4]*self.m[3]*self.m[9] - self.m[8]*self.m[1]*self.m[7] + self.m[8]*self.m[3]*self.m[5] + tmp[15] = self.m[0]*self.m[5]*self.m[10] - self.m[0]*self.m[6]*self.m[9] - self.m[4]*self.m[1]*self.m[10] + self.m[4]*self.m[2]*self.m[9] + self.m[8]*self.m[1]*self.m[6] - self.m[8]*self.m[2]*self.m[5] + + det = 1.0 / det + self.m[0] = tmp[0] * det + self.m[1] = tmp[1] * det + self.m[2] = tmp[2] * det + self.m[3] = tmp[3] * det + self.m[4] = tmp[4] * det + self.m[5] = tmp[5] * det + self.m[6] = tmp[6] * det + self.m[7] = tmp[7] * det + self.m[8] = tmp[8] * det + self.m[9] = tmp[9] * det + self.m[10] = tmp[10] * det + self.m[11] = tmp[11] * det + self.m[12] = tmp[12] * det + self.m[13] = tmp[13] * det + self.m[14] = tmp[14] * det + self.m[15] = tmp[15] * det + + def mulm(mat4 self, mat4 B, bint inplace=False): + """ + Return a matrix that is the result of multiplying this matrix by another. + + M = self * mat4 B + """ + + cdef uint8_t i + cdef mat4 tmp = mat4() + + tmp.m[0] = self.m[0] * B.m[0] + self.m[4] * B.m[1] + self.m[8] * B.m[2] + self.m[12] * B.m[3] + tmp.m[1] = self.m[1] * B.m[0] + self.m[5] * B.m[1] + self.m[9] * B.m[2] + self.m[13] * B.m[3] + tmp.m[2] = self.m[2] * B.m[0] + self.m[6] * B.m[1] + self.m[10] * B.m[2] + self.m[14] * B.m[3] + tmp.m[3] = self.m[3] * B.m[0] + self.m[7] * B.m[1] + self.m[11] * B.m[2] + self.m[15] * B.m[3] + tmp.m[4] = self.m[0] * B.m[4] + self.m[4] * B.m[5] + self.m[8] * B.m[6] + self.m[12] * B.m[7] + tmp.m[5] = self.m[1] * B.m[4] + self.m[5] * B.m[5] + self.m[9] * B.m[6] + self.m[13] * B.m[7] + tmp.m[6] = self.m[2] * B.m[4] + self.m[6] * B.m[5] + self.m[10] * B.m[6] + self.m[14] * B.m[7] + tmp.m[7] = self.m[3] * B.m[4] + self.m[7] * B.m[5] + self.m[11] * B.m[6] + self.m[15] * B.m[7] + tmp.m[8] = self.m[0] * B.m[8] + self.m[4] * B.m[9] + self.m[8] * B.m[10] + self.m[12] * B.m[11] + tmp.m[9] = self.m[1] * B.m[8] + self.m[5] * B.m[9] + self.m[9] * B.m[10] + self.m[13] * B.m[11] + tmp.m[10] = self.m[2] * B.m[8] + self.m[6] * B.m[9] + self.m[10] * B.m[10] + self.m[14] * B.m[11] + tmp.m[11] = self.m[3] * B.m[8] + self.m[7] * B.m[9] + self.m[11] * B.m[10] + self.m[15] * B.m[11] + tmp.m[12] = self.m[0] * B.m[12] + self.m[4] * B.m[13] + self.m[8] * B.m[14] + self.m[12] * B.m[15] + tmp.m[13] = self.m[1] * B.m[12] + self.m[5] * B.m[13] + self.m[9] * B.m[14] + self.m[13] * B.m[15] + tmp.m[14] = self.m[2] * B.m[12] + self.m[6] * B.m[13] + self.m[10] * B.m[14] + self.m[14] * B.m[15] + tmp.m[15] = self.m[3] * B.m[12] + self.m[7] * B.m[13] + self.m[11] * B.m[14] + self.m[15] * B.m[15] + + if inplace: + for i in range(16): + self.m[i] = tmp.m[i] + else: + return tmp + + def mulv(mat4 self, vec3 v): + """ + Return a vec3 that is the result of multiplying this matrix by a vec3. + + u = self * v + """ + + cdef mat4 tmp = vec3() + + tmp.v[0] = v.v[0]*self.m[0] + v.v[1]*self.m[4] + v.v[2]*self.m[8] + self.m[12] + tmp.v[1] = v.v[0]*self.m[1] + v.v[1]*self.m[5] + v.v[2]*self.m[9] + self.m[13] + tmp.v[2] = v.v[0]*self.m[2] + v.v[1]*self.m[6] + v.v[2]*self.m[10] + self.m[14] + + return tmp + + def mulf(mat4 self, f): + """ + Return a matrix that is the result of multiplying this matrix by a scalar. + + M = self * f + """ + + cdef mat4 tmp = mat4() + cdef int i + + for i in range(16): + tmp.m[i] = self.m[i] * f + + return tmp + + def __repr__(mat4 self): + lines = [] + + lines.append("mat4(%.1f %.1f %.1f %.1f" %(self.m[0], self.m[4], self.m[8], self.m[12])) + lines.append(" %.1f %.1f %.1f %.1f" %(self.m[1], self.m[5], self.m[9], self.m[13])) + lines.append(" %.1f %.1f %.1f %.1f" %(self.m[2], self.m[6], self.m[10], self.m[14])) + lines.append(" %.1f %.1f %.1f %.1f)" %(self.m[3], self.m[7], self.m[11], self.m[15])) + + return "\n".join(lines) + +cdef class vec3: + """ + A float 3D vector. + + >>> v = vec3(1, 1, 0) + >>> w = vec3(0, 1, 1) + >>> v.length + 1.4142135623730951 + >>> v.dot(w) + 1.0 + >>> v.cross(w) + vec4(1.00, 1.00, 1.00) + >>> v + w + vec4(1.00, 2.00, 1.00) + >>> w - v + vec4(-1.00, 0.00, 1.00) + + """ + + def __init__(vec3 self, *args): + """ + Create a vec3. + + Accepts any number of parameters between 0 and 3 to fill the vector from the left. + """ + + length = len(args) + + if length == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + length = len(args) + + if length > 3: + raise MathError("Attempt to initialize a vec3 with %d arguments." %(length)) + + for i in range(3): + if i < length: + self.v[i] = args[i] + else: + self.v[i] = 0.0 + + def __getitem__(vec3 self, int i): + if i >= 3 or i < 0: + raise IndexError("element index out of range(3)") + + return self.v[i] + + def __setitem__(vec3 self, int i, float value): + if i >= 3 or i < 0: + raise IndexError("element index out of range(3)") + + self.v[i] = value + + def __repr__(vec3 self): + return "vec3(%.2f, %.2f, %.2f)" %(self.v[0], self.v[1], self.v[2]) + + def __getstate__(vec3 self): + return (self.v[0], self.v[1], self.v[2]) + + def __setstate__(vec3 self, state): + self.v[0] = state[0] + self.v[1] = state[1] + self.v[2] = state[2] + + @property + def length(vec3 self): + """Contains the geometric length of the vector.""" + + return sqrt(self.v[0]**2 + self.v[1]**2 + self.v[2]**2) + + def normalized(vec3 self): + """Returns this vector, normalized.""" + + length = self.length + + return vec3(self.v[0] / length, self.v[1] / length, self.v[2] / length) + + def __add__(vec3 L, vec3 R): + return vec3(L.v[0] + R.v[0], L.v[1] + R.v[1], L.v[2] + R.v[2]) + + def __sub__(vec3 L, vec3 R): + return vec3(L.v[0] - R.v[0], L.v[1] - R.v[1], L.v[2] - R.v[2]) + + def __neg__(vec3 self): + return vec3(-self.v[0], -self.v[1], -self.v[2]) + + def dot(vec3 L, vec3 R): + """ + Returns the dot product of the two vectors. + + E.g. u.dot(v) -> u . v + """ + + return L.v[0] * R.v[0] + L.v[1] * R.v[1] + L.v[2] * R.v[2] + + def cross(vec3 L, vec3 R): + """ + Returns the cross product of the two vectors. + + E.g. u.cross(v) -> u x v + + """ + + return vec3(L.v[1]*R.v[2] - L.v[2]*R.v[1], L.v[0]*R.v[2] - L.v[2]*R.v[0], L.v[0]*R.v[1] - L.v[1]*R.v[0]) + + def __mul__(vec3 L, R): + """ + Multiplication of a vec3 by a float. + + The float has to be on the right. + """ + + return vec3(L.v[0] * R, L.v[1] * R, L.v[2] * R) diff --git a/m/m.header.pyx b/m/m.header.pyx deleted file mode 100644 index e8cf03f..0000000 --- a/m/m.header.pyx +++ /dev/null @@ -1,23 +0,0 @@ -""" -Vector And Matrix Math - -TODO some nice description -""" - -from libc.stdlib cimport malloc, realloc, free -from libc.stdint cimport uint8_t, uint16_t, uint64_t -from libc.math cimport sin, cos, sqrt - -class MathError(Exception): - def __init__(self, description): - self.description = description - - def __str__(self): - return self.description - -class GeneralError(Exception): - def __init__(self, description): - self.description = description - - def __str__(self): - return self.description diff --git a/m/m.vec3.pyx b/m/m.vec3.pyx deleted file mode 100644 index 61b0641..0000000 --- a/m/m.vec3.pyx +++ /dev/null @@ -1,113 +0,0 @@ -cdef class vec3: - """ - A float 3D vector. - - >>> v = vec3(1, 1, 0) - >>> w = vec3(0, 1, 1) - >>> v.length - 1.4142135623730951 - >>> v.dot(w) - 1.0 - >>> v.cross(w) - vec4(1.00, 1.00, 1.00) - >>> v + w - vec4(1.00, 2.00, 1.00) - >>> w - v - vec4(-1.00, 0.00, 1.00) - - """ - - def __init__(vec3 self, *args): - """ - Create a vec3. - - Accepts any number of parameters between 0 and 3 to fill the vector from the left. - """ - - length = len(args) - - if length == 1 and isinstance(args[0], (list, tuple)): - args = args[0] - length = len(args) - - if length > 3: - raise MathError("Attempt to initialize a vec3 with %d arguments." %(length)) - - for i in range(3): - if i < length: - self.v[i] = args[i] - else: - self.v[i] = 0.0 - - def __getitem__(vec3 self, int i): - if i >= 3 or i < 0: - raise IndexError("element index out of range(3)") - - return self.v[i] - - def __setitem__(vec3 self, int i, float value): - if i >= 3 or i < 0: - raise IndexError("element index out of range(3)") - - self.v[i] = value - - def __repr__(vec3 self): - return "vec3(%.2f, %.2f, %.2f)" %(self.v[0], self.v[1], self.v[2]) - - def __getstate__(vec3 self): - return (self.v[0], self.v[1], self.v[2]) - - def __setstate__(vec3 self, state): - self.v[0] = state[0] - self.v[1] = state[1] - self.v[2] = state[2] - - @property - def length(vec3 self): - """Contains the geometric length of the vector.""" - - return sqrt(self.v[0]**2 + self.v[1]**2 + self.v[2]**2) - - def normalized(vec3 self): - """Returns this vector, normalized.""" - - length = self.length - - return vec3(self.v[0] / length, self.v[1] / length, self.v[2] / length) - - def __add__(vec3 L, vec3 R): - return vec3(L.v[0] + R.v[0], L.v[1] + R.v[1], L.v[2] + R.v[2]) - - def __sub__(vec3 L, vec3 R): - return vec3(L.v[0] - R.v[0], L.v[1] - R.v[1], L.v[2] - R.v[2]) - - def __neg__(vec3 self): - return vec3(-self.v[0], -self.v[1], -self.v[2]) - - def dot(vec3 L, vec3 R): - """ - Returns the dot product of the two vectors. - - E.g. u.dot(v) -> u . v - """ - - return L.v[0] * R.v[0] + L.v[1] * R.v[1] + L.v[2] * R.v[2] - - def cross(vec3 L, vec3 R): - """ - Returns the cross product of the two vectors. - - E.g. u.cross(v) -> u x v - - """ - - return vec3(L.v[1]*R.v[2] - L.v[2]*R.v[1], L.v[0]*R.v[2] - L.v[2]*R.v[0], L.v[0]*R.v[1] - L.v[1]*R.v[0]) - - def __mul__(vec3 L, R): - """ - Multiplication of a vec3 by a float. - - The float has to be on the right. - """ - - return vec3(L.v[0] * R, L.v[1] * R, L.v[2] * R) diff --git a/m/m.mat4.pyx b/m/python_m.py similarity index 81% rename from m/m.mat4.pyx rename to m/python_m.py index 76254c8..d29bb01 100644 --- a/m/m.mat4.pyx +++ b/m/python_m.py @@ -1,4 +1,15 @@ -cdef class mat4: +#! /bin/env python3 +# encoding: utf-8 + +""" +Vector And Matrix Math + +Pure Python implementation. +""" + +from math import sin, cos, sqrt + +class mat4: """ A float 4x4 matrix. @@ -35,6 +46,8 @@ cdef class mat4: matrix from the upper left corner going down (column-wise). """ + self.m = + length = len(args) if length == 1 and isinstance(args[0], (list, tuple)): @@ -330,3 +343,117 @@ cdef class mat4: lines.append(" %.1f %.1f %.1f %.1f)" %(self.m[3], self.m[7], self.m[11], self.m[15])) return "\n".join(lines) + +cdef class vec3: + """ + A float 3D vector. + + >>> v = vec3(1, 1, 0) + >>> w = vec3(0, 1, 1) + >>> v.length + 1.4142135623730951 + >>> v.dot(w) + 1.0 + >>> v.cross(w) + vec4(1.00, 1.00, 1.00) + >>> v + w + vec4(1.00, 2.00, 1.00) + >>> w - v + vec4(-1.00, 0.00, 1.00) + + """ + + def __init__(vec3 self, *args): + """ + Create a vec3. + + Accepts any number of parameters between 0 and 3 to fill the vector from the left. + """ + + length = len(args) + + if length == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + length = len(args) + + if length > 3: + raise MathError("Attempt to initialize a vec3 with %d arguments." %(length)) + + for i in range(3): + if i < length: + self.v[i] = args[i] + else: + self.v[i] = 0.0 + + def __getitem__(vec3 self, int i): + if i >= 3 or i < 0: + raise IndexError("element index out of range(3)") + + return self.v[i] + + def __setitem__(vec3 self, int i, float value): + if i >= 3 or i < 0: + raise IndexError("element index out of range(3)") + + self.v[i] = value + + def __repr__(vec3 self): + return "vec3(%.2f, %.2f, %.2f)" %(self.v[0], self.v[1], self.v[2]) + + def __getstate__(vec3 self): + return (self.v[0], self.v[1], self.v[2]) + + def __setstate__(vec3 self, state): + self.v[0] = state[0] + self.v[1] = state[1] + self.v[2] = state[2] + + @property + def length(vec3 self): + """Contains the geometric length of the vector.""" + + return sqrt(self.v[0]**2 + self.v[1]**2 + self.v[2]**2) + + def normalized(vec3 self): + """Returns this vector, normalized.""" + + length = self.length + + return vec3(self.v[0] / length, self.v[1] / length, self.v[2] / length) + + def __add__(vec3 L, vec3 R): + return vec3(L.v[0] + R.v[0], L.v[1] + R.v[1], L.v[2] + R.v[2]) + + def __sub__(vec3 L, vec3 R): + return vec3(L.v[0] - R.v[0], L.v[1] - R.v[1], L.v[2] - R.v[2]) + + def __neg__(vec3 self): + return vec3(-self.v[0], -self.v[1], -self.v[2]) + + def dot(vec3 L, vec3 R): + """ + Returns the dot product of the two vectors. + + E.g. u.dot(v) -> u . v + """ + + return L.v[0] * R.v[0] + L.v[1] * R.v[1] + L.v[2] * R.v[2] + + def cross(vec3 L, vec3 R): + """ + Returns the cross product of the two vectors. + + E.g. u.cross(v) -> u x v + + """ + + return vec3(L.v[1]*R.v[2] - L.v[2]*R.v[1], L.v[0]*R.v[2] - L.v[2]*R.v[0], L.v[0]*R.v[1] - L.v[1]*R.v[0]) + + def __mul__(vec3 L, R): + """ + Multiplication of a vec3 by a float. + + The float has to be on the right. + """ + + return vec3(L.v[0] * R, L.v[1] * R, L.v[2] * R)