#
##
## SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.4 (Thu Nov 16 18:07:39 CET 2023)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## Distributed under the GNU General Public License version 3 or later.
##
## This program 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.
##
## This program 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 http://www.gnu.org/licenses/.
##
"""Tools for handling collections of elements belonging to multiple parts.
This module defines the Collection class.
"""
from copy import deepcopy
import numpy as np
import pyformex as pf
################# Collection of Actors or Actor Elements ###############
[docs]def uniq(data, keep_order):
"""Uniqify data, keeping order if needed"""
if keep_order:
return np.asarray(list(dict.fromkeys(data)))
else:
return np.unique(data)
[docs]def uniq_add(a, b, keep_order):
"""Add to a uniq array. a is supposed to be uniq, not checked!"""
if keep_order:
return np.concatenate([a, [i for i in b if i not in a]])
else:
return np.union1d(a, b)
[docs]def uniq_remove(a, b, keep_order):
"""Remove from a uniq array. a is supposed to be uniq, not checked!"""
if keep_order:
return np.asarray([i for i in a if i not in b])
else:
return np.setdiff1d(a, b)
[docs]class Collection:
"""A collection is a set of (int,int) tuples.
The first part of the tuple has a limited number of values and are used
as the keys in a dict.
The second part can have a lot of different values and is implemented
as an integer array with unique values.
This is e.g. used to identify a set of individual parts of one or more
OpenGL actors.
Examples
--------
>>> a = Collection()
>>> a.add(range(7),3)
>>> a.add(range(4))
>>> a.remove([2,4],3)
>>> print(a)
-1 [0 1 2 3]; 3 [0 1 3 5 6];
>>> a.add([[2,0],[2,3],[-1,7],[3,88]])
>>> print(a)
-1 [0 1 2 3 7]; 2 [0 3]; 3 [ 0 1 3 5 6 88];
>>> print(a.total())
13
>>> a[2] = [1,2,3]
>>> print(a)
-1 [0 1 2 3 7]; 2 [1 2 3]; 3 [ 0 1 3 5 6 88];
>>> a[2] = []
>>> print(a)
-1 [0 1 2 3 7]; 3 [ 0 1 3 5 6 88];
>>> a.set([[2,0],[2,3],[-1,7],[3,88]])
>>> print(a)
-1 [7]; 2 [0 3]; 3 [88];
>>> print(a.keys())
[-1 2 3]
>>> for k, i in a.singles(): print(k,i)
-1 7
2 0
2 3
3 88
>>> print([k for k in a])
[-1, 2, 3]
>>> 2 in a
True
>>> del a[2]
>>> 2 in a
False
"""
def __init__(self, obj_type=None, keep_order=False):
self.d = {}
self.obj_type = obj_type
self.keep_order = keep_order
def copy(self):
return deepcopy(self)
def __len__(self):
return len(self.d)
def total(self):
return sum([len(v) for v in self.d.values()])
def clear(self, keys=[]):
if keys:
for k in keys:
k = int(k)
if k in self.d:
del self.d[k]
else:
self.d = {}
[docs] def add(self, data, key=-1):
"""Add new data to a Collection.
Parameters
----------
data: :term:`array_like` | Collection
An int array with shape (ndata, 2) or (ndata,), or another
Collection with the same obj_type as self. If an array
with shape (ndata, 2), each row has a key value in column 0
and a data value in column 1. If an (ndata,) shaped array,
all items have the same key and it has to be specified separately.
key: int
The key value if data is an (ndata,) shaped array. Else ignored.
keep_order
separately, or a default value will be used.
data can also be another Collection, if it has the same object
type.
"""
if len(data) == 0:
return
if isinstance(data, Collection):
if data.obj_type == self.obj_type:
for k in data.d:
self.add(data.d[k], k)
return
else:
raise ValueError(
"Cannot add Collections with different object type")
data = np.asarray(data)
if data.ndim == 2:
for key in np.unique(data[:, 0]):
self.add(data[data[:, 0] == key, 1], key)
else:
if key in self.d:
self.d[key] = uniq_add(self.d[key], data, self.keep_order)
elif data.size > 0:
self.d[key] = uniq(data, self.keep_order)
[docs] def set(self, data, key=-1):
"""Set the collection to the specified data.
This is equivalent to clearing the corresponding keys
before adding.
"""
self.clear()
self.add(data, key)
[docs] def remove(self, data, key=-1):
"""Remove data from the collection."""
if isinstance(data, Collection):
if data.obj_type == self.obj_type:
for k in data.d:
self.remove(data.d[k], k)
return
else:
raise ValueError(
"Cannot remove Collections with different object type")
data = np.asarray(data)
if data.ndim == 2:
for key in np.unique(data[:, 0]):
self.remove(data[data[:, 0] == key, 1], key)
else:
key = int(key)
if key in self.d:
data = uniq_remove(self.d[key], data, self.keep_order)
if data.size > 0:
self.d[key] = data
else:
del self.d[key]
else:
pf.debug(f"Not removing from non-existing selection "
f"for actor {key}", pf.DEBUG.DRAW)
def __contains__(self, key):
"""Check whether the collection has an entry for the key.
This inplements the 'in' operator for a Collection.
"""
return key in self.d
def __setitem__(self, key, data):
"""Set new values for the given key. This does not force uniqueness"""
key = int(key)
data = np.asarray(data)
if data.size > 0:
self.d[key] = data
else:
del self.d[key]
def __iter__(self):
"""Return an iterator for the Collection
The iterator loops over the sorted keys.
"""
return iter(self.keys())
def __getitem__(self, key):
"""Return item with given key."""
return self.d[key]
def __delitem__(self, key):
"""Delete an item with given key"""
del self.d[key]
[docs] def get(self, key, default=[]):
"""Return item with given key or default."""
key = int(key)
return self.d.get(key, default)
[docs] def keys(self):
"""Return a sorted array with the keys"""
return np.asarray(sorted(self.d.keys()))
[docs] def items(self):
"""Return an iterator over (key,value) pairs."""
return self.d.items()
[docs] def singles(self):
"""Return an iterator over the single instances
Returns next (k, i) pair from the collection.
"""
for k in self:
for v in self.d[k]:
yield k, v
def __str__(self):
if len(self) == 0:
s = 'Empty Collection'
else:
s = ''
for k in self.keys():
s += f"{k} {self.d[k]}; "
s = s.rstrip()
return s
# End