Source code for storylines.proj

# Copyright (C) 2016-2024 Jan Berges
# This program is free software under the terms of the BSD Zero Clause License.

"""Project 3D object onto plane."""

from __future__ import division

import re

from .calc import divide, subtract, cross, dot, length, distance

[docs] def projection( r=[0.0, 0.0, 0.0], # object R=[0.0, -1.0, 0.0], # observer T=[0.0, 0.0, 0.0], # target U=[0.0, 0.0, 1.0], # up ): """Project 3D point onto 2D screen. Parameters ---------- T : list of float Object position. R : list of float Observer position. T : list of float Viewing direction (from observer). U : list of float Vertical direction. Returns ------- list of float x and y position as well as proximity factor z. """ # viewing direction: Z = subtract(T, R) Z = divide(Z, length(Z)) # horizontal screen direction: X = cross(Z, U) X = divide(X, length(X)) # vertical screen direction: Y = cross(X, Z) # observer-object distance vector: D = subtract(r, R) # observer-object distance (hypotenuse): hyp = length(D) # projection onto viewing direction (adjacent leg): adj = dot(D, Z) # secant of angle of object w.r.t. viewing direction: sec = hyp / adj # horizontal screen coordinate: x = dot(X, D) / adj # vertical screen coordinate: y = dot(Y, D) / adj # magnification factor ("zoom", "z-index"): z = sec / adj return [x, y, z]
[docs] def project(objects, by_distance=True, return_cosines=False, return_order=False, R=[0.0, -1.0, 0.0], *args, **kwargs): """Project list of 3D objects onto 2D screen. Line width, mark sizes, and length in angle brackets are scaled according to the distance from the observer. Parameters ---------- objects : list of tuple List of objects. Each object is represented by a tuple, which consists of a list of three-tuples ``(x, y, z)`` and a style dictionary. by_distance : bool, default True Sort the objects by distance so that close object overlay remote objects? return_cosines : bool, default False Calculate cosines of angles between objects and viewing direction? return_order : bool, default False Also return sorting order as list of indices? R : list of float, optional Observer position. *args, **kwargs Arguments passed to `projection`. Returns ------- list of tuple Objects in same format, but sorted with transformed coordinates and adjusted styles. list of float, optional Cosines of angles between objects and viewing direction. list of int, optional Sorting order. """ if by_distance: distances = [distance(R, [sum(x) / len(x) for x in zip(*coordinates)]) for coordinates, style in objects] if return_cosines: cosines = [] for coordinates, style in objects: view = subtract(R, [sum(x) / len(x) for x in zip(*coordinates)]) if len(coordinates) < 2: normal = view elif len(coordinates) == 2: normal = cross(subtract(coordinates[1], coordinates[0]), cross(subtract(coordinates[1], coordinates[0]), view)) else: normal = cross(subtract(coordinates[1], coordinates[0]), subtract(coordinates[2], coordinates[0])) cosines.append(abs(dot(normal, view)) / (length(normal) * length(view))) objects = [([projection(coordinate, R=R, *args, **kwargs) for coordinate in coordinates], style.copy()) for coordinates, style in objects] zoom = [sum(coordinate[2] for coordinate in coordinates) / len(coordinates) for coordinates, style in objects] for n, (coordinates, style) in enumerate(objects): for option in 'line_width', 'mark_size': if isinstance(style.get(option), (float, int)): style[option] *= zoom[n] for option in style: if isinstance(style[option], str): style[option] = re.sub('(?<=<)([\\d.]+)(?=>)', lambda match: '%.3f' % (float(match.group(1)) * zoom[n]), style[option]) if by_distance: order = sorted(range(len(distances)), key=lambda n: -distances[n]) objects = [objects[n] for n in order] if return_order: if return_cosines: return objects, cosines, order return objects, order if return_cosines: return objects, cosines return objects