import numpy as np
import ddg.conversion.nets.geometry.quadrics
import ddg.datastructures.halfedge.get as get
import ddg.datastructures.nets as nets
import ddg.datastructures.nets.conversion
from ddg._error_messages import should_never_happen
from ddg.conversion.nets.geometry.spheres import sphere_to_smooth_net
[docs]def from_discrete_net(net, /):
"""Compute all points and faces of a discrete net.
Parameters
----------
net : ddg.datastructures.nets.net.DiscreteNet
A net with `net.domain.bounded == True`.
Returns
-------
tuple of numpy.ndarray and numpy.ndarray
The first array is of shape `(n, d)` and comprises of the `n`
points in R^d of the net.
The second array is of shape `(m, k)` and comprises of the `m`
faces of `net.domain.face_data`.
Note that `k` isn't necessarily equal to 3.
See Also
--------
ddg.conversion.arrays.from_discrete_curve
"""
if not net.domain.bounded:
raise ValueError(f"{net.domain.bounded = } must be True")
return np.array(list(net[net.domain])), np.array(list(net.domain.face_data))
[docs]def from_discrete_curve(curve, /):
"""Compute all points of a discrete curve.
Parameters
----------
curve : ddg.datastructures.nets.net.DiscreteCurve
A curve with `curve.domain.bounded == True`.
Returns
-------
tuple of numpy.ndarray and bool
The array is of shape `(n, d)` and comprises of the `n`
points in R^d of the curve.
The bool is equal to `curve.domain.periodic`.
See Also
--------
ddg.conversion.arrays.from_discrete_net
"""
if not curve.domain.bounded:
raise ValueError(f"{curve.domain.bounded = } must be True")
return np.array(list(curve[curve.domain])), curve.domain.periodic
[docs]def from_empty_net(_, /):
"""Convert an empty net to an empty mesh.
Parameters
----------
_ : ddg.datastructures.nets.net.EmptyNet
Returns
-------
tuple of () and ()
"""
return (), ()
[docs]def from_point_net(net, /):
"""Convert a point net to a mesh of one point and no faces.
Parameters
----------
net : ddg.datastructures.nets.net.PointNet
Returns
-------
tuple of numpy.ndarray of shape ((1, n)) and ()
The array contains the value of `net()`, which is a point in R^n.
"""
return np.array(net())[None, :], ()
def _from_net(net):
if isinstance(net, nets.DiscreteCurve):
return from_discrete_curve(net)
elif isinstance(net, nets.DiscreteNet):
return from_discrete_net(net)
elif isinstance(net, nets.EmptyNet):
return from_empty_net(net)
elif isinstance(net, nets.PointNet):
return from_point_net(net)
else:
raise Exception(should_never_happen)
[docs]def from_net_collection(net_collection, /):
"""Convert a net collection to a list of curves and meshes.
Parameters
----------
nets : ddg.datastructures.nets.net.NetCollection
Every net in the collection has to be a DiscreteCurve or DiscreteNet
with bounded domain or a PointNet or an EmptyNet.
Returns
-------
list of curves and meshes
A curve is a pair of points and periodicity.
A mesh is a pair of points and faces.
"""
# Problem 1 (if we want to return *one* object): We may get multiple curves
# *and* meshes, but the representation as points and periodicity can only
# handle connected curves.
#
# Problem 2 (if we want to return *one* object): We may get curves *and*
# meshes. If that happens, we could represent the curves as meshes and
# merge everything into one mesh. Is that reasonable?
#
# Problem 3: This function could easily be generalised to operate on
# sequences or iterables. Should we do that?
def convertible(net):
if isinstance(net, nets.DiscreteNet):
if net.domain.bounded:
return net
else:
return "The net is a DiscreteNet, but its domain isn't bounded"
elif isinstance(net, (nets.EmptyNet, nets.PointNet)):
return net
else:
return (
f"The net's type is {type(net)}, so it is neither a DiscreteCurve "
"or DiscreteNet with a bounded domain, an EmptyNet or a PointNet"
)
nets_or_errors = list(map(convertible, net_collection.nets))
bad = [
(i, error) for i, error in enumerate(nets_or_errors) if isinstance(error, str)
]
if bad:
raise ValueError(
"Some nets in the collection can't be converted to arrays:\n"
+ "\n".join(f"Net {i}: {error}" for i, error in bad)
)
else:
good = list(filter(lambda n: isinstance(n, nets.Net), nets_or_errors))
return list(map(_from_net, good))
def _he_verts(surface, coordinate_attribute):
return np.array([getattr(v, coordinate_attribute) for v in surface.verts])
def _isolated_he_edges(surface):
edges = [
e for e in get.single_edges(surface) if e.face is None and e.opp.face is None
]
return np.array([(e.head.index, e.pre.head.index) for e in edges])
def _he_faces(surface):
return [tuple(v.index for v in get.get_vertices(f)) for f in surface.faces]
[docs]def from_half_edge_surface(surface, coordinate_attribute="co", /):
"""Get the points, isolated edges and faces of a half edge surface.
Parameters
----------
surface : ddg.datastructures.halfedge.Surface
coordinate_attribute : str (default="co")
Returns
-------
tuple of numpy.ndarray, numpy.ndarray and list
The first array is of shape (n, d), where n is the number of
points lying in R^d.
The second array is of shape (k, 2), where k is the number of
edges which don't belong to a face.
The list contains tuples of integers. Each tuple corresponds to
a face and each integer is the index of a vertex of that face.
"""
return (
_he_verts(surface, coordinate_attribute),
_isolated_he_edges(surface),
_he_faces(surface),
)
[docs]def from_1d_subspace(line, length, /):
"""Sample a 1-dimensional subspace.
Parameters
----------
line : ddg.geometry.subspaces.Subspace
A subspace with `line.dimension == 1`.
length : float
Must be greater than or equal to 0.0.
Returns
-------
(a, b), False
`a` and `b` are of type numpy.ndarray. `b - a` has length `length` and
`b - a / 2` is the closest point of the line to the origin.
Raises
------
ValueError
If `line.dimension != 1` or `length < 0.0`.
"""
if line.dimension != 1:
raise ValueError(f"{line.dimension = } must be 1.")
if length < 0:
raise ValueError(f"{length = } must be non-negative.")
match line.affine_point_and_directions:
case p, [d]:
d_ = length / 2 * d
return (p - d_, p + d_), False
case _:
raise Exception(should_never_happen)
[docs]def from_2d_subspace(plane, side_length_1, side_length_2, /):
"""Convert a 2-dimensional subspace to a mesh.
The mesh is given as two triangles::
d c
┌─────┐ ┐
│ /│ │
│ / │ │
│ x │ │ side_length_2
│ / │ │
│/ │ │
└─────┘ ┘
a b
└─────┘
side_length_1
such that `x` is the closest point of the subspace to the origin.
Parameters
----------
plane : ddg.geometry.subspaces.Subspace
A subspace with `subspace.dimension == 2`.
side_length_1 : float
Must be greater than or equal to 0.0.
side_length_2 : float
Must be greater than or equal to 0.0.
Returns
-------
points, faces : tuple of numpy.ndarray and tuple
The first entry points has shape (4, 3) and comprises of a, b, c, d.
The second entry faces comprises of two tuples of 3 integers encoding
the two triangles.
Raises
------
ValueError
If `plane.dimension != 2` or `side_length_1 < 0.0` or `side_length_2 < 0.0`.
"""
if plane.dimension != 2:
raise ValueError(f"{plane.dimension = } must be 2.")
if side_length_1 < 0 or side_length_2 < 0:
raise ValueError(
f"{side_length_1 = } and {side_length_2 = } must be non-negative."
)
match plane.affine_point_and_directions:
case p, [u, v]:
mu = side_length_1 / 2 * u
mv = side_length_2 / 2 * v
a = p - mu - mv
b = p + mu - mv
c = p + mu + mv
d = p - mu + mv
points = np.row_stack((a, b, c, d))
faces = (0, 1, 2), (0, 2, 3)
return points, faces
case _:
raise Exception(should_never_happen)
[docs]def from_quadric(quadric, /, *num_step):
"""Convert a quadric to a curve or a mesh or a list thereof.
Every convertible quadric has a parametrization, which is sampled. These
parametrizations are implementation details, but the following guarantees
hold:
- every parametrization is defined on a box I_1 x ... x I_d, where d is the
dimension of the quadric and I_1, ..., I_d are possibly unbounded
intervals
- the k-th `num_step` argument determines how I_k is sampled
- the first entry `num` of the k-th `num_step` determines the number of
samples of I_k
- if I_k is unbounded, the second entry `step` of the k-th `num_step`
determines the step size between the samples of I_k
Consequently,
- increasing the `num` arguments increases the number of points and
additionally the number of faces if the quadric is 2-dimensional.
- if I_k isn't bounded, decreasing the respective `step` results in a finer
curve or mesh.
Parameters
----------
quadric : ddg.geometry.quadrics.Quadric
*num_step : tuple of an int and a float
The length of `num_step` must be 0 if `quadric.dimension` is -1 or 0
and otherwise must be equal to `quadric.dimension`.
Returns
-------
curve or mesh or a list of curves and meshes
A curve is an tuple of points of type numpy.ndarray and the
periodicity of type bool.
A mesh is an tuple of points of type numpy.ndarray and the
faces of type numpy.ndarray.
Raises
------
ValueError
If the quadric is a (possibly empty) set of points and `num_step` is
given.
"""
# We intentionally don't allow for other ways to sample the
# parametrization, because the choice of parametrization is
# arbitrary and therefore an implementation detail. For example, it
# doesn't make sense to let the user specify samples in the domain
# such as anchor values.
#
# The parametrization isn't even always obvious, e.g. the paraboloid
# may be parametrized by
# (u, v) -> (u, v, u^2 + v^2)
# and also as a surface of revolution and a circle can be
# parametrized in two directions. So if the user wants more control,
# they should just directly sample a parametrization of their
# choice.
# TODO: Rewrite this using vectorised parametrization and domains
# once they are available.
net_or_collection = ddg.conversion.nets.geometry.quadrics.quadric_to_smooth_net(
quadric
)
if quadric.dimension in {-1, 0} and num_step:
raise ValueError(
"Since quadric.dimension is -1 or 0, no num_step may be given."
)
elif quadric.dimension == 1:
((num, step),) = num_step
# sampling is not allowed to be [(step, num, "c")], otherwise
# sample_interval raises a ValueError for some reason.
sampling = (step, num, "c")
else:
sampling = [(step, num, "c") for num, step in num_step]
if isinstance(net_or_collection, nets.SmoothNet):
discrete_net = nets.conversion.sample_smooth_net(net_or_collection, sampling)
return _from_net(discrete_net)
elif isinstance(net_or_collection, (nets.EmptyNet, nets.PointNet)):
return _from_net(net_or_collection)
elif isinstance(net_or_collection, nets.NetCollection):
discrete_net_collection = nets.conversion.sample_smooth_net(
net_or_collection, sampling
)
return list(map(_from_net, discrete_net_collection))
else:
raise Exception(should_never_happen)
[docs]def from_sphere(sphere, num, /):
"""Convert a sphere to a curve or a mesh.
Circles are converted to curves and 2-dimensional spheres are
converted to meshes.
Parameters
----------
sphere : ddg.geometry.spheres.Sphere
num : int
Increasing this number increases the number of samples in each
direction of the sphere parametrization.
Returns
-------
curve or mesh
A curve is an tuple of points of type numpy.ndarray and the
periodicity of type bool.
A mesh is an tuple of points of type numpy.ndarray and the
faces of type numpy.ndarray.
"""
smooth_net = sphere_to_smooth_net(sphere)
discrete_net = ddg.sample_smooth_net(smooth_net, (num, "t"))
if isinstance(discrete_net, nets.DiscreteCurve):
return from_discrete_curve(discrete_net)
else:
return from_discrete_net(discrete_net)
[docs]def from_indexed_face_set(ifs, coordinate_key="co", /):
"""Get the points, and faces of an indexed face set.
Parameters
----------
ifs : ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet
coordinate_attribute : str (default="co")
Returns
-------
tuple of numpy.ndarray and list
"""
# TODO: Decide if this function is even necessary.
# It shouldn't be necessary. After all, it duplicates the most basic
# functionality that one would expect from an indexed face set.
return ifs.vertex_attributes[coordinate_key], ifs.face_list()