From cff94b97ca2e3a4b7396845a7a2fd1c9ab812d55 Mon Sep 17 00:00:00 2001 From: Tavian Barnes Date: Mon, 31 Oct 2011 17:59:56 -0400 Subject: Ship dimension client inside the Python package. --- .gitignore | 8 +- Makefile.am | 10 +- configure.ac | 2 +- dimension/Makefile.am | 12 +- dimension/__init__.py | 23 + dimension/client.py.in | 240 +++++ dimension/dimension | 25 + dimension/dimension.in | 230 ----- dimension/tests/Makefile.am | 4 +- libdimension-python/Makefile.am | 22 +- libdimension-python/dimension.pxd | 397 -------- libdimension-python/dimension.pyx | 1590 --------------------------------- libdimension-python/tests/Makefile.am | 5 +- libdimension-python/wrapper.pxd | 397 ++++++++ libdimension-python/wrapper.pyx | 1590 +++++++++++++++++++++++++++++++++ 15 files changed, 2315 insertions(+), 2240 deletions(-) create mode 100644 dimension/__init__.py create mode 100644 dimension/client.py.in create mode 100755 dimension/dimension delete mode 100644 dimension/dimension.in delete mode 100644 libdimension-python/dimension.pxd delete mode 100644 libdimension-python/dimension.pyx create mode 100644 libdimension-python/wrapper.pxd create mode 100644 libdimension-python/wrapper.pyx diff --git a/.gitignore b/.gitignore index 6c39551..8394788 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Files and folders created by ./autogen.sh, ./configure, and make +# Files and folders created by ./autogen.sh, and ./configure /aclocal.m4 /autom4te.cache/ /compile @@ -13,8 +13,6 @@ /py-compile Makefile Makefile.in -libdimension-python/dimension.c -dimension/dimension # pkg-config files *.pc @@ -38,6 +36,10 @@ Doxyfile # Files and folders created by compilation .deps/ *.[oa] +libdimension-python/wrapper.c +dimension/client.py +dimension/wrapper.so +__pycache__ # Core dumps core diff --git a/Makefile.am b/Makefile.am index b30def4..f455fe3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -18,12 +18,20 @@ ########################################################################### ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = libdimension \ +SUBDIRS = . \ + libdimension \ libdimension-python \ dimension EXTRA_DIST = autogen.sh +# Set up ./dimension as a python package for tests +$(abs_builddir)/dimension/__init__.py: + ln -sf "$(abs_top_srcdir)/dimension/__init__.py" dimension/__init__.py +$(abs_builddir)/dimension/wrapper.so: + ln -sf ../libdimension-python/.libs/wrapper.so dimension/wrapper.so +all-local: $(abs_builddir)/dimension/__init__.py $(abs_builddir)/dimension/wrapper.so + bench: cd libdimension && $(MAKE) $(AM_MAKEFLAGS) bench diff --git a/configure.ac b/configure.ac index d763041..bafc63e 100644 --- a/configure.ac +++ b/configure.ac @@ -213,6 +213,7 @@ dnl Generate Makefiles AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_FILES([Makefile dimension/Makefile + dimension/client.py dimension/tests/Makefile libdimension/Makefile libdimension/Doxyfile @@ -221,5 +222,4 @@ AC_CONFIG_FILES([Makefile libdimension/tests/Makefile libdimension-python/Makefile libdimension-python/tests/Makefile]) -AC_CONFIG_FILES([dimension/dimension], [chmod +x dimension/dimension]) AC_OUTPUT diff --git a/dimension/Makefile.am b/dimension/Makefile.am index cd0603f..c12a6b7 100644 --- a/dimension/Makefile.am +++ b/dimension/Makefile.am @@ -20,4 +20,14 @@ SUBDIRS = . \ tests -bin_SCRIPTS = dimension +dist_bin_SCRIPTS = dimension + +# make distcheck fails on the client because Python cannot find the module, +# since it's not really installed, so disable the --help and --version checks +AM_INSTALLCHECK_STD_OPTIONS_EXEMPT = dimension + +pkgpython_PYTHON = __init__.py +nodist_pkgpython_PYTHON = client.py + +clean-local: + rm -rf __pycache__/ diff --git a/dimension/__init__.py b/dimension/__init__.py new file mode 100644 index 0000000..0fb85c4 --- /dev/null +++ b/dimension/__init__.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +######################################################################### +# Copyright (C) 2011 Tavian Barnes # +# # +# This file is part of Dimension. # +# # +# Dimension is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the # +# Free Software Foundation; either version 3 of the License, or (at # +# your option) any later version. # +# # +# Dimension is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # +# General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +######################################################################### + +# Import everything from the Cython wrapper +from .wrapper import * diff --git a/dimension/client.py.in b/dimension/client.py.in new file mode 100644 index 0000000..4881f62 --- /dev/null +++ b/dimension/client.py.in @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 + +######################################################################### +# Copyright (C) 2011 Tavian Barnes # +# # +# This file is part of Dimension. # +# # +# Dimension is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the # +# Free Software Foundation; either version 3 of the License, or (at # +# your option) any later version. # +# # +# Dimension is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # +# General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +######################################################################### + +import argparse +import re +import os +import sys +from contextlib import contextmanager +from dimension import * + +def main(): + """Invoke the client from the command line.""" + + # Parse the command line + parser = DimensionArgumentParser( + epilog = "@PACKAGE_STRING@\n" + "@PACKAGE_URL@\n" + "Copyright (C) 2009-2011 Tavian Barnes <@PACKAGE_BUGREPORT@>\n" + "Licensed under the GNU General Public License", + formatter_class = argparse.RawDescriptionHelpFormatter, + conflict_handler = "resolve", # For -h as height instead of help + ) + + parser.add_argument("-V", "--version", action = "version", + version = "@PACKAGE_STRING@") + + parser.add_argument("-w", "--width", action = "store", type = int, + default = 768, + help = "image width (default: %(default)s)") + parser.add_argument("-h", "--height", action = "store", type = int, + default = 480, + help = "image height (default: %(default)s)") + parser.add_argument("--region", action = "store", type = str, + help = "subregion to render, as \"(x1, y1)->(x2, y2)\"") + + parser.add_argument("-v", "--verbose", action = "store_true", + help = "print more information") + parser.add_argument("-q", "--quiet", action = "store_true", + help = "print less information") + + parser.add_argument("--threads", action = "store", type = int, + help = "the number of threads to render with") + parser.add_argument("--quality", action = "store", type = str, + help = "the scene quality") + parser.add_argument("--adc-bailout", action = "store", type = str, + help = "the ADC bailout (default: 1/255)") + + parser.add_argument("-o", "--output", action = "store", type = str, + help = "the output image file") + parser.add_argument("input", action = "store", type = str, + help = "the input scene description file") + + # Debugging/testing options + parser.add_argument("--strict", action = "store_true", + help = argparse.SUPPRESS) + + args = parser.parse_args() + + # Calculate subregion + calculate_subregion(args) + + # Default output is basename(input).png + if args.output is None: + noext = os.path.splitext(os.path.basename(args.input))[0] + args.output = noext + ".png" + + # Handle the --strict option + die_on_warnings(args.strict) + + # Sandbox dictionary for the scene + sandbox = { } + sandbox.update(__import__("dimension").__dict__) + sandbox.update(__import__("math").__dict__) + + # Defaults/available variables + sandbox.update({ + "image_width" : args.width, + "image_height" : args.height, + "objects" : [], + "lights" : [], + "camera" : PerspectiveCamera(), + "default_texture" : Texture(finish = Ambient(0.1) + Diffuse(0.7)), + "default_interior" : Interior(), + "background" : Black, + "recursion_limit" : None, + }) + + # Execute the input script + if not args.quiet: + print("Parsing scene ...") + + # Run with the script's dirname as the working directory + workdir = os.path.dirname(os.path.abspath(args.input)) + + parse_timer = Timer() + with open(args.input) as fh, working_directory(workdir): + exec(compile(fh.read(), args.input, "exec"), sandbox) + parse_timer.stop() + + # Make the canvas + canvas = Canvas(width = args.region_width, height = args.region_height) + canvas.optimize_PNG() + + # Make the scene object + scene = Scene(canvas = canvas, + objects = sandbox["objects"], + lights = sandbox["lights"], + camera = sandbox["camera"]) + scene.region_x = args.region_x + scene.region_y = args.region_y + scene.outer_width = args.width + scene.outer_height = args.height + scene.default_texture = sandbox["default_texture"] + scene.default_interior = sandbox["default_interior"] + scene.background = sandbox["background"] + if sandbox["recursion_limit"] is not None: + scene.recursion_limit = sandbox["recursion_limit"] + if args.threads is not None: + scene.nthreads = args.threads + if args.quality is not None: + scene.quality = args.quality + if args.adc_bailout is not None: + pattern = r"^(.*)/(.*)$" + match = re.match(pattern, args.adc_bailout) + if match is not None: + args.adc_bailout = float(match.group(1))/float(match.group(2)) + scene.adc_bailout = float(args.adc_bailout) + + # Ray-trace the scene + future = scene.ray_trace_async() + if not args.quiet: + if scene.nthreads == 1: + render_message = "Rendering scene" + else: + render_message = "Rendering scene (using %d threads)" % scene.nthreads + progress_bar(render_message, future) + future.join() + + # Write the output file + export_timer = Timer() + future = canvas.write_PNG_async(args.output) + if not args.quiet: + progress_bar("Writing %s" % args.output, future) + future.join() + export_timer.stop() + + # Print execution times + if args.verbose: + print() + print("Parsing time: ", parse_timer) + print("Bounding time: ", scene.bounding_timer) + print("Rendering time: ", scene.render_timer) + print("Exporting time: ", export_timer) + +class DimensionArgumentParser(argparse.ArgumentParser): + """ + Specialized parser to print --version output to stdout rather than stderr. + """ + def exit(self, status = 0, message = None): + if message: + file = sys.stdout if status == 0 else sys.stderr + file.write(message) + sys.exit(status) + +def calculate_subregion(args): + if args.region is None: + args.region_x = 0 + args.region_y = 0 + args.region_width = args.width + args.region_height = args.height + else: + pattern = r"^\s*\(\s*(\d*)\s*,\s*(\d*)\s*\)\s*->\s*\(\s*(\d*)\s*,\s*(\d*)\s*\)\s*$" + match = re.match(pattern, args.region) + if match is None: + raise RuntimeError("range specified in invalid format.") + args.region_x = int(match.group(1)) + args.region_y = int(match.group(2)) + region_xmax = int(match.group(3)) + region_ymax = int(match.group(4)) + args.region_width = region_xmax - args.region_x + args.region_height = region_ymax - args.region_y + if args.region_width <= 0 or args.region_height <= 0: + raise RuntimeError("region is degenerate.") + if region_xmax >= args.width or region_ymax > args.height: + raise RuntimeError("region exceeds bounds of image.") + +@contextmanager +def working_directory(newwd): + """Change the working directory within a with statement.""" + oldwd = os.getcwd() + try: + os.chdir(newwd) + yield + finally: + os.chdir(oldwd) + +def progress_bar(str, future): + """Display a progress bar while a Future completes.""" + try: + print(str, end = " ") + sys.stdout.flush() + + term_width = terminal_width() + width = term_width - (len(str) + 1)%term_width + for i in range(width): + future.wait((i + 1)/width) + print(".", end = "") + sys.stdout.flush() + + print() + sys.stdout.flush() + except KeyboardInterrupt: + print() + sys.stdout.flush() + + future.cancel() + try: + future.join() + except RuntimeError: + # Swallow the failure exception + pass + raise diff --git a/dimension/dimension b/dimension/dimension new file mode 100755 index 0000000..1dab118 --- /dev/null +++ b/dimension/dimension @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +######################################################################### +# Copyright (C) 2011 Tavian Barnes # +# # +# This file is part of Dimension. # +# # +# Dimension is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the # +# Free Software Foundation; either version 3 of the License, or (at # +# your option) any later version. # +# # +# Dimension is distributed in the hope that it will be useful, but # +# WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # +# General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +######################################################################### + +from dimension.client import main + +if __name__ == "__main__": + main() diff --git a/dimension/dimension.in b/dimension/dimension.in deleted file mode 100644 index cd2e82d..0000000 --- a/dimension/dimension.in +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env python3 - -######################################################################### -# Copyright (C) 2011 Tavian Barnes # -# # -# This file is part of Dimension. # -# # -# Dimension is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the # -# Free Software Foundation; either version 3 of the License, or (at # -# your option) any later version. # -# # -# Dimension is distributed in the hope that it will be useful, but # -# WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # -# General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -######################################################################### - -import argparse -import re -import os -import sys -from contextlib import contextmanager - -# Specialized parser to print --version output to stdout rather than stderr, -# to pass distcheck -class DimensionArgumentParser(argparse.ArgumentParser): - def exit(self, status = 0, message = None): - if message: - file = sys.stdout if status == 0 else sys.stderr - file.write(message) - sys.exit(status) - -# Change the working directory within a with statement -@contextmanager -def working_directory(newwd): - oldwd = os.getcwd() - try: - os.chdir(newwd) - yield - finally: - os.chdir(oldwd) - -# Parse the command line -parser = DimensionArgumentParser( - epilog = "@PACKAGE_STRING@\n" - "@PACKAGE_URL@\n" - "Copyright (C) 2009-2011 Tavian Barnes <@PACKAGE_BUGREPORT@>\n" - "Licensed under the GNU General Public License", - formatter_class = argparse.RawDescriptionHelpFormatter, - conflict_handler = "resolve", # For -h as height instead of help -) - -parser.add_argument("-V", "--version", action = "version", - version = "@PACKAGE_STRING@") - -parser.add_argument("-w", "--width", action = "store", type = int, - default = 768, - help = "image width (default: %(default)s)") -parser.add_argument("-h", "--height", action = "store", type = int, - default = 480, - help = "image height (default: %(default)s)") -parser.add_argument("--region", action = "store", type = str, - help = "subregion to render, as \"(x1, y1)->(x2, y2)\"") - -parser.add_argument("-v", "--verbose", action = "store_true", - help = "print more information") -parser.add_argument("-q", "--quiet", action = "store_true", - help = "print less information") - -parser.add_argument("--threads", action = "store", type = int, - help = "the number of threads to render with") -parser.add_argument("--quality", action = "store", type = str, - help = "the scene quality") -parser.add_argument("--adc-bailout", action = "store", type = str, - help = "the ADC bailout (default: 1/255)") - -parser.add_argument("-o", "--output", action = "store", type = str, - help = "the output image file") -parser.add_argument("input", action = "store", type = str, - help = "the input scene description file") - -# Debugging/testing options -parser.add_argument("--strict", action = "store_true", - help = argparse.SUPPRESS) - -args = parser.parse_args() - -from dimension import * - -# Calculate subregion -if args.region is None: - args.region_x = 0 - args.region_y = 0 - args.region_width = args.width - args.region_height = args.height -else: - pattern = r"^\s*\(\s*(\d*)\s*,\s*(\d*)\s*\)\s*->\s*\(\s*(\d*)\s*,\s*(\d*)\s*\)\s*$" - match = re.match(pattern, args.region) - if match is None: - raise RuntimeError("range specified in invalid format.") - args.region_x = int(match.group(1)) - args.region_y = int(match.group(2)) - region_xmax = int(match.group(3)) - region_ymax = int(match.group(4)) - args.region_width = region_xmax - args.region_x - args.region_height = region_ymax - args.region_y - if args.region_width <= 0 or args.region_height <= 0: - raise RuntimeError("region is degenerate.") - if region_xmax >= args.width or region_ymax > args.height: - raise RuntimeError("region exceeds bounds of image.") - -# Default output is basename(input).png -if args.output is None: - noext = os.path.splitext(os.path.basename(args.input))[0] - args.output = noext + ".png" - -# Display a progress bar -def progress_bar(str, future): - try: - if not args.quiet: - print(str, end = " ") - sys.stdout.flush() - - term_width = terminal_width() - width = term_width - (len(str) + 1)%term_width - for i in range(width): - future.wait((i + 1)/width) - print(".", end = "") - sys.stdout.flush() - - print() - sys.stdout.flush() - - future.join() - except KeyboardInterrupt: - print() - sys.stdout.flush() - - future.cancel() - try: - future.join() - except RuntimeError: - # Swallow the failure exception - pass - raise - -# --strict option -die_on_warnings(args.strict) - -# Sandbox dictionary for scene -sandbox = __import__("dimension").__dict__ -sandbox.update(__import__("math").__dict__) - -# Defaults/available variables -sandbox.update({ - "image_width" : args.width, - "image_height" : args.height, - "objects" : [], - "lights" : [], - "camera" : PerspectiveCamera(), - "default_texture" : Texture(finish = Ambient(0.1) + Diffuse(0.7)), - "default_interior" : Interior(), - "background" : Black, - "recursion_limit" : None, -}) - -# Execute the input script -if not args.quiet: - print("Parsing scene ...") - -# Run with the script's dirname as the working directory -workdir = os.path.dirname(os.path.abspath(args.input)) - -parse_timer = Timer() -with open(args.input) as fh, working_directory(workdir): - exec(compile(fh.read(), args.input, "exec"), sandbox) -parse_timer.stop() - -# Make the canvas -canvas = Canvas(width = args.region_width, height = args.region_height) -canvas.optimize_PNG() - -# Make the scene object -scene = Scene(canvas = canvas, - objects = sandbox["objects"], - lights = sandbox["lights"], - camera = sandbox["camera"]) -scene.region_x = args.region_x -scene.region_y = args.region_y -scene.outer_width = args.width -scene.outer_height = args.height -scene.default_texture = sandbox["default_texture"] -scene.default_interior = sandbox["default_interior"] -scene.background = sandbox["background"] -if sandbox["recursion_limit"] is not None: - scene.recursion_limit = sandbox["recursion_limit"] -if args.threads is not None: - scene.nthreads = args.threads -if args.quality is not None: - scene.quality = args.quality -if args.adc_bailout is not None: - pattern = r"^(.*)/(.*)" - match = re.match(pattern, args.adc_bailout) - if match is not None: - args.adc_bailout = float(match.group(1))/float(match.group(2)) - scene.adc_bailout = float(args.adc_bailout) - -# Ray-trace the scene -if scene.nthreads == 1: - render_message = "Rendering scene" -else: - render_message = "Rendering scene (using %d threads)" % scene.nthreads -progress_bar(render_message, scene.ray_trace_async()) - -# Write the output file -export_timer = Timer() -progress_bar("Writing %s" % args.output, canvas.write_PNG_async(args.output)) -export_timer.stop() - -# Print execution times -if args.verbose: - print() - print("Parsing time: ", parse_timer) - print("Bounding time: ", scene.bounding_timer) - print("Rendering time: ", scene.render_timer) - print("Exporting time: ", export_timer) diff --git a/dimension/tests/Makefile.am b/dimension/tests/Makefile.am index 147df24..fe224fa 100644 --- a/dimension/tests/Makefile.am +++ b/dimension/tests/Makefile.am @@ -20,9 +20,9 @@ TESTS = demo.dmnsn \ complex.dmnsn TEST_EXTENSIONS = .dmnsn -DMNSN_LOG_COMPILER = $(top_builddir)/dimension/dimension +DMNSN_LOG_COMPILER = $(top_srcdir)/dimension/dimension AM_DMNSN_LOG_FLAGS = --strict -TESTS_ENVIRONMENT = PYTHONPATH=$(top_builddir)/libdimension-python/.libs +TESTS_ENVIRONMENT = PYTHONPATH=$(abs_top_builddir) EXTRA_DIST = $(TESTS) diff --git a/libdimension-python/Makefile.am b/libdimension-python/Makefile.am index 5a8b528..33e93a5 100644 --- a/libdimension-python/Makefile.am +++ b/libdimension-python/Makefile.am @@ -25,22 +25,22 @@ INCLUDES = -I$(top_srcdir)/libdimension AM_CFLAGS = $(Python_CFLAGS) AM_LDFLAGS = $(Python_LDFLAGS) -dimension.c: dimension.pyx dimension.pxd - cython --line-directives dimension.pyx +wrapper.c: wrapper.pyx wrapper.pxd + cython --line-directives wrapper.pyx -pyexec_LTLIBRARIES = dimension.la -dimension_la_SOURCES = dimension.c \ - platform.c \ - platform.h -dimension_la_LDFLAGS = -avoid-version -module -dimension_la_LIBADD = $(top_builddir)/libdimension/libdimension.la +pkgpyexec_LTLIBRARIES = wrapper.la +wrapper_la_SOURCES = wrapper.c \ + platform.c \ + platform.h +wrapper_la_LDFLAGS = -avoid-version -module +wrapper_la_LIBADD = $(top_builddir)/libdimension/libdimension.la -EXTRA_DIST = dimension.pyx \ - dimension.pxd +EXTRA_DIST = wrapper.pyx \ + wrapper.pxd doc: mkdir -p doc - PYTHONPATH=$(top_builddir)/libdimension-python/.libs pydoc -w dimension + PYTHONPATH=$(top_builddir) pydoc -w dimension mv dimension.html doc/ clean-doc: rm -rf doc diff --git a/libdimension-python/dimension.pxd b/libdimension-python/dimension.pxd deleted file mode 100644 index 7ba17e9..0000000 --- a/libdimension-python/dimension.pxd +++ /dev/null @@ -1,397 +0,0 @@ -######################################################################### -# Copyright (C) 2011 Tavian Barnes # -# # -# This file is part of The Dimension Python Module. # -# # -# The Dimension Python Module is free software; you can redistribute it # -# and/or modify it under the terms of the GNU Lesser General Public # -# License as published by the Free Software Foundation; either version # -# 3 of the License, or (at your option) any later version. # -# # -# The Dimension Python Module is distributed in the hope that it will # -# be useful, but WITHOUT ANY WARRANTY; without even the implied # -# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # -# the GNU Lesser General Public License for more details. # -# # -# You should have received a copy of the GNU Lesser General Public # -# License along with this program. If not, see # -# . # -######################################################################### - -from cpython cimport bool -from libc.math cimport * -from libc.stdio cimport * - -cdef extern from "errno.h": - int errno - -cdef extern from "../libdimension/dimension.h": - - ########### - # Globals # - ########### - - ctypedef void dmnsn_callback_fn(void *ptr) - - void DMNSN_INCREF(void *) - - void dmnsn_die_on_warnings(bint always_die) - - double dmnsn_epsilon - - ########## - # Arrays # - ########## - - ctypedef struct dmnsn_array: - pass - - dmnsn_array *dmnsn_new_array(size_t objsize) - void dmnsn_delete_array(dmnsn_array *array) - - size_t dmnsn_array_size(dmnsn_array *array) - void dmnsn_array_resize(dmnsn_array *array, size_t length) - dmnsn_array *dmnsn_array_copy(dmnsn_array *array) - dmnsn_array *dmnsn_array_split(dmnsn_array *array) - void dmnsn_array_get(dmnsn_array *array, size_t i, void *obj) - void dmnsn_array_set(dmnsn_array *array, size_t i, void *obj) - void *dmnsn_array_first(dmnsn_array *array) - void *dmnsn_array_last(dmnsn_array *array) - void *dmnsn_array_at(dmnsn_array *array, size_t i) - void dmnsn_array_push(dmnsn_array *array, void *obj) - void dmnsn_array_pop(dmnsn_array *array, void *obj) - void dmnsn_array_insert(dmnsn_array *array, size_t i, void *obj) - void dmnsn_array_remove(dmnsn_array *array, size_t i) - void dmnsn_array_apply(dmnsn_array *array, dmnsn_callback_fn *callback) - - ########### - # Futures # - ########### - - ctypedef struct dmnsn_future - - int dmnsn_future_join(dmnsn_future *future) - void dmnsn_future_cancel(dmnsn_future *future) - double dmnsn_future_progress(dmnsn_future *future) - void dmnsn_future_wait(dmnsn_future *future, double progress) - - ########## - # Timers # - ########## - - ctypedef struct dmnsn_timer: - double real - double user - double system - - void dmnsn_timer_start(dmnsn_timer *timer) - void dmnsn_timer_stop(dmnsn_timer *timer) - - ############ - # Geometry # - ############ - - double dmnsn_radians(double degrees) - double dmnsn_degrees(double radians) - - ctypedef struct dmnsn_vector: - double x - double y - double z - - dmnsn_vector dmnsn_new_vector(double x, double y, double z) - - dmnsn_vector dmnsn_vector_negate(dmnsn_vector rhs) - - dmnsn_vector dmnsn_vector_add(dmnsn_vector lhs, dmnsn_vector rhs) - dmnsn_vector dmnsn_vector_sub(dmnsn_vector lhs, dmnsn_vector rhs) - dmnsn_vector dmnsn_vector_mul(double lhs, dmnsn_vector rhs) - dmnsn_vector dmnsn_vector_div(dmnsn_vector lhs, double rhs) - - dmnsn_vector dmnsn_vector_cross(dmnsn_vector lhs, dmnsn_vector rhs) - double dmnsn_vector_dot(dmnsn_vector lhs, dmnsn_vector rhs) - dmnsn_vector dmnsn_vector_proj(dmnsn_vector u, dmnsn_vector d) - - double dmnsn_vector_norm(dmnsn_vector v) - dmnsn_vector dmnsn_vector_normalized(dmnsn_vector v) - - dmnsn_vector dmnsn_zero - dmnsn_vector dmnsn_x - dmnsn_vector dmnsn_y - dmnsn_vector dmnsn_z - - ctypedef struct dmnsn_matrix: - double n[3][4] - - dmnsn_matrix dmnsn_new_matrix(double a1, double a2, double a3, double a4, - double b1, double b2, double b3, double b4, - double c1, double c2, double c3, double c4) - - dmnsn_matrix dmnsn_matrix_inverse(dmnsn_matrix m) - - dmnsn_matrix dmnsn_matrix_mul(dmnsn_matrix lhs, dmnsn_matrix rhs) - dmnsn_vector dmnsn_transform_point(dmnsn_matrix lhs, dmnsn_vector rhs) - dmnsn_vector dmnsn_transform_direction(dmnsn_matrix lhs, dmnsn_vector rhs) - dmnsn_vector dmnsn_transform_normal(dmnsn_matrix lhs, dmnsn_vector rhs) - - dmnsn_matrix dmnsn_scale_matrix(dmnsn_vector s) - dmnsn_matrix dmnsn_translation_matrix(dmnsn_vector d) - dmnsn_matrix dmnsn_rotation_matrix(dmnsn_vector theta) - dmnsn_matrix dmnsn_alignment_matrix(dmnsn_vector frm, dmnsn_vector to, - dmnsn_vector axis1, dmnsn_vector axis2) - - ########## - # Colors # - ########## - - ctypedef struct dmnsn_color: - double R - double G - double B - double trans - double filter - - dmnsn_color dmnsn_new_color(double R, double G, double B) - dmnsn_color dmnsn_new_color5(double R, double G, double B, - double trans, double filter) - - dmnsn_color dmnsn_color_from_sRGB(dmnsn_color color) - dmnsn_color dmnsn_color_to_sRGB(dmnsn_color color) - - double dmnsn_color_intensity(dmnsn_color color) - dmnsn_color dmnsn_color_add(dmnsn_color color1, dmnsn_color color2) - dmnsn_color dmnsn_color_mul(double n, dmnsn_color color) - - bint dmnsn_color_is_black(dmnsn_color color) - - dmnsn_color dmnsn_black - dmnsn_color dmnsn_white - dmnsn_color dmnsn_clear - dmnsn_color dmnsn_red - dmnsn_color dmnsn_green - dmnsn_color dmnsn_blue - dmnsn_color dmnsn_magenta - dmnsn_color dmnsn_orange - dmnsn_color dmnsn_yellow - dmnsn_color dmnsn_cyan - - ############ - # Canvases # - ############ - - ctypedef struct dmnsn_canvas: - size_t width - size_t height - - dmnsn_canvas *dmnsn_new_canvas(size_t width, size_t height) - void dmnsn_delete_canvas(dmnsn_canvas *canvas) - - dmnsn_color dmnsn_canvas_get_pixel(dmnsn_canvas *canvas, size_t x, size_t y) - void dmnsn_canvas_set_pixel(dmnsn_canvas *canvas, size_t x, size_t y, - dmnsn_color color) - - void dmnsn_canvas_clear(dmnsn_canvas *canvas, dmnsn_color color) - - int dmnsn_png_optimize_canvas(dmnsn_canvas *canvas) - int dmnsn_png_write_canvas(dmnsn_canvas *canvas, FILE *file) - dmnsn_future *dmnsn_png_write_canvas_async(dmnsn_canvas *canvas, FILE *file) - dmnsn_canvas *dmnsn_png_read_canvas(FILE *file) - dmnsn_future *dmnsn_png_read_canvas_async(dmnsn_canvas **canvas, FILE *file) - - int dmnsn_gl_optimize_canvas(dmnsn_canvas *canvas) - int dmnsn_gl_write_canvas(dmnsn_canvas *canvas) - - ############ - # Patterns # - ############ - - ctypedef struct dmnsn_pattern: - pass - - void dmnsn_delete_pattern(dmnsn_pattern *pattern) - - dmnsn_pattern *dmnsn_new_checker_pattern() - dmnsn_pattern *dmnsn_new_gradient_pattern(dmnsn_vector orientation) - dmnsn_pattern *dmnsn_new_leopard_pattern() - - ######## - # Maps # - ######## - - ctypedef struct dmnsn_map: - pass - - void dmnsn_delete_map(dmnsn_map *map) - - void dmnsn_map_add_entry(dmnsn_map *map, double n, void *obj) - size_t dmnsn_map_size(dmnsn_map *map) - - dmnsn_map *dmnsn_new_pigment_map() - - ############ - # Pigments # - ############ - - ctypedef struct dmnsn_pigment: - dmnsn_matrix trans - dmnsn_color quick_color - - ctypedef enum dmnsn_pigment_map_flags: - DMNSN_PIGMENT_MAP_REGULAR - DMNSN_PIGMENT_MAP_SRGB - - void dmnsn_delete_pigment(dmnsn_pigment *pigment) - - dmnsn_pigment *dmnsn_new_solid_pigment(dmnsn_color color) - dmnsn_pigment *dmnsn_new_canvas_pigment(dmnsn_canvas *canvas) - dmnsn_pigment *dmnsn_new_pigment_map_pigment(dmnsn_pattern *pattern, - dmnsn_map *map, - dmnsn_pigment_map_flags flags) - - ############ - # Finishes # - ############ - - ctypedef struct dmnsn_ambient - ctypedef struct dmnsn_diffuse - ctypedef struct dmnsn_specular - ctypedef struct dmnsn_reflection - - ctypedef struct dmnsn_finish: - dmnsn_ambient *ambient - dmnsn_diffuse *diffuse - dmnsn_specular *specular - dmnsn_reflection *reflection - - dmnsn_finish dmnsn_new_finish() - void dmnsn_delete_finish(dmnsn_finish finish) - - void dmnsn_finish_incref(dmnsn_finish *finish) - - void dmnsn_finish_cascade(dmnsn_finish *default_finish, dmnsn_finish *finish) - - dmnsn_ambient *dmnsn_new_basic_ambient(dmnsn_color ambient) - dmnsn_diffuse *dmnsn_new_lambertian(double diffuse) - dmnsn_specular *dmnsn_new_phong(double specular, double exp) - dmnsn_reflection *dmnsn_new_basic_reflection(dmnsn_color min, dmnsn_color max, - double falloff) - - ############ - # Textures # - ############ - - ctypedef struct dmnsn_texture: - dmnsn_pigment *pigment - dmnsn_finish finish - dmnsn_matrix trans - - dmnsn_texture *dmnsn_new_texture() - void dmnsn_delete_texture(dmnsn_texture *texture) - - void dmnsn_texture_cascade(dmnsn_texture *default_texture, - dmnsn_texture **texture) - - ############# - # Interiors # - ############# - - ctypedef struct dmnsn_interior: - double ior - - dmnsn_interior *dmnsn_new_interior() - void dmnsn_delete_interior(dmnsn_interior *interior) - - ########### - # Objects # - ########### - - ctypedef struct dmnsn_object: - dmnsn_texture *texture - dmnsn_interior *interior - dmnsn_matrix trans - dmnsn_matrix intrinsic_trans - - dmnsn_object *dmnsn_new_object() - void dmnsn_delete_object(dmnsn_object *object) - - dmnsn_object *dmnsn_new_triangle(dmnsn_vector a, - dmnsn_vector b, - dmnsn_vector c) - dmnsn_object *dmnsn_new_plane(dmnsn_vector normal) - dmnsn_object *dmnsn_new_sphere() - dmnsn_object *dmnsn_new_cube() - dmnsn_object *dmnsn_new_cone(double r1, double r2, bint open) - dmnsn_object *dmnsn_new_torus(double major, double minor) - - dmnsn_object *dmnsn_new_csg_union(dmnsn_array *objects) - dmnsn_object *dmnsn_new_csg_intersection(dmnsn_object *A, dmnsn_object *B) - dmnsn_object *dmnsn_new_csg_difference(dmnsn_object *A, dmnsn_object *B) - dmnsn_object *dmnsn_new_csg_merge(dmnsn_object *A, dmnsn_object *B) - - ########## - # Lights # - ########## - - ctypedef struct dmnsn_light - - dmnsn_light *dmnsn_new_light() - void dmnsn_delete_light(dmnsn_light *light) - - dmnsn_light *dmnsn_new_point_light(dmnsn_vector x0, dmnsn_color color) - - ########### - # Cameras # - ########### - - ctypedef struct dmnsn_camera: - dmnsn_matrix trans - - dmnsn_camera *dmnsn_new_camera() - void dmnsn_delete_camera(dmnsn_camera *camera) - - dmnsn_camera *dmnsn_new_perspective_camera() - - ########## - # Scenes # - ########## - - ctypedef enum dmnsn_quality: - DMNSN_RENDER_NONE - DMNSN_RENDER_PIGMENT - DMNSN_RENDER_LIGHTS - DMNSN_RENDER_FINISH - DMNSN_RENDER_TRANSPARENCY - DMNSN_RENDER_REFLECTION - DMNSN_RENDER_FULL - - ctypedef struct dmnsn_scene: - dmnsn_pigment *background - dmnsn_texture *default_texture - dmnsn_interior *default_interior - - dmnsn_canvas *canvas - size_t region_x - size_t region_y - size_t outer_width - size_t outer_height - - dmnsn_array *objects - dmnsn_array *lights - dmnsn_camera *camera - - dmnsn_quality quality - unsigned int reclimit - double adc_bailout - unsigned int nthreads - - dmnsn_timer bounding_timer - dmnsn_timer render_timer - - dmnsn_scene *dmnsn_new_scene() - void dmnsn_delete_scene(dmnsn_scene *scene) - - void dmnsn_ray_trace(dmnsn_scene *scene) - dmnsn_future *dmnsn_ray_trace_async(dmnsn_scene *scene) - -cdef extern from "platform.h": - unsigned int dmnsn_terminal_width() diff --git a/libdimension-python/dimension.pyx b/libdimension-python/dimension.pyx deleted file mode 100644 index 738a63f..0000000 --- a/libdimension-python/dimension.pyx +++ /dev/null @@ -1,1590 +0,0 @@ -######################################################################### -# Copyright (C) 2011 Tavian Barnes # -# # -# This file is part of The Dimension Python Module. # -# # -# The Dimension Python Module is free software; you can redistribute it # -# and/or modify it under the terms of the GNU Lesser General Public # -# License as published by the Free Software Foundation; either version # -# 3 of the License, or (at your option) any later version. # -# # -# The Dimension Python Module is distributed in the hope that it will # -# be useful, but WITHOUT ANY WARRANTY; without even the implied # -# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # -# the GNU Lesser General Public License for more details. # -# # -# You should have received a copy of the GNU Lesser General Public # -# License along with this program. If not, see # -# . # -######################################################################### - -""" -Dimension: a high-performance photo-realistic 3D renderer. -""" - -import os - -########### -# Helpers # -########### - -cdef _raise_OSError(filename = None): - if filename is None: - raise OSError(errno, os.strerror(errno)) - else: - raise OSError(errno, os.strerror(errno), filename) - -########### -# Globals # -########### - -def die_on_warnings(always_die): - """Whether to treat Dimension warnings as errors.""" - dmnsn_die_on_warnings(always_die) - -def terminal_width(): - """Return the width of the terminal, if present.""" - return dmnsn_terminal_width() - -########### -# Futures # -########### - -cdef class Future: - cdef dmnsn_future *_future - cdef _finalizer - - def __cinit__(self): - self._future = NULL - self._finalizer = None - - def __init__(self): - raise RuntimeError("attempt to create a Future object.") - - def __dealloc__(self): - if self._future != NULL: - self.join() - - def join(self): - self._assert_unfinished() - try: - if dmnsn_future_join(self._future) != 0: - raise RuntimeError("background task failed.") - if self._finalizer is not None: - self._finalizer() - finally: - self._future = NULL - - def cancel(self): - self._assert_unfinished() - dmnsn_future_cancel(self._future) - - def progress(self): - self._assert_unfinished() - return dmnsn_future_progress(self._future) - - def wait(self, progress): - self._assert_unfinished() - dmnsn_future_wait(self._future, progress) - - def _assert_unfinished(self): - if self._future == NULL: - raise RuntimeError("background task finished.") - -cdef Future _Future(dmnsn_future *future): - """Wrap a Future object around an existing dmnsn_future *.""" - cdef Future self = Future.__new__(Future) - self._future = future - return self - -########## -# Timers # -########## - -cdef class Timer: - """A timer for Dimension tasks.""" - cdef dmnsn_timer _timer - cdef bool _stopped - - def __init__(self): - """ - Create a Timer. - - Timing starts as soon as the object is created. - """ - self._stopped = False - dmnsn_timer_start(&self._timer) - - def stop(self): - """Stop the Timer.""" - if self._stopped: - raise RuntimeError("timer already stopped.") - - dmnsn_timer_stop(&self._timer) - self._stopped = True - - property real: - """Real (wall clock) time.""" - def __get__(self): - self._assert_stopped() - return self._timer.real - property user: - """User (CPU) time.""" - def __get__(self): - self._assert_stopped() - return self._timer.user - property system: - """System time.""" - def __get__(self): - self._assert_stopped() - return self._timer.system - - def __str__(self): - self._assert_stopped() - return "%.2fs (user: %.2fs; system: %.2fs)" % \ - (self._timer.real, self._timer.user, self._timer.system) - - def _assert_stopped(self): - if not self._stopped: - raise RuntimeError("timer still running.") - -cdef Timer _Timer(dmnsn_timer timer): - """Wrap a Timer object around a dmnsn_timer.""" - cdef Timer self = Timer.__new__(Timer) - self._timer = timer - self._stopped = True - return self - -############ -# Geometry # -############ - -cdef class Vector: - """A vector (or point or pseudovector) in 3D space.""" - cdef dmnsn_vector _v - - def __init__(self, *args, **kwargs): - """ - Create a Vector. - - Keyword arguments: - x -- The x coordinate - y -- The y coordinate - z -- The z coordinate - - Alternatively, you can pass another Vector, the value 0, or a tuple or other - sequence (x, y, z). - """ - if len(args) == 1: - if isinstance(args[0], Vector): - self._v = (args[0])._v - elif hasattr(args[0], "__iter__"): # Faster than try: ... except: - self._real_init(*args[0]) - elif args[0] == 0: - self._v = dmnsn_zero - else: - raise TypeError("expected a sequence or 0") - else: - self._real_init(*args, **kwargs) - - def _real_init(self, double x, double y, double z): - self._v = dmnsn_new_vector(x, y, z) - - property x: - """The x coordinate.""" - def __get__(self): - return self._v.x - property y: - """The y coordinate.""" - def __get__(self): - return self._v.y - property z: - """The z coordinate.""" - def __get__(self): - return self._v.z - - def __pos__(self): - return self - def __neg__(self): - return _Vector(dmnsn_vector_negate(self._v)) - def __nonzero__(self): - return dmnsn_vector_norm(self._v) >= dmnsn_epsilon - - def __add__(lhs, rhs): - return _Vector(dmnsn_vector_add(Vector(lhs)._v, Vector(rhs)._v)) - def __sub__(lhs, rhs): - return _Vector(dmnsn_vector_sub(Vector(lhs)._v, Vector(rhs)._v)) - def __mul__(lhs, rhs): - if isinstance(lhs, Vector): - return _Vector(dmnsn_vector_mul(rhs, (lhs)._v)) - else: - return _Vector(dmnsn_vector_mul(lhs, (rhs)._v)) - def __truediv__(Vector lhs not None, double rhs): - return _Vector(dmnsn_vector_div(lhs._v, rhs)) - - def __richcmp__(lhs, rhs, int op): - equal = (Vector(lhs) - Vector(rhs)).norm() < dmnsn_epsilon - if op == 2: # == - return equal - elif op == 3: # != - return not equal - else: - return NotImplemented - - def norm(self): - """Return the magnitude of the vector.""" - return dmnsn_vector_norm(self._v) - def normalized(self): - """Return the direction of the vector.""" - return _Vector(dmnsn_vector_normalized(self._v)) - - def __repr__(self): - return "dimension.Vector(%r, %r, %r)" % (self.x, self.y, self.z) - - def __str__(self): - return "<%s, %s, %s>" % (self.x, self.y, self.z) - -cdef Vector _Vector(dmnsn_vector v): - """Wrap a Vector object around a dmnsn_vector.""" - cdef Vector self = Vector.__new__(Vector) - self._v = v - return self - -def cross(Vector lhs not None, Vector rhs not None): - """Vector cross product.""" - return _Vector(dmnsn_vector_cross(lhs._v, rhs._v)) -def dot(Vector lhs not None, Vector rhs not None): - """Vector dot product.""" - return dmnsn_vector_dot(lhs._v, rhs._v) -def proj(Vector u not None, Vector d not None): - """Vector projection (of u onto d).""" - return _Vector(dmnsn_vector_proj(u._v, d._v)) - -X = _Vector(dmnsn_x) -Y = _Vector(dmnsn_y) -Z = _Vector(dmnsn_z) - -cdef class Matrix: - """An affine transformation matrix.""" - cdef dmnsn_matrix _m - - def __init__(self, - double a1, double a2, double a3, double a4, - double b1, double b2, double b3, double b4, - double c1, double c2, double c3, double c4): - """Create a Matrix.""" - self._m = dmnsn_new_matrix(a1, a2, a3, a4, - b1, b2, b3, b4, - c1, c2, c3, c4) - - def __nonzero__(self): - cdef double sum = 0.0 - for i in range(3): - for j in range(4): - sum += self._m.n[i][j] - return sqrt(sum) >= dmnsn_epsilon - - def __mul__(Matrix lhs not None, rhs): - if isinstance(rhs, Matrix): - return _Matrix(dmnsn_matrix_mul(lhs._m, (rhs)._m)) - else: - return _Vector(dmnsn_transform_point(lhs._m, (rhs)._v)) - - def __richcmp__(Matrix lhs not None, Matrix rhs not None, int op): - cdef double sum = 0.0 - for i in range(3): - for j in range(4): - diff = lhs._m.n[i][j] - rhs._m.n[i][j] - sum += diff*diff - equal = sqrt(sum) < dmnsn_epsilon - - if op == 2: # == - return equal - elif op == 3: # != - return not equal - else: - return NotImplemented - - def inverse(self): - """Return the inverse of a matrix.""" - return _Matrix(dmnsn_matrix_inverse(self._m)) - - def __repr__(self): - return \ - "dimension.Matrix(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)" % \ - (self._m.n[0][0], self._m.n[0][1], self._m.n[0][2], self._m.n[0][3], - self._m.n[1][0], self._m.n[1][1], self._m.n[1][2], self._m.n[1][3], - self._m.n[2][0], self._m.n[2][1], self._m.n[2][2], self._m.n[2][3]) - - def __str__(self): - return \ - "\n[%s\t%s\t%s\t%s]" \ - "\n[%s\t%s\t%s\t%s]" \ - "\n[%s\t%s\t%s\t%s]" \ - "\n[%s\t%s\t%s\t%s]" %\ - (self._m.n[0][0], self._m.n[0][1], self._m.n[0][2], self._m.n[0][3], - self._m.n[1][0], self._m.n[1][1], self._m.n[1][2], self._m.n[1][3], - self._m.n[2][0], self._m.n[2][1], self._m.n[2][2], self._m.n[2][3], - 0.0, 0.0, 0.0, 1.0) - -cdef Matrix _Matrix(dmnsn_matrix m): - """Wrap a Matrix object around a dmnsn_matrix.""" - cdef Matrix self = Matrix.__new__(Matrix) - self._m = m - return self - -def scale(*args, **kwargs): - """ - Return a scale transformation. - - Accepts the same arguments that Vector(...) does. The transformation scales - by a factor of x in the x direction, y in the y direction, and z in the z - direction. In particular, this means that scale(2*X) is probably a mistake, - as the y and z coordinates will disappear. - - Alternatively, a single argument may be passed, which specifies the scaling - factor in every component. - """ - cdef Vector s - try: - s = Vector(*args, **kwargs) - except: - s = args[0]*(X + Y + Z) - return _Matrix(dmnsn_scale_matrix(s._v)) -def translate(*args, **kwargs): - """ - Return a translation. - - Accepts the same arguments that Vector(...) does. - """ - return _Matrix(dmnsn_translation_matrix(Vector(*args, **kwargs)._v)) -def rotate(*args, **kwargs): - """ - Return a rotation. - - Accepts the same arguments that Vector(...) does. theta.norm() is the left- - handed angle of rotation, and theta.normalized() is the axis of rotation. - theta is specified in degrees. - """ - cdef Vector rad = dmnsn_radians(1.0)*Vector(*args, **kwargs) - return _Matrix(dmnsn_rotation_matrix(rad._v)) - -cdef class _Transformable: - def scale(self, *args, **kwargs): - """Scale. Equivalent to self.transform(scale(...)).""" - return self.transform(scale(*args, **kwargs)) - def translate(self, *args, **kwargs): - """Translate. Equivalent to self.transform(translate(...)).""" - return self.transform(translate(*args, **kwargs)) - def rotate(self, *args, **kwargs): - """Rotate. Equivalent to self.transform(rotate(...)).""" - return self.transform(rotate(*args, **kwargs)) - -########## -# Colors # -########## - -cdef class Color: - """ - An sRGB color. - - Note that 0.5*White == Color(0.5, 0.5, 0.5), which is not technically a half- - intensity white, due to sRGB gamma. Dimension handles the gamma correctly - when rendering, though. - """ - cdef dmnsn_color _c - cdef dmnsn_color _sRGB - - def __init__(self, *args, **kwargs): - """ - Create a Color. - - Keyword arguments: - red -- The red component - green -- The green component - blue -- The blue component - trans -- The transparency of the color, 0.0 meaning opaque (default 0.0) - filter -- How filtered the transparency is (default 0.0) - - Alternatively, you can pass another Color, a gray intensity like 0.5, or a - tuple or other sequence (red, green, blue[, trans[, filter]]). - """ - if len(args) == 1: - if isinstance(args[0], Color): - self._sRGB = (args[0])._sRGB - elif hasattr(args[0], "__iter__"): - self._real_init(*args[0]) - else: - self._sRGB = dmnsn_color_mul(args[0], dmnsn_white) - else: - self._real_init(*args, **kwargs) - - self._c = dmnsn_color_from_sRGB(self._sRGB) - - def _real_init(self, double red, double green, double blue, - double trans = 0.0, double filter = 0.0): - self._sRGB = dmnsn_new_color5(red, green, blue, trans, filter) - - property red: - """The red component.""" - def __get__(self): - return self._sRGB.R - property green: - """The green component.""" - def __get__(self): - return self._sRGB.G - property blue: - """The blue component.""" - def __get__(self): - return self._sRGB.B - property trans: - """The transparency of the color.""" - def __get__(self): - return self._sRGB.trans - property filter: - """How filtered the transparency is.""" - def __get__(self): - return self._sRGB.filter - - def __nonzero__(self): - """Return whether a color is not black.""" - return not dmnsn_color_is_black(self._c) - - def __add__(lhs, rhs): - return _sRGBColor(dmnsn_color_add(Color(lhs)._sRGB, Color(rhs)._sRGB)) - def __mul__(lhs, rhs): - if isinstance(lhs, Color): - return _sRGBColor(dmnsn_color_mul(rhs, (lhs)._sRGB)) - else: - return _sRGBColor(dmnsn_color_mul(lhs, (rhs)._sRGB)) - - def __richcmp__(lhs, rhs, int op): - cdef clhs = Color(lhs) - cdef crhs = Color(rhs) - - cdef double rdiff = clhs.red - crhs.red - cdef double gdiff = clhs.green - crhs.green - cdef double bdiff = clhs.blue - crhs.blue - cdef double tdiff = clhs.trans - crhs.trans - cdef double fdiff = clhs.filter - crhs.filter - cdef double sum = rdiff*rdiff + gdiff*gdiff + bdiff*bdiff \ - + tdiff*tdiff + fdiff*fdiff - equal = sqrt(sum) < dmnsn_epsilon - if op == 2: # == - return equal - elif op == 3: # != - return not equal - else: - return NotImplemented - - def __repr__(self): - return "dimension.Color(%r, %r, %r, %r, %r)" % \ - (self.red, self.green, self.blue, self.trans, self.filter) - - def __str__(self): - if self.trans >= dmnsn_epsilon: - return "" % \ - (self.red, self.green, self.blue, self.trans, self.filter) - else: - return "" % \ - (self.red, self.green, self.blue) - -cdef Color _sRGBColor(dmnsn_color sRGB): - """Wrap a color object around a dmnsn_color already in sRGB.""" - cdef Color self = Color.__new__(Color) - self._sRGB = sRGB - self._c = dmnsn_color_from_sRGB(sRGB) - return self - -cdef Color _Color(dmnsn_color c): - """Wrap a Color object around a dmnsn_color.""" - cdef Color self = Color.__new__(Color) - self._c = c - self._sRGB = dmnsn_color_to_sRGB(c) - return self - -Black = _Color(dmnsn_black) -White = _Color(dmnsn_white) -Clear = _Color(dmnsn_clear) -Red = _Color(dmnsn_red) -Green = _Color(dmnsn_green) -Blue = _Color(dmnsn_blue) -Magenta = _Color(dmnsn_magenta) -Orange = _Color(dmnsn_orange) -Yellow = _Color(dmnsn_yellow) -Cyan = _Color(dmnsn_cyan) - -############ -# Canvases # -############ - -cdef class Canvas: - """A rendering target.""" - cdef dmnsn_canvas *_canvas - - def __init__(self, width, height): - """ - Create a Canvas. - - Keyword arguments: - width -- the width of the canvas - height -- the height of the canvas - """ - self._canvas = dmnsn_new_canvas(width, height) - self.clear(Black) - - def __dealloc__(self): - dmnsn_delete_canvas(self._canvas) - - property width: - """The width of the canvas.""" - def __get__(self): - return self._canvas.width - property height: - """The height of the canvas.""" - def __get__(self): - return self._canvas.height - - def __len__(self): - """The width of the canvas.""" - return self.width - def __getitem__(self, int x): - """Get a column of the canvas.""" - if x < 0 or x >= self.width: - raise IndexError("x coordinate out of bounds.") - return _CanvasProxy(self, x) - - def optimize_PNG(self): - """Optimize a canvas for PNG output.""" - if dmnsn_png_optimize_canvas(self._canvas) != 0: - _raise_OSError() - - def optimize_GL(self): - """Optimize a canvas for OpenGL output.""" - if dmnsn_gl_optimize_canvas(self._canvas) != 0: - _raise_OSError() - - def clear(self, c): - """Clear a canvas with a solid color.""" - dmnsn_canvas_clear(self._canvas, Color(c)._c) - - def write_PNG(self, path): - """Export the canvas as a PNG file.""" - self.write_PNG_async(path).join() - def write_PNG_async(self, path): - """Export the canvas as a PNG file, in the background.""" - bpath = path.encode("UTF-8") - cdef char *cpath = bpath - cdef FILE *file = fopen(cpath, "wb") - if file == NULL: - _raise_OSError(path) - - def finalize(): - if fclose(file) != 0: - _raise_OSError() - - cdef dmnsn_future *future = dmnsn_png_write_canvas_async(self._canvas, file) - - try: - if future == NULL: - _raise_OSError() - - ret = _Future(future) - ret._finalizer = finalize - return ret - except: - finalize() - raise - - def draw_GL(self): - """Export the canvas to the current OpenGL context.""" - if dmnsn_gl_write_canvas(self._canvas) != 0: - _raise_OSError() - -cdef class _CanvasProxy: - cdef dmnsn_canvas *_canvas - cdef int _x - - def __init__(self, Canvas canvas not None, int x): - self._canvas = canvas._canvas - self._x = x - - def __len__(self): - """The height of the canvas.""" - return self._canvas.height - def __getitem__(self, int y): - self._bounds_check(y) - return _Color(dmnsn_canvas_get_pixel(self._canvas, self._x, y)) - def __setitem__(self, int y, color): - self._bounds_check(y) - dmnsn_canvas_set_pixel(self._canvas, self._x, y, Color(color)._c) - - def _bounds_check(self, int y): - if y < 0 or y >= self._canvas.height: - raise IndexError("y coordinate out of bounds.") - -############ -# Patterns # -############ - -cdef class Pattern: - """A function which maps points in 3D space to scalar values.""" - cdef dmnsn_pattern *_pattern - - def __cinit__(self): - self._pattern = NULL - - def __dealloc__(self): - dmnsn_delete_pattern(self._pattern) - -cdef class Checker(Pattern): - """A checkerboard pattern.""" - def __init__(self): - self._pattern = dmnsn_new_checker_pattern() - Pattern.__init__(self) - -cdef class Gradient(Pattern): - """A gradient pattern.""" - def __init__(self, orientation): - """ - Create a gradient pattern. - - Keyword arguments: - orientation -- The direction of the linear gradient. - """ - self._pattern = dmnsn_new_gradient_pattern(Vector(orientation)._v) - Pattern.__init__(self) - -cdef class Leopard(Pattern): - """A leopard pattern.""" - def __init__(self): - self._pattern = dmnsn_new_leopard_pattern() - Pattern.__init__(self) - -############ -# Pigments # -############ - -cdef class Pigment(_Transformable): - """Object surface coloring.""" - cdef dmnsn_pigment *_pigment - - def __cinit__(self): - self._pigment = NULL - - def __init__(self, quick_color = None): - """ - Create a Pigment. - - With an arguement, create a solid pigment of that color. Otherwise, create - a base Pigment. - - Keyword arguments: - quick_color -- the object's quick color for low-quality renders - """ - if quick_color is not None: - if self._pigment == NULL: - if isinstance(quick_color, Pigment): - self._pigment = (quick_color)._pigment - DMNSN_INCREF(self._pigment) - else: - self._pigment = dmnsn_new_solid_pigment(Color(quick_color)._c) - else: - self._pigment.quick_color = Color(quick_color)._c - - def __dealloc__(self): - dmnsn_delete_pigment(self._pigment) - - def transform(self, Matrix trans not None): - """Transform a pigment.""" - self._pigment.trans = dmnsn_matrix_mul(trans._m, self._pigment.trans) - return self - -cdef Pigment _Pigment(dmnsn_pigment *pigment): - """Wrap a Pigment object around a dmnsn_pigment *.""" - cdef Pigment self = Pigment.__new__(Pigment) - self._pigment = pigment - DMNSN_INCREF(self._pigment) - return self - -cdef class ImageMap(Pigment): - """An image-mapped pigment.""" - def __init__(self, path, *args, **kwargs): - """ - Create an ImageMap. - - Keyword arguments: - path -- the path of the PNG file to open - """ - bpath = path.encode("UTF-8") - cdef char *cpath = bpath - cdef FILE *file = fopen(cpath, "rb") - if file == NULL: - _raise_OSError(path) - cdef dmnsn_canvas *canvas = dmnsn_png_read_canvas(file) - if canvas == NULL: - _raise_OSError(path) - if fclose(file) != 0: - _raise_OSError() - - self._pigment = dmnsn_new_canvas_pigment(canvas) - Pigment.__init__(self, *args, **kwargs) - -cdef class PigmentMap(Pigment): - """A pigment map.""" - def __init__(self, Pattern pattern not None, map, bool sRGB not None = True, - *args, **kwargs): - """ - Create a PigmentMap. - - Keyword arguments: - pattern -- the pattern to use for the mapping - map -- a dictionary of the form { val1: color1, val2: pigment2, ... }, - or a list of the form [color1, pigment2, ...] - sRGB -- whether the gradients should be in sRGB or linear space - (default True) - """ - cdef dmnsn_map *pigment_map = dmnsn_new_pigment_map() - cdef dmnsn_pigment *real_pigment - if hasattr(map, "items"): - for i, pigment in map.items(): - pigment = Pigment(pigment) - real_pigment = (pigment)._pigment - DMNSN_INCREF(real_pigment) - dmnsn_map_add_entry(pigment_map, i, &real_pigment) - else: - for i, pigment in enumerate(map): - pigment = Pigment(pigment) - real_pigment = (pigment)._pigment - DMNSN_INCREF(real_pigment) - dmnsn_map_add_entry(pigment_map, i/len(map), &real_pigment) - - cdef dmnsn_pigment_map_flags flags - if sRGB: - flags = DMNSN_PIGMENT_MAP_SRGB - else: - flags = DMNSN_PIGMENT_MAP_REGULAR - - DMNSN_INCREF(pattern._pattern) - self._pigment = dmnsn_new_pigment_map_pigment(pattern._pattern, pigment_map, - flags) - Pigment.__init__(self, *args, **kwargs) - -############ -# Finishes # -############ - -cdef class Finish: - """Object surface qualities.""" - cdef dmnsn_finish _finish - - def __cinit__(self): - self._finish = dmnsn_new_finish() - - def __dealloc__(self): - dmnsn_delete_finish(self._finish) - - def __add__(Finish lhs not None, Finish rhs not None): - """ - Combine two finishes. - - In lhs + rhs, the attributes of rhs override those of lhs if any conflict; - thus, Ambient(0.1) + Ambient(0.2) is the same as Ambient(0.2) - """ - cdef Finish ret = Finish() - dmnsn_finish_cascade(&lhs._finish, &ret._finish) - dmnsn_finish_cascade(&rhs._finish, &ret._finish) # rhs gets priority - return ret - -cdef Finish _Finish(dmnsn_finish finish): - """Wrap a Finish object around a dmnsn_finish.""" - cdef Finish self = Finish.__new__(Finish) - self._finish = finish - dmnsn_finish_incref(&self._finish) - return self - -cdef class Ambient(Finish): - """Ambient light reflected.""" - def __init__(self, color): - """ - Create an Ambient finish. - - Keyword arguments: - color -- the color and intensity of the ambient light - """ - self._finish.ambient = dmnsn_new_basic_ambient(Color(color)._c) - -cdef class Diffuse(Finish): - """Lambertian diffuse reflection.""" - def __init__(self, double diffuse): - """ - Create a Diffuse finish. - - Keyword arguments: - diffuse -- the intensity of the diffuse reflection - """ - cdef dmnsn_color gray = dmnsn_color_mul(diffuse, dmnsn_white) - diffuse = dmnsn_color_intensity(dmnsn_color_from_sRGB(gray)) - self._finish.diffuse = dmnsn_new_lambertian(diffuse) - -cdef class Phong(Finish): - """Phong specular highlight.""" - def __init__(self, double strength, double size = 40.0): - """ - Create a Phong highlight. - """ - self._finish.specular = dmnsn_new_phong(strength, size) - -cdef class Reflection(Finish): - """Reflective finish.""" - def __init__(self, min, max = None, double falloff = 1.0): - """ - Create a Reflection. - - Keyword arguments: - min -- color and intensity of reflection at indirect angles - max -- color and intensity of reflection at direct angles (default: min) - falloff -- exponent for reflection falloff (default: 1.0) - """ - if max is None: - max = min - - # Use sRGB value because Reflection(0.5) should really mean "reflect half - # the light" - self._finish.reflection = dmnsn_new_basic_reflection(Color(min)._sRGB, - Color(max)._sRGB, - falloff) - -############ -# Textures # -############ - -cdef class Texture(_Transformable): - """Object surface properties.""" - cdef dmnsn_texture *_texture - - def __init__(self, pigment = None, Finish finish = None): - """ - Create a Texture. - - Keyword arguments: - pigment -- the Pigment for the texture, or a color (default: None) - finish -- the Finish for the texture (default: None) - """ - self._texture = dmnsn_new_texture() - - if pigment is not None: - self.pigment = Pigment(pigment) - - if finish is not None: - self.finish = finish - - def __dealloc__(self): - dmnsn_delete_texture(self._texture) - - property pigment: - """The texture's pigment.""" - def __get__(self): - if self._texture.pigment == NULL: - return None - else: - return _Pigment(self._texture.pigment) - def __set__(self, pigment): - dmnsn_delete_pigment(self._texture.pigment) - cdef Pigment real_pigment - if pigment is None: - self._texture.pigment = NULL - else: - real_pigment = Pigment(pigment) - self._texture.pigment = real_pigment._pigment - DMNSN_INCREF(self._texture.pigment) - - property finish: - """The texture's finish.""" - def __get__(self): - return _Finish(self._texture.finish) - def __set__(self, Finish finish not None): - dmnsn_delete_finish(self._texture.finish) - self._texture.finish = finish._finish - dmnsn_finish_incref(&self._texture.finish) - - def transform(self, Matrix trans not None): - """Transform a texture.""" - self._texture.trans = dmnsn_matrix_mul(trans._m, self._texture.trans) - return self - -cdef Texture _Texture(dmnsn_texture *texture): - """Wrap a Texture object around a dmnsn_texture *.""" - cdef Texture self = Texture.__new__(Texture) - self._texture = texture - DMNSN_INCREF(self._texture) - return self - -############# -# Interiors # -############# - -cdef class Interior: - """Object interior properties.""" - cdef dmnsn_interior *_interior - - def __init__(self, double ior = 1.0): - """ - Create an Interior. - - Keyword arguments: - ior -- index of reflection - """ - self._interior = dmnsn_new_interior() - self._interior.ior = ior - - def __dealloc__(self): - dmnsn_delete_interior(self._interior) - - property ior: - """Index of reflection.""" - def __get__(self): - return self._interior.ior - def __set__(self, double ior): - self._interior.ior = ior - -cdef Interior _Interior(dmnsn_interior *interior): - """Wrap an Interior object around a dmnsn_interior *.""" - cdef Interior self = Interior.__new__(Interior) - self._interior = interior - DMNSN_INCREF(self._interior) - return self - -########### -# Objects # -########### - -cdef class Object(_Transformable): - """Physical objects.""" - cdef dmnsn_object *_object - - def __cinit__(self): - self._object = NULL - - def __init__(self, Texture texture = None, pigment = None, - Finish finish = None, Interior interior = None): - """ - Initialize an Object. - - Keyword arguments: - texture -- the object's Texture - pigment -- shorthand for specifying the texture's pigment - finish -- shorthand for specifying the texture's finish - interior -- the object's Interior - """ - if self._object == NULL: - raise TypeError("attempt to initialize base Object") - - self.texture = texture - if pigment is not None: - if texture is not None: - raise TypeError("both texture and pigment specified.") - else: - if self.texture is None: - self.texture = Texture() - self.texture.pigment = pigment - - if finish is not None: - if texture is not None: - raise TypeError("both texture and finish specified.") - else: - if self.texture is None: - self.texture = Texture() - self.texture.finish = finish - - if interior is not None: - self.interior = interior - - def __dealloc__(self): - dmnsn_delete_object(self._object) - - property texture: - """The object's Texture.""" - def __get__(self): - if self._object.texture == NULL: - return None - else: - return _Texture(self._object.texture) - def __set__(self, Texture texture): - dmnsn_delete_texture(self._object.texture) - if texture is None: - self._object.texture = NULL - else: - self._object.texture = texture._texture - DMNSN_INCREF(self._object.texture) - - property interior: - """The object's Interior.""" - def __get__(self): - return _Interior(self._object.interior) - def __set__(self, Interior interior not None): - self._object.interior = interior._interior - DMNSN_INCREF(self._object.interior) - - def transform(self, Matrix trans not None): - """Transform an object.""" - self._object.trans = dmnsn_matrix_mul(trans._m, self._object.trans) - return self - - # Transform an object without affecting the texture - cdef _intrinsic_transform(self, Matrix trans): - self._object.intrinsic_trans = dmnsn_matrix_mul( - trans._m, - self._object.intrinsic_trans - ) - -cdef class Triangle(Object): - """A triangle.""" - def __init__(self, a, b, c, *args, **kwargs): - """ - Create a Triangle. - - Keyword arguments: - a, b, c -- the corners of the triangle - - Additionally, Triangle() accepts any arguments that Object() accepts. - """ - self._object = dmnsn_new_triangle(Vector(a)._v, Vector(b)._v, Vector(c)._v) - Object.__init__(self, *args, **kwargs) - -cdef class Plane(Object): - """A plane.""" - def __init__(self, normal, double distance, *args, **kwargs): - """ - Create a Plane. - - Keyword arguments: - normal -- a vector perpendicular to the plane - distance -- the distance from the origin to the plane, in the direction of - normal - - Additionally, Plane() accepts any arguments that Object() accepts. - """ - self._object = dmnsn_new_plane(Vector(normal)._v) - Object.__init__(self, *args, **kwargs) - - self._intrinsic_transform(translate(distance*Vector(normal))) - -cdef class Sphere(Object): - """A sphere.""" - def __init__(self, center, double radius, *args, **kwargs): - """ - Create a Sphere. - - Keyword arguments: - center -- the center of the sphere - radius -- the radius of the sphere - - Additionally, Sphere() accepts any arguments that Object() accepts. - """ - self._object = dmnsn_new_sphere() - Object.__init__(self, *args, **kwargs) - - cdef Matrix trans = translate(Vector(center)) - trans *= scale(radius, radius, radius) - self._intrinsic_transform(trans) - -cdef class Box(Object): - """An axis-aligned rectangular prism.""" - def __init__(self, min, max, *args, **kwargs): - """ - Create a Box. - - Keyword arguments: - min -- the coordinate-wise minimal extent of the box - max -- the coordinate-wise maximal extent of the box - - Additionally, Box() accepts any arguments that Object() accepts. - """ - self._object = dmnsn_new_cube() - Object.__init__(self, *args, **kwargs) - - min = Vector(min) - max = Vector(max) - cdef Matrix trans = translate((max + min)/2) - trans *= scale((max - min)/2) - self._intrinsic_transform(trans) - -cdef class Cone(Object): - """A cone or cone slice.""" - def __init__(self, bottom, double bottom_radius, top, double top_radius = 0.0, - bool open not None = False, *args, **kwargs): - """ - Create a Cone. - - Keyword arguments: - bottom -- the location of the bottom of the cone - bottom_radius -- the radius at the bottom of the cone - top -- the location of the top of the cone - top_radius -- the radius at the top of the cone/cone slice (default 0.0) - open -- whether to draw the cone cap(s) - - Additionally, Cone() accepts any arguments that Object() accepts. - """ - self._object = dmnsn_new_cone(bottom_radius, top_radius, open) - Object.__init__(self, *args, **kwargs) - - # Lift the cone to start at the origin, then scale, rotate, and translate - # properly - - cdef Vector dir = Vector(top) - Vector(bottom) - - cdef Matrix trans = translate(Y) - trans = scale(1.0, dir.norm()/2, 1.0)*trans - trans = _Matrix(dmnsn_alignment_matrix(dmnsn_y, dir._v, dmnsn_x, dmnsn_z))*trans - trans = translate(bottom)*trans - - self._intrinsic_transform(trans) - -cdef class Cylinder(Cone): - """A cylinder.""" - def __init__(self, bottom, top, double radius, bool open not None = False, - *args, **kwargs): - """ - Create a Cylinder. - - Keyword arguments: - bottom -- the location of the bottom of the cylinder - top -- the location of the top of the cylinder - radius -- the radius of the cylinder - open -- whether to draw the cylinder caps - - Additionally, Cylinder() accepts any arguments that Object() accepts. - """ - Cone.__init__(self, - bottom = bottom, bottom_radius = radius, - top = top, top_radius = radius, - open = open, - *args, **kwargs) - -cdef class Torus(Object): - """A torus.""" - def __init__(self, double major_radius, double minor_radius, *args, **kwargs): - """ - Create a Torus. - - Keyword arguments: - major_radius -- the distance from the center of the torus to the center of - a circular cross-section of the torus - minor_radius -- the radius of the circular cross-sections of the torus - - Additionally, Torus() accepts any arguments that Object() accepts. - """ - self._object = dmnsn_new_torus(major_radius, minor_radius) - Object.__init__(self, *args, **kwargs) - -cdef class Union(Object): - """A CSG union.""" - def __init__(self, objects, *args, **kwargs): - """ - Create a Union. - - Keyword arguments: - objects -- a list of objects to include in the union - - Additionally, Union() accepts any arguments that Object() accepts. - """ - if len(objects) < 2: - raise TypeError("expected a list of two or more Objects") - - cdef dmnsn_array *array = dmnsn_new_array(sizeof(dmnsn_object *)) - cdef dmnsn_object *o - - try: - for obj in objects: - o = (obj)._object - DMNSN_INCREF(o) - dmnsn_array_push(array, &o) - - self._object = dmnsn_new_csg_union(array) - finally: - dmnsn_delete_array(array) - - Object.__init__(self, *args, **kwargs) - -cdef class Intersection(Object): - """A CSG intersection.""" - def __init__(self, objects, *args, **kwargs): - """ - Create an Intersection. - - Keyword arguments: - objects -- a list of objects to include in the intersection - - Additionally, Intersection() accepts any arguments that Object() accepts. - """ - if len(objects) < 2: - raise TypeError("expected a list of two or more Objects") - - cdef dmnsn_object *o - - for obj in objects: - if self._object == NULL: - self._object = (obj)._object - DMNSN_INCREF(self._object) - else: - o = (obj)._object - DMNSN_INCREF(o) - self._object = dmnsn_new_csg_intersection(self._object, o) - - Object.__init__(self, *args, **kwargs) - -cdef class Difference(Object): - """A CSG difference.""" - def __init__(self, objects, *args, **kwargs): - """ - Create a Difference. - - Keyword arguments: - objects -- a list of objects to include in the difference - - Additionally, Difference() accepts any arguments that Object() accepts. - """ - if len(objects) < 2: - raise TypeError("expected a list of two or more Objects") - - cdef dmnsn_object *o - - for obj in objects: - if self._object == NULL: - self._object = (obj)._object - DMNSN_INCREF(self._object) - else: - o = (obj)._object - DMNSN_INCREF(o) - self._object = dmnsn_new_csg_difference(self._object, o) - - Object.__init__(self, *args, **kwargs) - -cdef class Merge(Object): - """A CSG merge.""" - def __init__(self, objects, *args, **kwargs): - """ - Create a Merge. - - Keyword arguments: - objects -- a list of objects to include in the merge - - Additionally, Merge() accepts any arguments that Object() accepts. - """ - if len(objects) < 2: - raise TypeError("expected a list of two or more Objects") - - cdef dmnsn_object *o - - for obj in objects: - if self._object == NULL: - self._object = (obj)._object - DMNSN_INCREF(self._object) - else: - o = (obj)._object - DMNSN_INCREF(o) - self._object = dmnsn_new_csg_merge(self._object, o) - - Object.__init__(self, *args, **kwargs) - -########## -# Lights # -########## - -cdef class Light: - """A light.""" - cdef dmnsn_light *_light - - def __dealloc__(self): - dmnsn_delete_light(self._light) - -cdef class PointLight(Light): - """A point light.""" - def __init__(self, location, color): - """ - Create a PointLight. - - Keyword arguments: - location -- the origin of the light rays - color -- the color and intensity of the light - """ - # Take the sRGB component because "color = 0.5*White" should really mean - # a half-intensity white light - self._light = dmnsn_new_point_light(Vector(location)._v, Color(color)._sRGB) - Light.__init__(self) - -########### -# Cameras # -########### - -cdef class Camera(_Transformable): - """A camera.""" - cdef dmnsn_camera *_camera - - def __cinit__(self): - self._camera = NULL - - def __init__(self): - if self._camera == NULL: - raise TypeError("attempt to initialize base Camera") - - def __dealloc__(self): - dmnsn_delete_camera(self._camera) - - def transform(self, Matrix trans not None): - """Transform a camera.""" - self._camera.trans = dmnsn_matrix_mul(trans._m, self._camera.trans) - return self - -cdef class PerspectiveCamera(Camera): - """A regular perspective camera.""" - def __init__(self, location = -Z, look_at = 0, sky = Y, - angle = dmnsn_degrees(atan(1.0))): - """ - Create a PerspectiveCamera. - - Keyword arguments: - location -- the location of the camera (default: -Z) - look_at -- where to aim the camera (default: 0) - sky -- the direction of the top of the camera (default: Y) - angle -- the field of view angle (from bottom to top) (default: 45) - """ - self._camera = dmnsn_new_perspective_camera() - Camera.__init__(self) - - # Apply the field of view angle - self.scale(tan(dmnsn_radians(angle))*(X + Y) + Z) - - cdef Vector dir = Vector(look_at) - Vector(location) - cdef Vector vsky = Vector(sky) - - # Line up the top of the viewport with the sky vector - cdef Matrix align_sky = _Matrix(dmnsn_alignment_matrix(dmnsn_y, vsky._v, - dmnsn_z, dmnsn_x)) - cdef Vector forward = align_sky*Z - cdef Vector right = align_sky*X - - # Line up the look at point with look_at - self.transform(_Matrix(dmnsn_alignment_matrix(forward._v, dir._v, - vsky._v, right._v))) - - # Move the camera into position - self.translate(Vector(location)) - -########## -# Scenes # -########## - -cdef class Scene: - """An entire scene.""" - cdef dmnsn_scene *_scene - - def __init__(self, Canvas canvas not None, objects, lights, - Camera camera not None): - """ - Create a Scene. - - Keyword arguments: - canvas -- the rendering Canvas - objects -- the list of objects in the scene - lights -- the list of lights in the scene - camera -- the camera for the scene - """ - self._scene = dmnsn_new_scene() - - self._scene.canvas = canvas._canvas - DMNSN_INCREF(self._scene.canvas) - self.outer_width = self._scene.canvas.width - self.outer_height = self._scene.canvas.height - self.background = Black - - cdef dmnsn_object *o - for obj in objects: - o = (obj)._object - DMNSN_INCREF(o) - dmnsn_array_push(self._scene.objects, &o) - - cdef dmnsn_light *l - for light in lights: - l = (light)._light - DMNSN_INCREF(l) - dmnsn_array_push(self._scene.lights, &l) - - self._scene.camera = camera._camera - DMNSN_INCREF(self._scene.camera) - - # Subregion render support - property region_x: - """The x-coordinate of the subregion in the broader image.""" - def __get__(self): - return self._scene.region_x - def __set__(self, x): - self._scene.region_x = x - property region_y: - """The y-coordinate of the subregion in the broader image.""" - def __get__(self): - return self._scene.region_y - def __set__(self, y): - self._scene.region_y = y - property outer_width: - """The width of the broader image.""" - def __get__(self): - return self._scene.outer_width - def __set__(self, width): - self._scene.outer_width = width - property outer_height: - """The height of the broader image.""" - def __get__(self): - return self._scene.outer_height - def __set__(self, height): - self._scene.outer_height = height - - property default_texture: - """The default Texture for objects.""" - def __get__(self): - return _Texture(self._scene.default_texture) - def __set__(self, Texture texture not None): - dmnsn_delete_texture(self._scene.default_texture) - self._scene.default_texture = texture._texture - DMNSN_INCREF(self._scene.default_texture) - property default_interior: - """The default Interior for objects.""" - def __get__(self): - return _Interior(self._scene.default_interior) - def __set__(self, Interior interior not None): - dmnsn_delete_interior(self._scene.default_interior) - self._scene.default_interior = interior._interior - DMNSN_INCREF(self._scene.default_interior) - - property background: - """The background pigment of the scene (default: Black).""" - def __get__(self): - return _Pigment(self._scene.background) - def __set__(self, pigment): - dmnsn_delete_pigment(self._scene.background) - cdef Pigment real_pigment = Pigment(pigment) - self._scene.background = real_pigment._pigment - DMNSN_INCREF(self._scene.background) - - property adc_bailout: - """The adaptive depth control bailout (default: 1/255).""" - def __get__(self): - return self._scene.adc_bailout - def __set__(self, double bailout): - self._scene.adc_bailout = bailout - - property recursion_limit: - """The rendering recursion limit (default: 5).""" - def __get__(self): - return self._scene.reclimit - def __set__(self, level): - self._scene.reclimit = level - - property nthreads: - """The number of threads to use for the render.""" - def __get__(self): - return self._scene.nthreads - def __set__(self, n): - if n <= 0: - raise ValueError("%d is an invalid thread count." % n) - self._scene.nthreads = n - - property quality: - """The render quality.""" - def __get__(self): - return _quality_to_string(self._scene.quality) - def __set__(self, q): - self._scene.quality = _string_to_quality(q) - - property bounding_timer: - """The Timer for building the bounding hierarchy.""" - def __get__(self): - return _Timer(self._scene.bounding_timer) - property render_timer: - """The Timer for the actual render.""" - def __get__(self): - return _Timer(self._scene.render_timer) - - def ray_trace(self): - """Render the scene.""" - self.ray_trace_async().join() - def ray_trace_async(self): - """Render the scene, in the background.""" - # Account for image dimensions in the camera - # Do this here so subregion renders can tell us the broader image size - self._scene.camera.trans = dmnsn_matrix_mul( - self._scene.camera.trans, - dmnsn_scale_matrix( - dmnsn_new_vector( - self.outer_width/self.outer_height, - 1.0, - 1.0 - ) - ) - ) - # Ensure the default texture is complete - cdef Texture default = Texture(pigment = Black) - dmnsn_texture_cascade(default._texture, &self._scene.default_texture) - return _Future(dmnsn_ray_trace_async(self._scene)) - - def __dealloc__(self): - dmnsn_delete_scene(self._scene) - -def _quality_to_string(int quality): - cdef str s = "" - - if quality & DMNSN_RENDER_PIGMENT: - s += 'p' - if quality & DMNSN_RENDER_LIGHTS: - s += 'l' - if quality & DMNSN_RENDER_FINISH: - s += 'f' - if quality & DMNSN_RENDER_TRANSPARENCY: - s += 't' - if quality & DMNSN_RENDER_REFLECTION: - s += 'r' - - if s == "": - return "0" - else: - return s - -def _string_to_quality(str quality not None): - cdef int q = DMNSN_RENDER_NONE - inverse = False - - if quality == "": - return q - - if quality[0] == '^': - inverse = True - quality = quality[1:] - - if quality != "0": - while len(quality) > 0: - ch = quality[0] - quality = quality[1:] - - if ch == 'p': - flag = DMNSN_RENDER_PIGMENT - elif ch == 'l': - flag = DMNSN_RENDER_LIGHTS - elif ch == 'f': - flag = DMNSN_RENDER_FINISH - elif ch == 't': - flag = DMNSN_RENDER_TRANSPARENCY - elif ch == 'r': - flag = DMNSN_RENDER_REFLECTION - else: - raise ValueError("unknown quality flag '%c'" % ch) - - if q & flag: - raise ValueError("flag '%c' specified twice" % ch) - else: - q |= flag - - if inverse: - q = ~q - - return q diff --git a/libdimension-python/tests/Makefile.am b/libdimension-python/tests/Makefile.am index ce12ed7..420d03b 100644 --- a/libdimension-python/tests/Makefile.am +++ b/libdimension-python/tests/Makefile.am @@ -22,12 +22,9 @@ TESTS = geometry.py \ canvas.py \ demo.py TEST_EXTENSIONS = .py -TESTS_ENVIRONMENT = PYTHONPATH=$(top_builddir)/libdimension-python/.libs \ +TESTS_ENVIRONMENT = PYTHONPATH=$(abs_top_builddir) \ top_srcdir=$(top_srcdir) -.py: - cp $(srcdir)/$@ . - EXTRA_DIST = $(TESTS) clean-local: diff --git a/libdimension-python/wrapper.pxd b/libdimension-python/wrapper.pxd new file mode 100644 index 0000000..7ba17e9 --- /dev/null +++ b/libdimension-python/wrapper.pxd @@ -0,0 +1,397 @@ +######################################################################### +# Copyright (C) 2011 Tavian Barnes # +# # +# This file is part of The Dimension Python Module. # +# # +# The Dimension Python Module is free software; you can redistribute it # +# and/or modify it under the terms of the GNU Lesser General Public # +# License as published by the Free Software Foundation; either version # +# 3 of the License, or (at your option) any later version. # +# # +# The Dimension Python Module is distributed in the hope that it will # +# be useful, but WITHOUT ANY WARRANTY; without even the implied # +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # +# the GNU Lesser General Public License for more details. # +# # +# You should have received a copy of the GNU Lesser General Public # +# License along with this program. If not, see # +# . # +######################################################################### + +from cpython cimport bool +from libc.math cimport * +from libc.stdio cimport * + +cdef extern from "errno.h": + int errno + +cdef extern from "../libdimension/dimension.h": + + ########### + # Globals # + ########### + + ctypedef void dmnsn_callback_fn(void *ptr) + + void DMNSN_INCREF(void *) + + void dmnsn_die_on_warnings(bint always_die) + + double dmnsn_epsilon + + ########## + # Arrays # + ########## + + ctypedef struct dmnsn_array: + pass + + dmnsn_array *dmnsn_new_array(size_t objsize) + void dmnsn_delete_array(dmnsn_array *array) + + size_t dmnsn_array_size(dmnsn_array *array) + void dmnsn_array_resize(dmnsn_array *array, size_t length) + dmnsn_array *dmnsn_array_copy(dmnsn_array *array) + dmnsn_array *dmnsn_array_split(dmnsn_array *array) + void dmnsn_array_get(dmnsn_array *array, size_t i, void *obj) + void dmnsn_array_set(dmnsn_array *array, size_t i, void *obj) + void *dmnsn_array_first(dmnsn_array *array) + void *dmnsn_array_last(dmnsn_array *array) + void *dmnsn_array_at(dmnsn_array *array, size_t i) + void dmnsn_array_push(dmnsn_array *array, void *obj) + void dmnsn_array_pop(dmnsn_array *array, void *obj) + void dmnsn_array_insert(dmnsn_array *array, size_t i, void *obj) + void dmnsn_array_remove(dmnsn_array *array, size_t i) + void dmnsn_array_apply(dmnsn_array *array, dmnsn_callback_fn *callback) + + ########### + # Futures # + ########### + + ctypedef struct dmnsn_future + + int dmnsn_future_join(dmnsn_future *future) + void dmnsn_future_cancel(dmnsn_future *future) + double dmnsn_future_progress(dmnsn_future *future) + void dmnsn_future_wait(dmnsn_future *future, double progress) + + ########## + # Timers # + ########## + + ctypedef struct dmnsn_timer: + double real + double user + double system + + void dmnsn_timer_start(dmnsn_timer *timer) + void dmnsn_timer_stop(dmnsn_timer *timer) + + ############ + # Geometry # + ############ + + double dmnsn_radians(double degrees) + double dmnsn_degrees(double radians) + + ctypedef struct dmnsn_vector: + double x + double y + double z + + dmnsn_vector dmnsn_new_vector(double x, double y, double z) + + dmnsn_vector dmnsn_vector_negate(dmnsn_vector rhs) + + dmnsn_vector dmnsn_vector_add(dmnsn_vector lhs, dmnsn_vector rhs) + dmnsn_vector dmnsn_vector_sub(dmnsn_vector lhs, dmnsn_vector rhs) + dmnsn_vector dmnsn_vector_mul(double lhs, dmnsn_vector rhs) + dmnsn_vector dmnsn_vector_div(dmnsn_vector lhs, double rhs) + + dmnsn_vector dmnsn_vector_cross(dmnsn_vector lhs, dmnsn_vector rhs) + double dmnsn_vector_dot(dmnsn_vector lhs, dmnsn_vector rhs) + dmnsn_vector dmnsn_vector_proj(dmnsn_vector u, dmnsn_vector d) + + double dmnsn_vector_norm(dmnsn_vector v) + dmnsn_vector dmnsn_vector_normalized(dmnsn_vector v) + + dmnsn_vector dmnsn_zero + dmnsn_vector dmnsn_x + dmnsn_vector dmnsn_y + dmnsn_vector dmnsn_z + + ctypedef struct dmnsn_matrix: + double n[3][4] + + dmnsn_matrix dmnsn_new_matrix(double a1, double a2, double a3, double a4, + double b1, double b2, double b3, double b4, + double c1, double c2, double c3, double c4) + + dmnsn_matrix dmnsn_matrix_inverse(dmnsn_matrix m) + + dmnsn_matrix dmnsn_matrix_mul(dmnsn_matrix lhs, dmnsn_matrix rhs) + dmnsn_vector dmnsn_transform_point(dmnsn_matrix lhs, dmnsn_vector rhs) + dmnsn_vector dmnsn_transform_direction(dmnsn_matrix lhs, dmnsn_vector rhs) + dmnsn_vector dmnsn_transform_normal(dmnsn_matrix lhs, dmnsn_vector rhs) + + dmnsn_matrix dmnsn_scale_matrix(dmnsn_vector s) + dmnsn_matrix dmnsn_translation_matrix(dmnsn_vector d) + dmnsn_matrix dmnsn_rotation_matrix(dmnsn_vector theta) + dmnsn_matrix dmnsn_alignment_matrix(dmnsn_vector frm, dmnsn_vector to, + dmnsn_vector axis1, dmnsn_vector axis2) + + ########## + # Colors # + ########## + + ctypedef struct dmnsn_color: + double R + double G + double B + double trans + double filter + + dmnsn_color dmnsn_new_color(double R, double G, double B) + dmnsn_color dmnsn_new_color5(double R, double G, double B, + double trans, double filter) + + dmnsn_color dmnsn_color_from_sRGB(dmnsn_color color) + dmnsn_color dmnsn_color_to_sRGB(dmnsn_color color) + + double dmnsn_color_intensity(dmnsn_color color) + dmnsn_color dmnsn_color_add(dmnsn_color color1, dmnsn_color color2) + dmnsn_color dmnsn_color_mul(double n, dmnsn_color color) + + bint dmnsn_color_is_black(dmnsn_color color) + + dmnsn_color dmnsn_black + dmnsn_color dmnsn_white + dmnsn_color dmnsn_clear + dmnsn_color dmnsn_red + dmnsn_color dmnsn_green + dmnsn_color dmnsn_blue + dmnsn_color dmnsn_magenta + dmnsn_color dmnsn_orange + dmnsn_color dmnsn_yellow + dmnsn_color dmnsn_cyan + + ############ + # Canvases # + ############ + + ctypedef struct dmnsn_canvas: + size_t width + size_t height + + dmnsn_canvas *dmnsn_new_canvas(size_t width, size_t height) + void dmnsn_delete_canvas(dmnsn_canvas *canvas) + + dmnsn_color dmnsn_canvas_get_pixel(dmnsn_canvas *canvas, size_t x, size_t y) + void dmnsn_canvas_set_pixel(dmnsn_canvas *canvas, size_t x, size_t y, + dmnsn_color color) + + void dmnsn_canvas_clear(dmnsn_canvas *canvas, dmnsn_color color) + + int dmnsn_png_optimize_canvas(dmnsn_canvas *canvas) + int dmnsn_png_write_canvas(dmnsn_canvas *canvas, FILE *file) + dmnsn_future *dmnsn_png_write_canvas_async(dmnsn_canvas *canvas, FILE *file) + dmnsn_canvas *dmnsn_png_read_canvas(FILE *file) + dmnsn_future *dmnsn_png_read_canvas_async(dmnsn_canvas **canvas, FILE *file) + + int dmnsn_gl_optimize_canvas(dmnsn_canvas *canvas) + int dmnsn_gl_write_canvas(dmnsn_canvas *canvas) + + ############ + # Patterns # + ############ + + ctypedef struct dmnsn_pattern: + pass + + void dmnsn_delete_pattern(dmnsn_pattern *pattern) + + dmnsn_pattern *dmnsn_new_checker_pattern() + dmnsn_pattern *dmnsn_new_gradient_pattern(dmnsn_vector orientation) + dmnsn_pattern *dmnsn_new_leopard_pattern() + + ######## + # Maps # + ######## + + ctypedef struct dmnsn_map: + pass + + void dmnsn_delete_map(dmnsn_map *map) + + void dmnsn_map_add_entry(dmnsn_map *map, double n, void *obj) + size_t dmnsn_map_size(dmnsn_map *map) + + dmnsn_map *dmnsn_new_pigment_map() + + ############ + # Pigments # + ############ + + ctypedef struct dmnsn_pigment: + dmnsn_matrix trans + dmnsn_color quick_color + + ctypedef enum dmnsn_pigment_map_flags: + DMNSN_PIGMENT_MAP_REGULAR + DMNSN_PIGMENT_MAP_SRGB + + void dmnsn_delete_pigment(dmnsn_pigment *pigment) + + dmnsn_pigment *dmnsn_new_solid_pigment(dmnsn_color color) + dmnsn_pigment *dmnsn_new_canvas_pigment(dmnsn_canvas *canvas) + dmnsn_pigment *dmnsn_new_pigment_map_pigment(dmnsn_pattern *pattern, + dmnsn_map *map, + dmnsn_pigment_map_flags flags) + + ############ + # Finishes # + ############ + + ctypedef struct dmnsn_ambient + ctypedef struct dmnsn_diffuse + ctypedef struct dmnsn_specular + ctypedef struct dmnsn_reflection + + ctypedef struct dmnsn_finish: + dmnsn_ambient *ambient + dmnsn_diffuse *diffuse + dmnsn_specular *specular + dmnsn_reflection *reflection + + dmnsn_finish dmnsn_new_finish() + void dmnsn_delete_finish(dmnsn_finish finish) + + void dmnsn_finish_incref(dmnsn_finish *finish) + + void dmnsn_finish_cascade(dmnsn_finish *default_finish, dmnsn_finish *finish) + + dmnsn_ambient *dmnsn_new_basic_ambient(dmnsn_color ambient) + dmnsn_diffuse *dmnsn_new_lambertian(double diffuse) + dmnsn_specular *dmnsn_new_phong(double specular, double exp) + dmnsn_reflection *dmnsn_new_basic_reflection(dmnsn_color min, dmnsn_color max, + double falloff) + + ############ + # Textures # + ############ + + ctypedef struct dmnsn_texture: + dmnsn_pigment *pigment + dmnsn_finish finish + dmnsn_matrix trans + + dmnsn_texture *dmnsn_new_texture() + void dmnsn_delete_texture(dmnsn_texture *texture) + + void dmnsn_texture_cascade(dmnsn_texture *default_texture, + dmnsn_texture **texture) + + ############# + # Interiors # + ############# + + ctypedef struct dmnsn_interior: + double ior + + dmnsn_interior *dmnsn_new_interior() + void dmnsn_delete_interior(dmnsn_interior *interior) + + ########### + # Objects # + ########### + + ctypedef struct dmnsn_object: + dmnsn_texture *texture + dmnsn_interior *interior + dmnsn_matrix trans + dmnsn_matrix intrinsic_trans + + dmnsn_object *dmnsn_new_object() + void dmnsn_delete_object(dmnsn_object *object) + + dmnsn_object *dmnsn_new_triangle(dmnsn_vector a, + dmnsn_vector b, + dmnsn_vector c) + dmnsn_object *dmnsn_new_plane(dmnsn_vector normal) + dmnsn_object *dmnsn_new_sphere() + dmnsn_object *dmnsn_new_cube() + dmnsn_object *dmnsn_new_cone(double r1, double r2, bint open) + dmnsn_object *dmnsn_new_torus(double major, double minor) + + dmnsn_object *dmnsn_new_csg_union(dmnsn_array *objects) + dmnsn_object *dmnsn_new_csg_intersection(dmnsn_object *A, dmnsn_object *B) + dmnsn_object *dmnsn_new_csg_difference(dmnsn_object *A, dmnsn_object *B) + dmnsn_object *dmnsn_new_csg_merge(dmnsn_object *A, dmnsn_object *B) + + ########## + # Lights # + ########## + + ctypedef struct dmnsn_light + + dmnsn_light *dmnsn_new_light() + void dmnsn_delete_light(dmnsn_light *light) + + dmnsn_light *dmnsn_new_point_light(dmnsn_vector x0, dmnsn_color color) + + ########### + # Cameras # + ########### + + ctypedef struct dmnsn_camera: + dmnsn_matrix trans + + dmnsn_camera *dmnsn_new_camera() + void dmnsn_delete_camera(dmnsn_camera *camera) + + dmnsn_camera *dmnsn_new_perspective_camera() + + ########## + # Scenes # + ########## + + ctypedef enum dmnsn_quality: + DMNSN_RENDER_NONE + DMNSN_RENDER_PIGMENT + DMNSN_RENDER_LIGHTS + DMNSN_RENDER_FINISH + DMNSN_RENDER_TRANSPARENCY + DMNSN_RENDER_REFLECTION + DMNSN_RENDER_FULL + + ctypedef struct dmnsn_scene: + dmnsn_pigment *background + dmnsn_texture *default_texture + dmnsn_interior *default_interior + + dmnsn_canvas *canvas + size_t region_x + size_t region_y + size_t outer_width + size_t outer_height + + dmnsn_array *objects + dmnsn_array *lights + dmnsn_camera *camera + + dmnsn_quality quality + unsigned int reclimit + double adc_bailout + unsigned int nthreads + + dmnsn_timer bounding_timer + dmnsn_timer render_timer + + dmnsn_scene *dmnsn_new_scene() + void dmnsn_delete_scene(dmnsn_scene *scene) + + void dmnsn_ray_trace(dmnsn_scene *scene) + dmnsn_future *dmnsn_ray_trace_async(dmnsn_scene *scene) + +cdef extern from "platform.h": + unsigned int dmnsn_terminal_width() diff --git a/libdimension-python/wrapper.pyx b/libdimension-python/wrapper.pyx new file mode 100644 index 0000000..738a63f --- /dev/null +++ b/libdimension-python/wrapper.pyx @@ -0,0 +1,1590 @@ +######################################################################### +# Copyright (C) 2011 Tavian Barnes # +# # +# This file is part of The Dimension Python Module. # +# # +# The Dimension Python Module is free software; you can redistribute it # +# and/or modify it under the terms of the GNU Lesser General Public # +# License as published by the Free Software Foundation; either version # +# 3 of the License, or (at your option) any later version. # +# # +# The Dimension Python Module is distributed in the hope that it will # +# be useful, but WITHOUT ANY WARRANTY; without even the implied # +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # +# the GNU Lesser General Public License for more details. # +# # +# You should have received a copy of the GNU Lesser General Public # +# License along with this program. If not, see # +# . # +######################################################################### + +""" +Dimension: a high-performance photo-realistic 3D renderer. +""" + +import os + +########### +# Helpers # +########### + +cdef _raise_OSError(filename = None): + if filename is None: + raise OSError(errno, os.strerror(errno)) + else: + raise OSError(errno, os.strerror(errno), filename) + +########### +# Globals # +########### + +def die_on_warnings(always_die): + """Whether to treat Dimension warnings as errors.""" + dmnsn_die_on_warnings(always_die) + +def terminal_width(): + """Return the width of the terminal, if present.""" + return dmnsn_terminal_width() + +########### +# Futures # +########### + +cdef class Future: + cdef dmnsn_future *_future + cdef _finalizer + + def __cinit__(self): + self._future = NULL + self._finalizer = None + + def __init__(self): + raise RuntimeError("attempt to create a Future object.") + + def __dealloc__(self): + if self._future != NULL: + self.join() + + def join(self): + self._assert_unfinished() + try: + if dmnsn_future_join(self._future) != 0: + raise RuntimeError("background task failed.") + if self._finalizer is not None: + self._finalizer() + finally: + self._future = NULL + + def cancel(self): + self._assert_unfinished() + dmnsn_future_cancel(self._future) + + def progress(self): + self._assert_unfinished() + return dmnsn_future_progress(self._future) + + def wait(self, progress): + self._assert_unfinished() + dmnsn_future_wait(self._future, progress) + + def _assert_unfinished(self): + if self._future == NULL: + raise RuntimeError("background task finished.") + +cdef Future _Future(dmnsn_future *future): + """Wrap a Future object around an existing dmnsn_future *.""" + cdef Future self = Future.__new__(Future) + self._future = future + return self + +########## +# Timers # +########## + +cdef class Timer: + """A timer for Dimension tasks.""" + cdef dmnsn_timer _timer + cdef bool _stopped + + def __init__(self): + """ + Create a Timer. + + Timing starts as soon as the object is created. + """ + self._stopped = False + dmnsn_timer_start(&self._timer) + + def stop(self): + """Stop the Timer.""" + if self._stopped: + raise RuntimeError("timer already stopped.") + + dmnsn_timer_stop(&self._timer) + self._stopped = True + + property real: + """Real (wall clock) time.""" + def __get__(self): + self._assert_stopped() + return self._timer.real + property user: + """User (CPU) time.""" + def __get__(self): + self._assert_stopped() + return self._timer.user + property system: + """System time.""" + def __get__(self): + self._assert_stopped() + return self._timer.system + + def __str__(self): + self._assert_stopped() + return "%.2fs (user: %.2fs; system: %.2fs)" % \ + (self._timer.real, self._timer.user, self._timer.system) + + def _assert_stopped(self): + if not self._stopped: + raise RuntimeError("timer still running.") + +cdef Timer _Timer(dmnsn_timer timer): + """Wrap a Timer object around a dmnsn_timer.""" + cdef Timer self = Timer.__new__(Timer) + self._timer = timer + self._stopped = True + return self + +############ +# Geometry # +############ + +cdef class Vector: + """A vector (or point or pseudovector) in 3D space.""" + cdef dmnsn_vector _v + + def __init__(self, *args, **kwargs): + """ + Create a Vector. + + Keyword arguments: + x -- The x coordinate + y -- The y coordinate + z -- The z coordinate + + Alternatively, you can pass another Vector, the value 0, or a tuple or other + sequence (x, y, z). + """ + if len(args) == 1: + if isinstance(args[0], Vector): + self._v = (args[0])._v + elif hasattr(args[0], "__iter__"): # Faster than try: ... except: + self._real_init(*args[0]) + elif args[0] == 0: + self._v = dmnsn_zero + else: + raise TypeError("expected a sequence or 0") + else: + self._real_init(*args, **kwargs) + + def _real_init(self, double x, double y, double z): + self._v = dmnsn_new_vector(x, y, z) + + property x: + """The x coordinate.""" + def __get__(self): + return self._v.x + property y: + """The y coordinate.""" + def __get__(self): + return self._v.y + property z: + """The z coordinate.""" + def __get__(self): + return self._v.z + + def __pos__(self): + return self + def __neg__(self): + return _Vector(dmnsn_vector_negate(self._v)) + def __nonzero__(self): + return dmnsn_vector_norm(self._v) >= dmnsn_epsilon + + def __add__(lhs, rhs): + return _Vector(dmnsn_vector_add(Vector(lhs)._v, Vector(rhs)._v)) + def __sub__(lhs, rhs): + return _Vector(dmnsn_vector_sub(Vector(lhs)._v, Vector(rhs)._v)) + def __mul__(lhs, rhs): + if isinstance(lhs, Vector): + return _Vector(dmnsn_vector_mul(rhs, (lhs)._v)) + else: + return _Vector(dmnsn_vector_mul(lhs, (rhs)._v)) + def __truediv__(Vector lhs not None, double rhs): + return _Vector(dmnsn_vector_div(lhs._v, rhs)) + + def __richcmp__(lhs, rhs, int op): + equal = (Vector(lhs) - Vector(rhs)).norm() < dmnsn_epsilon + if op == 2: # == + return equal + elif op == 3: # != + return not equal + else: + return NotImplemented + + def norm(self): + """Return the magnitude of the vector.""" + return dmnsn_vector_norm(self._v) + def normalized(self): + """Return the direction of the vector.""" + return _Vector(dmnsn_vector_normalized(self._v)) + + def __repr__(self): + return "dimension.Vector(%r, %r, %r)" % (self.x, self.y, self.z) + + def __str__(self): + return "<%s, %s, %s>" % (self.x, self.y, self.z) + +cdef Vector _Vector(dmnsn_vector v): + """Wrap a Vector object around a dmnsn_vector.""" + cdef Vector self = Vector.__new__(Vector) + self._v = v + return self + +def cross(Vector lhs not None, Vector rhs not None): + """Vector cross product.""" + return _Vector(dmnsn_vector_cross(lhs._v, rhs._v)) +def dot(Vector lhs not None, Vector rhs not None): + """Vector dot product.""" + return dmnsn_vector_dot(lhs._v, rhs._v) +def proj(Vector u not None, Vector d not None): + """Vector projection (of u onto d).""" + return _Vector(dmnsn_vector_proj(u._v, d._v)) + +X = _Vector(dmnsn_x) +Y = _Vector(dmnsn_y) +Z = _Vector(dmnsn_z) + +cdef class Matrix: + """An affine transformation matrix.""" + cdef dmnsn_matrix _m + + def __init__(self, + double a1, double a2, double a3, double a4, + double b1, double b2, double b3, double b4, + double c1, double c2, double c3, double c4): + """Create a Matrix.""" + self._m = dmnsn_new_matrix(a1, a2, a3, a4, + b1, b2, b3, b4, + c1, c2, c3, c4) + + def __nonzero__(self): + cdef double sum = 0.0 + for i in range(3): + for j in range(4): + sum += self._m.n[i][j] + return sqrt(sum) >= dmnsn_epsilon + + def __mul__(Matrix lhs not None, rhs): + if isinstance(rhs, Matrix): + return _Matrix(dmnsn_matrix_mul(lhs._m, (rhs)._m)) + else: + return _Vector(dmnsn_transform_point(lhs._m, (rhs)._v)) + + def __richcmp__(Matrix lhs not None, Matrix rhs not None, int op): + cdef double sum = 0.0 + for i in range(3): + for j in range(4): + diff = lhs._m.n[i][j] - rhs._m.n[i][j] + sum += diff*diff + equal = sqrt(sum) < dmnsn_epsilon + + if op == 2: # == + return equal + elif op == 3: # != + return not equal + else: + return NotImplemented + + def inverse(self): + """Return the inverse of a matrix.""" + return _Matrix(dmnsn_matrix_inverse(self._m)) + + def __repr__(self): + return \ + "dimension.Matrix(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)" % \ + (self._m.n[0][0], self._m.n[0][1], self._m.n[0][2], self._m.n[0][3], + self._m.n[1][0], self._m.n[1][1], self._m.n[1][2], self._m.n[1][3], + self._m.n[2][0], self._m.n[2][1], self._m.n[2][2], self._m.n[2][3]) + + def __str__(self): + return \ + "\n[%s\t%s\t%s\t%s]" \ + "\n[%s\t%s\t%s\t%s]" \ + "\n[%s\t%s\t%s\t%s]" \ + "\n[%s\t%s\t%s\t%s]" %\ + (self._m.n[0][0], self._m.n[0][1], self._m.n[0][2], self._m.n[0][3], + self._m.n[1][0], self._m.n[1][1], self._m.n[1][2], self._m.n[1][3], + self._m.n[2][0], self._m.n[2][1], self._m.n[2][2], self._m.n[2][3], + 0.0, 0.0, 0.0, 1.0) + +cdef Matrix _Matrix(dmnsn_matrix m): + """Wrap a Matrix object around a dmnsn_matrix.""" + cdef Matrix self = Matrix.__new__(Matrix) + self._m = m + return self + +def scale(*args, **kwargs): + """ + Return a scale transformation. + + Accepts the same arguments that Vector(...) does. The transformation scales + by a factor of x in the x direction, y in the y direction, and z in the z + direction. In particular, this means that scale(2*X) is probably a mistake, + as the y and z coordinates will disappear. + + Alternatively, a single argument may be passed, which specifies the scaling + factor in every component. + """ + cdef Vector s + try: + s = Vector(*args, **kwargs) + except: + s = args[0]*(X + Y + Z) + return _Matrix(dmnsn_scale_matrix(s._v)) +def translate(*args, **kwargs): + """ + Return a translation. + + Accepts the same arguments that Vector(...) does. + """ + return _Matrix(dmnsn_translation_matrix(Vector(*args, **kwargs)._v)) +def rotate(*args, **kwargs): + """ + Return a rotation. + + Accepts the same arguments that Vector(...) does. theta.norm() is the left- + handed angle of rotation, and theta.normalized() is the axis of rotation. + theta is specified in degrees. + """ + cdef Vector rad = dmnsn_radians(1.0)*Vector(*args, **kwargs) + return _Matrix(dmnsn_rotation_matrix(rad._v)) + +cdef class _Transformable: + def scale(self, *args, **kwargs): + """Scale. Equivalent to self.transform(scale(...)).""" + return self.transform(scale(*args, **kwargs)) + def translate(self, *args, **kwargs): + """Translate. Equivalent to self.transform(translate(...)).""" + return self.transform(translate(*args, **kwargs)) + def rotate(self, *args, **kwargs): + """Rotate. Equivalent to self.transform(rotate(...)).""" + return self.transform(rotate(*args, **kwargs)) + +########## +# Colors # +########## + +cdef class Color: + """ + An sRGB color. + + Note that 0.5*White == Color(0.5, 0.5, 0.5), which is not technically a half- + intensity white, due to sRGB gamma. Dimension handles the gamma correctly + when rendering, though. + """ + cdef dmnsn_color _c + cdef dmnsn_color _sRGB + + def __init__(self, *args, **kwargs): + """ + Create a Color. + + Keyword arguments: + red -- The red component + green -- The green component + blue -- The blue component + trans -- The transparency of the color, 0.0 meaning opaque (default 0.0) + filter -- How filtered the transparency is (default 0.0) + + Alternatively, you can pass another Color, a gray intensity like 0.5, or a + tuple or other sequence (red, green, blue[, trans[, filter]]). + """ + if len(args) == 1: + if isinstance(args[0], Color): + self._sRGB = (args[0])._sRGB + elif hasattr(args[0], "__iter__"): + self._real_init(*args[0]) + else: + self._sRGB = dmnsn_color_mul(args[0], dmnsn_white) + else: + self._real_init(*args, **kwargs) + + self._c = dmnsn_color_from_sRGB(self._sRGB) + + def _real_init(self, double red, double green, double blue, + double trans = 0.0, double filter = 0.0): + self._sRGB = dmnsn_new_color5(red, green, blue, trans, filter) + + property red: + """The red component.""" + def __get__(self): + return self._sRGB.R + property green: + """The green component.""" + def __get__(self): + return self._sRGB.G + property blue: + """The blue component.""" + def __get__(self): + return self._sRGB.B + property trans: + """The transparency of the color.""" + def __get__(self): + return self._sRGB.trans + property filter: + """How filtered the transparency is.""" + def __get__(self): + return self._sRGB.filter + + def __nonzero__(self): + """Return whether a color is not black.""" + return not dmnsn_color_is_black(self._c) + + def __add__(lhs, rhs): + return _sRGBColor(dmnsn_color_add(Color(lhs)._sRGB, Color(rhs)._sRGB)) + def __mul__(lhs, rhs): + if isinstance(lhs, Color): + return _sRGBColor(dmnsn_color_mul(rhs, (lhs)._sRGB)) + else: + return _sRGBColor(dmnsn_color_mul(lhs, (rhs)._sRGB)) + + def __richcmp__(lhs, rhs, int op): + cdef clhs = Color(lhs) + cdef crhs = Color(rhs) + + cdef double rdiff = clhs.red - crhs.red + cdef double gdiff = clhs.green - crhs.green + cdef double bdiff = clhs.blue - crhs.blue + cdef double tdiff = clhs.trans - crhs.trans + cdef double fdiff = clhs.filter - crhs.filter + cdef double sum = rdiff*rdiff + gdiff*gdiff + bdiff*bdiff \ + + tdiff*tdiff + fdiff*fdiff + equal = sqrt(sum) < dmnsn_epsilon + if op == 2: # == + return equal + elif op == 3: # != + return not equal + else: + return NotImplemented + + def __repr__(self): + return "dimension.Color(%r, %r, %r, %r, %r)" % \ + (self.red, self.green, self.blue, self.trans, self.filter) + + def __str__(self): + if self.trans >= dmnsn_epsilon: + return "" % \ + (self.red, self.green, self.blue, self.trans, self.filter) + else: + return "" % \ + (self.red, self.green, self.blue) + +cdef Color _sRGBColor(dmnsn_color sRGB): + """Wrap a color object around a dmnsn_color already in sRGB.""" + cdef Color self = Color.__new__(Color) + self._sRGB = sRGB + self._c = dmnsn_color_from_sRGB(sRGB) + return self + +cdef Color _Color(dmnsn_color c): + """Wrap a Color object around a dmnsn_color.""" + cdef Color self = Color.__new__(Color) + self._c = c + self._sRGB = dmnsn_color_to_sRGB(c) + return self + +Black = _Color(dmnsn_black) +White = _Color(dmnsn_white) +Clear = _Color(dmnsn_clear) +Red = _Color(dmnsn_red) +Green = _Color(dmnsn_green) +Blue = _Color(dmnsn_blue) +Magenta = _Color(dmnsn_magenta) +Orange = _Color(dmnsn_orange) +Yellow = _Color(dmnsn_yellow) +Cyan = _Color(dmnsn_cyan) + +############ +# Canvases # +############ + +cdef class Canvas: + """A rendering target.""" + cdef dmnsn_canvas *_canvas + + def __init__(self, width, height): + """ + Create a Canvas. + + Keyword arguments: + width -- the width of the canvas + height -- the height of the canvas + """ + self._canvas = dmnsn_new_canvas(width, height) + self.clear(Black) + + def __dealloc__(self): + dmnsn_delete_canvas(self._canvas) + + property width: + """The width of the canvas.""" + def __get__(self): + return self._canvas.width + property height: + """The height of the canvas.""" + def __get__(self): + return self._canvas.height + + def __len__(self): + """The width of the canvas.""" + return self.width + def __getitem__(self, int x): + """Get a column of the canvas.""" + if x < 0 or x >= self.width: + raise IndexError("x coordinate out of bounds.") + return _CanvasProxy(self, x) + + def optimize_PNG(self): + """Optimize a canvas for PNG output.""" + if dmnsn_png_optimize_canvas(self._canvas) != 0: + _raise_OSError() + + def optimize_GL(self): + """Optimize a canvas for OpenGL output.""" + if dmnsn_gl_optimize_canvas(self._canvas) != 0: + _raise_OSError() + + def clear(self, c): + """Clear a canvas with a solid color.""" + dmnsn_canvas_clear(self._canvas, Color(c)._c) + + def write_PNG(self, path): + """Export the canvas as a PNG file.""" + self.write_PNG_async(path).join() + def write_PNG_async(self, path): + """Export the canvas as a PNG file, in the background.""" + bpath = path.encode("UTF-8") + cdef char *cpath = bpath + cdef FILE *file = fopen(cpath, "wb") + if file == NULL: + _raise_OSError(path) + + def finalize(): + if fclose(file) != 0: + _raise_OSError() + + cdef dmnsn_future *future = dmnsn_png_write_canvas_async(self._canvas, file) + + try: + if future == NULL: + _raise_OSError() + + ret = _Future(future) + ret._finalizer = finalize + return ret + except: + finalize() + raise + + def draw_GL(self): + """Export the canvas to the current OpenGL context.""" + if dmnsn_gl_write_canvas(self._canvas) != 0: + _raise_OSError() + +cdef class _CanvasProxy: + cdef dmnsn_canvas *_canvas + cdef int _x + + def __init__(self, Canvas canvas not None, int x): + self._canvas = canvas._canvas + self._x = x + + def __len__(self): + """The height of the canvas.""" + return self._canvas.height + def __getitem__(self, int y): + self._bounds_check(y) + return _Color(dmnsn_canvas_get_pixel(self._canvas, self._x, y)) + def __setitem__(self, int y, color): + self._bounds_check(y) + dmnsn_canvas_set_pixel(self._canvas, self._x, y, Color(color)._c) + + def _bounds_check(self, int y): + if y < 0 or y >= self._canvas.height: + raise IndexError("y coordinate out of bounds.") + +############ +# Patterns # +############ + +cdef class Pattern: + """A function which maps points in 3D space to scalar values.""" + cdef dmnsn_pattern *_pattern + + def __cinit__(self): + self._pattern = NULL + + def __dealloc__(self): + dmnsn_delete_pattern(self._pattern) + +cdef class Checker(Pattern): + """A checkerboard pattern.""" + def __init__(self): + self._pattern = dmnsn_new_checker_pattern() + Pattern.__init__(self) + +cdef class Gradient(Pattern): + """A gradient pattern.""" + def __init__(self, orientation): + """ + Create a gradient pattern. + + Keyword arguments: + orientation -- The direction of the linear gradient. + """ + self._pattern = dmnsn_new_gradient_pattern(Vector(orientation)._v) + Pattern.__init__(self) + +cdef class Leopard(Pattern): + """A leopard pattern.""" + def __init__(self): + self._pattern = dmnsn_new_leopard_pattern() + Pattern.__init__(self) + +############ +# Pigments # +############ + +cdef class Pigment(_Transformable): + """Object surface coloring.""" + cdef dmnsn_pigment *_pigment + + def __cinit__(self): + self._pigment = NULL + + def __init__(self, quick_color = None): + """ + Create a Pigment. + + With an arguement, create a solid pigment of that color. Otherwise, create + a base Pigment. + + Keyword arguments: + quick_color -- the object's quick color for low-quality renders + """ + if quick_color is not None: + if self._pigment == NULL: + if isinstance(quick_color, Pigment): + self._pigment = (quick_color)._pigment + DMNSN_INCREF(self._pigment) + else: + self._pigment = dmnsn_new_solid_pigment(Color(quick_color)._c) + else: + self._pigment.quick_color = Color(quick_color)._c + + def __dealloc__(self): + dmnsn_delete_pigment(self._pigment) + + def transform(self, Matrix trans not None): + """Transform a pigment.""" + self._pigment.trans = dmnsn_matrix_mul(trans._m, self._pigment.trans) + return self + +cdef Pigment _Pigment(dmnsn_pigment *pigment): + """Wrap a Pigment object around a dmnsn_pigment *.""" + cdef Pigment self = Pigment.__new__(Pigment) + self._pigment = pigment + DMNSN_INCREF(self._pigment) + return self + +cdef class ImageMap(Pigment): + """An image-mapped pigment.""" + def __init__(self, path, *args, **kwargs): + """ + Create an ImageMap. + + Keyword arguments: + path -- the path of the PNG file to open + """ + bpath = path.encode("UTF-8") + cdef char *cpath = bpath + cdef FILE *file = fopen(cpath, "rb") + if file == NULL: + _raise_OSError(path) + cdef dmnsn_canvas *canvas = dmnsn_png_read_canvas(file) + if canvas == NULL: + _raise_OSError(path) + if fclose(file) != 0: + _raise_OSError() + + self._pigment = dmnsn_new_canvas_pigment(canvas) + Pigment.__init__(self, *args, **kwargs) + +cdef class PigmentMap(Pigment): + """A pigment map.""" + def __init__(self, Pattern pattern not None, map, bool sRGB not None = True, + *args, **kwargs): + """ + Create a PigmentMap. + + Keyword arguments: + pattern -- the pattern to use for the mapping + map -- a dictionary of the form { val1: color1, val2: pigment2, ... }, + or a list of the form [color1, pigment2, ...] + sRGB -- whether the gradients should be in sRGB or linear space + (default True) + """ + cdef dmnsn_map *pigment_map = dmnsn_new_pigment_map() + cdef dmnsn_pigment *real_pigment + if hasattr(map, "items"): + for i, pigment in map.items(): + pigment = Pigment(pigment) + real_pigment = (pigment)._pigment + DMNSN_INCREF(real_pigment) + dmnsn_map_add_entry(pigment_map, i, &real_pigment) + else: + for i, pigment in enumerate(map): + pigment = Pigment(pigment) + real_pigment = (pigment)._pigment + DMNSN_INCREF(real_pigment) + dmnsn_map_add_entry(pigment_map, i/len(map), &real_pigment) + + cdef dmnsn_pigment_map_flags flags + if sRGB: + flags = DMNSN_PIGMENT_MAP_SRGB + else: + flags = DMNSN_PIGMENT_MAP_REGULAR + + DMNSN_INCREF(pattern._pattern) + self._pigment = dmnsn_new_pigment_map_pigment(pattern._pattern, pigment_map, + flags) + Pigment.__init__(self, *args, **kwargs) + +############ +# Finishes # +############ + +cdef class Finish: + """Object surface qualities.""" + cdef dmnsn_finish _finish + + def __cinit__(self): + self._finish = dmnsn_new_finish() + + def __dealloc__(self): + dmnsn_delete_finish(self._finish) + + def __add__(Finish lhs not None, Finish rhs not None): + """ + Combine two finishes. + + In lhs + rhs, the attributes of rhs override those of lhs if any conflict; + thus, Ambient(0.1) + Ambient(0.2) is the same as Ambient(0.2) + """ + cdef Finish ret = Finish() + dmnsn_finish_cascade(&lhs._finish, &ret._finish) + dmnsn_finish_cascade(&rhs._finish, &ret._finish) # rhs gets priority + return ret + +cdef Finish _Finish(dmnsn_finish finish): + """Wrap a Finish object around a dmnsn_finish.""" + cdef Finish self = Finish.__new__(Finish) + self._finish = finish + dmnsn_finish_incref(&self._finish) + return self + +cdef class Ambient(Finish): + """Ambient light reflected.""" + def __init__(self, color): + """ + Create an Ambient finish. + + Keyword arguments: + color -- the color and intensity of the ambient light + """ + self._finish.ambient = dmnsn_new_basic_ambient(Color(color)._c) + +cdef class Diffuse(Finish): + """Lambertian diffuse reflection.""" + def __init__(self, double diffuse): + """ + Create a Diffuse finish. + + Keyword arguments: + diffuse -- the intensity of the diffuse reflection + """ + cdef dmnsn_color gray = dmnsn_color_mul(diffuse, dmnsn_white) + diffuse = dmnsn_color_intensity(dmnsn_color_from_sRGB(gray)) + self._finish.diffuse = dmnsn_new_lambertian(diffuse) + +cdef class Phong(Finish): + """Phong specular highlight.""" + def __init__(self, double strength, double size = 40.0): + """ + Create a Phong highlight. + """ + self._finish.specular = dmnsn_new_phong(strength, size) + +cdef class Reflection(Finish): + """Reflective finish.""" + def __init__(self, min, max = None, double falloff = 1.0): + """ + Create a Reflection. + + Keyword arguments: + min -- color and intensity of reflection at indirect angles + max -- color and intensity of reflection at direct angles (default: min) + falloff -- exponent for reflection falloff (default: 1.0) + """ + if max is None: + max = min + + # Use sRGB value because Reflection(0.5) should really mean "reflect half + # the light" + self._finish.reflection = dmnsn_new_basic_reflection(Color(min)._sRGB, + Color(max)._sRGB, + falloff) + +############ +# Textures # +############ + +cdef class Texture(_Transformable): + """Object surface properties.""" + cdef dmnsn_texture *_texture + + def __init__(self, pigment = None, Finish finish = None): + """ + Create a Texture. + + Keyword arguments: + pigment -- the Pigment for the texture, or a color (default: None) + finish -- the Finish for the texture (default: None) + """ + self._texture = dmnsn_new_texture() + + if pigment is not None: + self.pigment = Pigment(pigment) + + if finish is not None: + self.finish = finish + + def __dealloc__(self): + dmnsn_delete_texture(self._texture) + + property pigment: + """The texture's pigment.""" + def __get__(self): + if self._texture.pigment == NULL: + return None + else: + return _Pigment(self._texture.pigment) + def __set__(self, pigment): + dmnsn_delete_pigment(self._texture.pigment) + cdef Pigment real_pigment + if pigment is None: + self._texture.pigment = NULL + else: + real_pigment = Pigment(pigment) + self._texture.pigment = real_pigment._pigment + DMNSN_INCREF(self._texture.pigment) + + property finish: + """The texture's finish.""" + def __get__(self): + return _Finish(self._texture.finish) + def __set__(self, Finish finish not None): + dmnsn_delete_finish(self._texture.finish) + self._texture.finish = finish._finish + dmnsn_finish_incref(&self._texture.finish) + + def transform(self, Matrix trans not None): + """Transform a texture.""" + self._texture.trans = dmnsn_matrix_mul(trans._m, self._texture.trans) + return self + +cdef Texture _Texture(dmnsn_texture *texture): + """Wrap a Texture object around a dmnsn_texture *.""" + cdef Texture self = Texture.__new__(Texture) + self._texture = texture + DMNSN_INCREF(self._texture) + return self + +############# +# Interiors # +############# + +cdef class Interior: + """Object interior properties.""" + cdef dmnsn_interior *_interior + + def __init__(self, double ior = 1.0): + """ + Create an Interior. + + Keyword arguments: + ior -- index of reflection + """ + self._interior = dmnsn_new_interior() + self._interior.ior = ior + + def __dealloc__(self): + dmnsn_delete_interior(self._interior) + + property ior: + """Index of reflection.""" + def __get__(self): + return self._interior.ior + def __set__(self, double ior): + self._interior.ior = ior + +cdef Interior _Interior(dmnsn_interior *interior): + """Wrap an Interior object around a dmnsn_interior *.""" + cdef Interior self = Interior.__new__(Interior) + self._interior = interior + DMNSN_INCREF(self._interior) + return self + +########### +# Objects # +########### + +cdef class Object(_Transformable): + """Physical objects.""" + cdef dmnsn_object *_object + + def __cinit__(self): + self._object = NULL + + def __init__(self, Texture texture = None, pigment = None, + Finish finish = None, Interior interior = None): + """ + Initialize an Object. + + Keyword arguments: + texture -- the object's Texture + pigment -- shorthand for specifying the texture's pigment + finish -- shorthand for specifying the texture's finish + interior -- the object's Interior + """ + if self._object == NULL: + raise TypeError("attempt to initialize base Object") + + self.texture = texture + if pigment is not None: + if texture is not None: + raise TypeError("both texture and pigment specified.") + else: + if self.texture is None: + self.texture = Texture() + self.texture.pigment = pigment + + if finish is not None: + if texture is not None: + raise TypeError("both texture and finish specified.") + else: + if self.texture is None: + self.texture = Texture() + self.texture.finish = finish + + if interior is not None: + self.interior = interior + + def __dealloc__(self): + dmnsn_delete_object(self._object) + + property texture: + """The object's Texture.""" + def __get__(self): + if self._object.texture == NULL: + return None + else: + return _Texture(self._object.texture) + def __set__(self, Texture texture): + dmnsn_delete_texture(self._object.texture) + if texture is None: + self._object.texture = NULL + else: + self._object.texture = texture._texture + DMNSN_INCREF(self._object.texture) + + property interior: + """The object's Interior.""" + def __get__(self): + return _Interior(self._object.interior) + def __set__(self, Interior interior not None): + self._object.interior = interior._interior + DMNSN_INCREF(self._object.interior) + + def transform(self, Matrix trans not None): + """Transform an object.""" + self._object.trans = dmnsn_matrix_mul(trans._m, self._object.trans) + return self + + # Transform an object without affecting the texture + cdef _intrinsic_transform(self, Matrix trans): + self._object.intrinsic_trans = dmnsn_matrix_mul( + trans._m, + self._object.intrinsic_trans + ) + +cdef class Triangle(Object): + """A triangle.""" + def __init__(self, a, b, c, *args, **kwargs): + """ + Create a Triangle. + + Keyword arguments: + a, b, c -- the corners of the triangle + + Additionally, Triangle() accepts any arguments that Object() accepts. + """ + self._object = dmnsn_new_triangle(Vector(a)._v, Vector(b)._v, Vector(c)._v) + Object.__init__(self, *args, **kwargs) + +cdef class Plane(Object): + """A plane.""" + def __init__(self, normal, double distance, *args, **kwargs): + """ + Create a Plane. + + Keyword arguments: + normal -- a vector perpendicular to the plane + distance -- the distance from the origin to the plane, in the direction of + normal + + Additionally, Plane() accepts any arguments that Object() accepts. + """ + self._object = dmnsn_new_plane(Vector(normal)._v) + Object.__init__(self, *args, **kwargs) + + self._intrinsic_transform(translate(distance*Vector(normal))) + +cdef class Sphere(Object): + """A sphere.""" + def __init__(self, center, double radius, *args, **kwargs): + """ + Create a Sphere. + + Keyword arguments: + center -- the center of the sphere + radius -- the radius of the sphere + + Additionally, Sphere() accepts any arguments that Object() accepts. + """ + self._object = dmnsn_new_sphere() + Object.__init__(self, *args, **kwargs) + + cdef Matrix trans = translate(Vector(center)) + trans *= scale(radius, radius, radius) + self._intrinsic_transform(trans) + +cdef class Box(Object): + """An axis-aligned rectangular prism.""" + def __init__(self, min, max, *args, **kwargs): + """ + Create a Box. + + Keyword arguments: + min -- the coordinate-wise minimal extent of the box + max -- the coordinate-wise maximal extent of the box + + Additionally, Box() accepts any arguments that Object() accepts. + """ + self._object = dmnsn_new_cube() + Object.__init__(self, *args, **kwargs) + + min = Vector(min) + max = Vector(max) + cdef Matrix trans = translate((max + min)/2) + trans *= scale((max - min)/2) + self._intrinsic_transform(trans) + +cdef class Cone(Object): + """A cone or cone slice.""" + def __init__(self, bottom, double bottom_radius, top, double top_radius = 0.0, + bool open not None = False, *args, **kwargs): + """ + Create a Cone. + + Keyword arguments: + bottom -- the location of the bottom of the cone + bottom_radius -- the radius at the bottom of the cone + top -- the location of the top of the cone + top_radius -- the radius at the top of the cone/cone slice (default 0.0) + open -- whether to draw the cone cap(s) + + Additionally, Cone() accepts any arguments that Object() accepts. + """ + self._object = dmnsn_new_cone(bottom_radius, top_radius, open) + Object.__init__(self, *args, **kwargs) + + # Lift the cone to start at the origin, then scale, rotate, and translate + # properly + + cdef Vector dir = Vector(top) - Vector(bottom) + + cdef Matrix trans = translate(Y) + trans = scale(1.0, dir.norm()/2, 1.0)*trans + trans = _Matrix(dmnsn_alignment_matrix(dmnsn_y, dir._v, dmnsn_x, dmnsn_z))*trans + trans = translate(bottom)*trans + + self._intrinsic_transform(trans) + +cdef class Cylinder(Cone): + """A cylinder.""" + def __init__(self, bottom, top, double radius, bool open not None = False, + *args, **kwargs): + """ + Create a Cylinder. + + Keyword arguments: + bottom -- the location of the bottom of the cylinder + top -- the location of the top of the cylinder + radius -- the radius of the cylinder + open -- whether to draw the cylinder caps + + Additionally, Cylinder() accepts any arguments that Object() accepts. + """ + Cone.__init__(self, + bottom = bottom, bottom_radius = radius, + top = top, top_radius = radius, + open = open, + *args, **kwargs) + +cdef class Torus(Object): + """A torus.""" + def __init__(self, double major_radius, double minor_radius, *args, **kwargs): + """ + Create a Torus. + + Keyword arguments: + major_radius -- the distance from the center of the torus to the center of + a circular cross-section of the torus + minor_radius -- the radius of the circular cross-sections of the torus + + Additionally, Torus() accepts any arguments that Object() accepts. + """ + self._object = dmnsn_new_torus(major_radius, minor_radius) + Object.__init__(self, *args, **kwargs) + +cdef class Union(Object): + """A CSG union.""" + def __init__(self, objects, *args, **kwargs): + """ + Create a Union. + + Keyword arguments: + objects -- a list of objects to include in the union + + Additionally, Union() accepts any arguments that Object() accepts. + """ + if len(objects) < 2: + raise TypeError("expected a list of two or more Objects") + + cdef dmnsn_array *array = dmnsn_new_array(sizeof(dmnsn_object *)) + cdef dmnsn_object *o + + try: + for obj in objects: + o = (obj)._object + DMNSN_INCREF(o) + dmnsn_array_push(array, &o) + + self._object = dmnsn_new_csg_union(array) + finally: + dmnsn_delete_array(array) + + Object.__init__(self, *args, **kwargs) + +cdef class Intersection(Object): + """A CSG intersection.""" + def __init__(self, objects, *args, **kwargs): + """ + Create an Intersection. + + Keyword arguments: + objects -- a list of objects to include in the intersection + + Additionally, Intersection() accepts any arguments that Object() accepts. + """ + if len(objects) < 2: + raise TypeError("expected a list of two or more Objects") + + cdef dmnsn_object *o + + for obj in objects: + if self._object == NULL: + self._object = (obj)._object + DMNSN_INCREF(self._object) + else: + o = (obj)._object + DMNSN_INCREF(o) + self._object = dmnsn_new_csg_intersection(self._object, o) + + Object.__init__(self, *args, **kwargs) + +cdef class Difference(Object): + """A CSG difference.""" + def __init__(self, objects, *args, **kwargs): + """ + Create a Difference. + + Keyword arguments: + objects -- a list of objects to include in the difference + + Additionally, Difference() accepts any arguments that Object() accepts. + """ + if len(objects) < 2: + raise TypeError("expected a list of two or more Objects") + + cdef dmnsn_object *o + + for obj in objects: + if self._object == NULL: + self._object = (obj)._object + DMNSN_INCREF(self._object) + else: + o = (obj)._object + DMNSN_INCREF(o) + self._object = dmnsn_new_csg_difference(self._object, o) + + Object.__init__(self, *args, **kwargs) + +cdef class Merge(Object): + """A CSG merge.""" + def __init__(self, objects, *args, **kwargs): + """ + Create a Merge. + + Keyword arguments: + objects -- a list of objects to include in the merge + + Additionally, Merge() accepts any arguments that Object() accepts. + """ + if len(objects) < 2: + raise TypeError("expected a list of two or more Objects") + + cdef dmnsn_object *o + + for obj in objects: + if self._object == NULL: + self._object = (obj)._object + DMNSN_INCREF(self._object) + else: + o = (obj)._object + DMNSN_INCREF(o) + self._object = dmnsn_new_csg_merge(self._object, o) + + Object.__init__(self, *args, **kwargs) + +########## +# Lights # +########## + +cdef class Light: + """A light.""" + cdef dmnsn_light *_light + + def __dealloc__(self): + dmnsn_delete_light(self._light) + +cdef class PointLight(Light): + """A point light.""" + def __init__(self, location, color): + """ + Create a PointLight. + + Keyword arguments: + location -- the origin of the light rays + color -- the color and intensity of the light + """ + # Take the sRGB component because "color = 0.5*White" should really mean + # a half-intensity white light + self._light = dmnsn_new_point_light(Vector(location)._v, Color(color)._sRGB) + Light.__init__(self) + +########### +# Cameras # +########### + +cdef class Camera(_Transformable): + """A camera.""" + cdef dmnsn_camera *_camera + + def __cinit__(self): + self._camera = NULL + + def __init__(self): + if self._camera == NULL: + raise TypeError("attempt to initialize base Camera") + + def __dealloc__(self): + dmnsn_delete_camera(self._camera) + + def transform(self, Matrix trans not None): + """Transform a camera.""" + self._camera.trans = dmnsn_matrix_mul(trans._m, self._camera.trans) + return self + +cdef class PerspectiveCamera(Camera): + """A regular perspective camera.""" + def __init__(self, location = -Z, look_at = 0, sky = Y, + angle = dmnsn_degrees(atan(1.0))): + """ + Create a PerspectiveCamera. + + Keyword arguments: + location -- the location of the camera (default: -Z) + look_at -- where to aim the camera (default: 0) + sky -- the direction of the top of the camera (default: Y) + angle -- the field of view angle (from bottom to top) (default: 45) + """ + self._camera = dmnsn_new_perspective_camera() + Camera.__init__(self) + + # Apply the field of view angle + self.scale(tan(dmnsn_radians(angle))*(X + Y) + Z) + + cdef Vector dir = Vector(look_at) - Vector(location) + cdef Vector vsky = Vector(sky) + + # Line up the top of the viewport with the sky vector + cdef Matrix align_sky = _Matrix(dmnsn_alignment_matrix(dmnsn_y, vsky._v, + dmnsn_z, dmnsn_x)) + cdef Vector forward = align_sky*Z + cdef Vector right = align_sky*X + + # Line up the look at point with look_at + self.transform(_Matrix(dmnsn_alignment_matrix(forward._v, dir._v, + vsky._v, right._v))) + + # Move the camera into position + self.translate(Vector(location)) + +########## +# Scenes # +########## + +cdef class Scene: + """An entire scene.""" + cdef dmnsn_scene *_scene + + def __init__(self, Canvas canvas not None, objects, lights, + Camera camera not None): + """ + Create a Scene. + + Keyword arguments: + canvas -- the rendering Canvas + objects -- the list of objects in the scene + lights -- the list of lights in the scene + camera -- the camera for the scene + """ + self._scene = dmnsn_new_scene() + + self._scene.canvas = canvas._canvas + DMNSN_INCREF(self._scene.canvas) + self.outer_width = self._scene.canvas.width + self.outer_height = self._scene.canvas.height + self.background = Black + + cdef dmnsn_object *o + for obj in objects: + o = (obj)._object + DMNSN_INCREF(o) + dmnsn_array_push(self._scene.objects, &o) + + cdef dmnsn_light *l + for light in lights: + l = (light)._light + DMNSN_INCREF(l) + dmnsn_array_push(self._scene.lights, &l) + + self._scene.camera = camera._camera + DMNSN_INCREF(self._scene.camera) + + # Subregion render support + property region_x: + """The x-coordinate of the subregion in the broader image.""" + def __get__(self): + return self._scene.region_x + def __set__(self, x): + self._scene.region_x = x + property region_y: + """The y-coordinate of the subregion in the broader image.""" + def __get__(self): + return self._scene.region_y + def __set__(self, y): + self._scene.region_y = y + property outer_width: + """The width of the broader image.""" + def __get__(self): + return self._scene.outer_width + def __set__(self, width): + self._scene.outer_width = width + property outer_height: + """The height of the broader image.""" + def __get__(self): + return self._scene.outer_height + def __set__(self, height): + self._scene.outer_height = height + + property default_texture: + """The default Texture for objects.""" + def __get__(self): + return _Texture(self._scene.default_texture) + def __set__(self, Texture texture not None): + dmnsn_delete_texture(self._scene.default_texture) + self._scene.default_texture = texture._texture + DMNSN_INCREF(self._scene.default_texture) + property default_interior: + """The default Interior for objects.""" + def __get__(self): + return _Interior(self._scene.default_interior) + def __set__(self, Interior interior not None): + dmnsn_delete_interior(self._scene.default_interior) + self._scene.default_interior = interior._interior + DMNSN_INCREF(self._scene.default_interior) + + property background: + """The background pigment of the scene (default: Black).""" + def __get__(self): + return _Pigment(self._scene.background) + def __set__(self, pigment): + dmnsn_delete_pigment(self._scene.background) + cdef Pigment real_pigment = Pigment(pigment) + self._scene.background = real_pigment._pigment + DMNSN_INCREF(self._scene.background) + + property adc_bailout: + """The adaptive depth control bailout (default: 1/255).""" + def __get__(self): + return self._scene.adc_bailout + def __set__(self, double bailout): + self._scene.adc_bailout = bailout + + property recursion_limit: + """The rendering recursion limit (default: 5).""" + def __get__(self): + return self._scene.reclimit + def __set__(self, level): + self._scene.reclimit = level + + property nthreads: + """The number of threads to use for the render.""" + def __get__(self): + return self._scene.nthreads + def __set__(self, n): + if n <= 0: + raise ValueError("%d is an invalid thread count." % n) + self._scene.nthreads = n + + property quality: + """The render quality.""" + def __get__(self): + return _quality_to_string(self._scene.quality) + def __set__(self, q): + self._scene.quality = _string_to_quality(q) + + property bounding_timer: + """The Timer for building the bounding hierarchy.""" + def __get__(self): + return _Timer(self._scene.bounding_timer) + property render_timer: + """The Timer for the actual render.""" + def __get__(self): + return _Timer(self._scene.render_timer) + + def ray_trace(self): + """Render the scene.""" + self.ray_trace_async().join() + def ray_trace_async(self): + """Render the scene, in the background.""" + # Account for image dimensions in the camera + # Do this here so subregion renders can tell us the broader image size + self._scene.camera.trans = dmnsn_matrix_mul( + self._scene.camera.trans, + dmnsn_scale_matrix( + dmnsn_new_vector( + self.outer_width/self.outer_height, + 1.0, + 1.0 + ) + ) + ) + # Ensure the default texture is complete + cdef Texture default = Texture(pigment = Black) + dmnsn_texture_cascade(default._texture, &self._scene.default_texture) + return _Future(dmnsn_ray_trace_async(self._scene)) + + def __dealloc__(self): + dmnsn_delete_scene(self._scene) + +def _quality_to_string(int quality): + cdef str s = "" + + if quality & DMNSN_RENDER_PIGMENT: + s += 'p' + if quality & DMNSN_RENDER_LIGHTS: + s += 'l' + if quality & DMNSN_RENDER_FINISH: + s += 'f' + if quality & DMNSN_RENDER_TRANSPARENCY: + s += 't' + if quality & DMNSN_RENDER_REFLECTION: + s += 'r' + + if s == "": + return "0" + else: + return s + +def _string_to_quality(str quality not None): + cdef int q = DMNSN_RENDER_NONE + inverse = False + + if quality == "": + return q + + if quality[0] == '^': + inverse = True + quality = quality[1:] + + if quality != "0": + while len(quality) > 0: + ch = quality[0] + quality = quality[1:] + + if ch == 'p': + flag = DMNSN_RENDER_PIGMENT + elif ch == 'l': + flag = DMNSN_RENDER_LIGHTS + elif ch == 'f': + flag = DMNSN_RENDER_FINISH + elif ch == 't': + flag = DMNSN_RENDER_TRANSPARENCY + elif ch == 'r': + flag = DMNSN_RENDER_REFLECTION + else: + raise ValueError("unknown quality flag '%c'" % ch) + + if q & flag: + raise ValueError("flag '%c' specified twice" % ch) + else: + q |= flag + + if inverse: + q = ~q + + return q -- cgit v1.2.3