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. --- 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 +- 6 files changed, 301 insertions(+), 233 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 (limited to 'dimension') 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) -- cgit v1.2.3