-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/stacks' of github.com:damogranlabs/classy_block…
…s into feature/stacks
- Loading branch information
Showing
15 changed files
with
452 additions
and
210 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
from typing import Dict, List, Set | ||
|
||
import numpy as np | ||
import scipy.optimize | ||
|
||
from classy_blocks.construct.flat.quad import Quad | ||
from classy_blocks.types import NPPointListType, QuadIndexType | ||
from classy_blocks.util import functions as f | ||
|
||
|
||
class QuadMap: | ||
def __init__(self, positions: NPPointListType, indexes: List[QuadIndexType]): | ||
self.indexes = indexes | ||
self.positions = positions | ||
|
||
self.quads = [Quad(self.positions, quad_indexes) for quad_indexes in indexes] | ||
|
||
def update(self) -> None: | ||
for quad in self.quads: | ||
quad.update() | ||
|
||
@property | ||
def connections(self) -> List[Set[int]]: | ||
return f.flatten_2d_list([quad.connections for quad in self.quads]) | ||
|
||
@property | ||
def neighbours(self) -> Dict[int, Set[int]]: | ||
"""Returns a dictionary point:[neighbour points] as defined by quads""" | ||
length = int(max(np.array(self.indexes).ravel())) + 1 | ||
|
||
neighbours: Dict[int, Set[int]] = {i: set() for i in range(length)} | ||
connections = self.connections | ||
|
||
for connection in connections: | ||
clist = list(connection) | ||
neighbours[clist[0]].add(clist[1]) | ||
neighbours[clist[1]].add(clist[0]) | ||
|
||
return neighbours | ||
|
||
@property | ||
def fixed_points(self) -> Set[int]: | ||
"""Returns indexes of points that can be smoothed""" | ||
connections = self.connections | ||
fixed_points: Set[int] = set() | ||
|
||
for edge in connections: | ||
if connections.count(edge) == 1: | ||
fixed_points.update(edge) | ||
|
||
return fixed_points | ||
|
||
def smooth_laplacian(self, iterations: int = 5) -> None: | ||
"""Smooth the points using laplacian smoothing; | ||
each point is moved to the average of its neighbours""" | ||
neighbours = self.neighbours | ||
fixed_points = self.fixed_points | ||
|
||
for _ in range(iterations): | ||
for point_index, point_neighbours in neighbours.items(): | ||
if point_index in fixed_points: | ||
continue | ||
|
||
nei_positions = [self.positions[i] for i in point_neighbours] | ||
|
||
self.positions[point_index] = np.average(nei_positions, axis=0) | ||
|
||
self.update() | ||
|
||
def get_nearby_quads(self) -> Dict[int, List[Quad]]: | ||
"""Returns a list of quads that contain each movable point""" | ||
corner_points: Dict[int, List[Quad]] = {} | ||
|
||
fixed_points = self.fixed_points | ||
|
||
for i, point in enumerate(self.positions): | ||
if i in fixed_points: | ||
continue | ||
|
||
corner_points[i] = [] | ||
|
||
for quad in self.quads: | ||
if quad.contains(point): | ||
corner_points[i].append(quad) | ||
|
||
return corner_points | ||
|
||
def optimize_energy(self): | ||
"""Replace quad edges by springs and moves their positions | ||
so that all springs are in the most relaxed state possible, | ||
minimizing the energy of the system""" | ||
# get quads that are defined by each point | ||
fixed_points = self.fixed_points | ||
|
||
e1 = self.quads[0].e1 | ||
e2 = self.quads[0].e2 | ||
|
||
def energy(i, x) -> float: | ||
e = 0 | ||
|
||
self.positions[i] = x[0] * e1 + x[1] * e2 | ||
|
||
for quad in self.quads: | ||
quad.update() | ||
e += quad.energy | ||
|
||
return e | ||
|
||
for i in range(len(self.positions)): | ||
if i in fixed_points: | ||
continue | ||
|
||
initial = [1, 1] | ||
scipy.optimize.minimize(lambda x, i=i: energy(i, x), initial) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,71 @@ | ||
from typing import Dict, List, Set, Tuple | ||
from typing import List, Set | ||
|
||
import numpy as np | ||
|
||
from classy_blocks.construct.flat.face import Face | ||
from classy_blocks.types import NPPointListType | ||
from classy_blocks.types import NPPointListType, NPPointType, QuadIndexType | ||
from classy_blocks.util import functions as f | ||
from classy_blocks.util.constants import DTYPE | ||
|
||
QuadType = Tuple[int, int, int, int] | ||
from classy_blocks.util.constants import TOL | ||
|
||
|
||
class Quad: | ||
"""A helper class for tracking positions-faces-indexes-neighbours-whatnot""" | ||
|
||
def __init__(self, positions: NPPointListType, indexes: Tuple[int, int, int, int]): | ||
def __init__(self, positions: NPPointListType, indexes: QuadIndexType): | ||
self.indexes = indexes | ||
self.face = Face(np.take(positions, list(indexes), axis=0)) | ||
|
||
|
||
# TODO: move functions into Quad or remove Quad at all | ||
def get_connections(quad: QuadType) -> List[Set[int]]: | ||
return [{quad[i], quad[(i + 1) % 4]} for i in range(4)] | ||
|
||
|
||
def get_all_connections(quads) -> List[Set[int]]: | ||
return f.flatten_2d_list([get_connections(quad) for quad in quads]) | ||
self.positions = positions | ||
self.face = Face([self.positions[i] for i in self.indexes]) | ||
|
||
def update(self) -> None: | ||
"""Update Face position""" | ||
self.face.update([self.positions[i] for i in self.indexes]) | ||
|
||
def find_neighbours(quads: List[QuadType]) -> Dict[int, Set[int]]: | ||
"""Returns a dictionary point:[neighbour points] as defined by quads""" | ||
length = int(max(np.array(quads, dtype=DTYPE).ravel())) + 1 | ||
def contains(self, point: NPPointType) -> bool: | ||
"""Returns True if the given point is a part of this quad""" | ||
for this_point in self.points: | ||
if f.norm(point - this_point) < TOL: | ||
return True | ||
|
||
neighbours: Dict[int, Set[int]] = {i: set() for i in range(length)} | ||
connections = get_all_connections(quads) | ||
return False | ||
|
||
for connection in connections: | ||
clist = list(connection) | ||
neighbours[clist[0]].add(clist[1]) | ||
neighbours[clist[1]].add(clist[0]) | ||
@property | ||
def points(self) -> NPPointListType: | ||
return self.face.point_array | ||
|
||
return neighbours | ||
@property | ||
def connections(self) -> List[Set[int]]: | ||
return [{self.indexes[i], self.indexes[(i + 1) % 4]} for i in range(4)] | ||
|
||
@property | ||
def perimeter(self): | ||
return sum([f.norm(self.points[i] - self.points[(i + 1) % 4]) for i in range(4)]) | ||
|
||
def get_fixed_points(quads) -> Set[int]: | ||
"""Returns indexes of points that can be smoothed""" | ||
connections = get_all_connections(quads) | ||
fixed_points: Set[int] = set() | ||
@property | ||
def center(self): | ||
return np.average(self.points, axis=0) | ||
|
||
for edge in connections: | ||
if connections.count(edge) == 1: | ||
fixed_points.update(edge) | ||
@property | ||
def energy(self): | ||
e = 0 | ||
|
||
return fixed_points | ||
ideal_side = self.perimeter / 4 | ||
ideal_diagonal = (ideal_side / 2) * 2**0.5 | ||
center = self.center | ||
|
||
for i in range(4): | ||
e += (f.norm(self.points[i] - self.points[(i + 1) % 4]) - ideal_side) ** 2 | ||
e += (f.norm(center - self.points[i]) - ideal_diagonal) ** 2 | ||
|
||
def smooth(positions, quads, iterations: int) -> NPPointListType: | ||
neighbours = find_neighbours(quads) | ||
fixed_points = get_fixed_points(quads) | ||
return e / 8 | ||
|
||
for _ in range(iterations): | ||
for point_index, point_neighbours in neighbours.items(): | ||
if point_index in fixed_points: | ||
continue | ||
@property | ||
def e1(self): | ||
return f.unit_vector(self.points[1] - self.points[0]) | ||
|
||
nei_positions = np.take(positions, list(point_neighbours), axis=0) | ||
positions[point_index] = np.average(nei_positions, axis=0) | ||
@property | ||
def normal(self): | ||
return f.unit_vector(np.cross(self.points[1] - self.points[0], self.points[3] - self.points[0])) | ||
|
||
return positions | ||
@property | ||
def e2(self): | ||
return f.unit_vector(-np.cross(self.e1, self.normal)) |
Oops, something went wrong.