Skip to content

Commit

Permalink
KnitDiNetwork class for finding cycles (faces) of the network as an e…
Browse files Browse the repository at this point in the history
…ffort towards #2
  • Loading branch information
fstwn committed Jul 6, 2020
1 parent 7b949eb commit 3c4836b
Show file tree
Hide file tree
Showing 2 changed files with 376 additions and 0 deletions.
361 changes: 361 additions & 0 deletions modules/Cockatoo/KnitDiNetwork.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
"""
Directional KnitNetwork for finding faces (cycles) of a KnitNetwork.
Author: Max Eschenbach
License: Apache License 2.0
Version: 200503
"""

# PYTHON STANDARD LIBRARY IMPORTS ----------------------------------------------
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from collections import deque
import math
from operator import itemgetter

# LOCAL MODULE IMPORTS ---------------------------------------------------------
from .Environment import IsRhinoInside
from .KnitNetworkBase import KnitNetworkBase
from .Utilities import is_ccw_xy
from .Utilities import pairwise

# THIRD PARTY MODULE IMPORTS ---------------------------------------------------
import networkx as nx

# RHINO IMPORTS ----------------------------------------------------------------
if IsRhinoInside():
import rhinoinside
rhinoinside.load()
from Rhino.Geometry import Mesh as RhinoMesh
from Rhino.Geometry import NurbsSurface as RhinoNurbsSurface
from Rhino.Geometry import Plane as RhinoPlane
from Rhino.Geometry import Vector3d as RhinoVector3d
else:
from Rhino.Geometry import Mesh as RhinoMesh
from Rhino.Geometry import NurbsSurface as RhinoNurbsSurface
from Rhino.Geometry import Plane as RhinoPlane
from Rhino.Geometry import Vector3d as RhinoVector3d

# AUTHORSHIP -------------------------------------------------------------------
__author__ = """Max Eschenbach ([email protected])"""

# ALL DICTIONARY ---------------------------------------------------------------
__all__ = [
"KnitDiNetwork"
]

# ACTUAL CLASS -----------------------------------------------------------------
class KnitDiNetwork(nx.DiGraph, KnitNetworkBase):
"""
Class for representing a mapping network that facilitates the automatic
generation of knitting patterns based on Rhino geometry.
This is intended only to be instanced by a fully segmented instance of
KnitNetwork.
"""

# INITIALIZATION -----------------------------------------------------------

def __init__(self, data=None, **attr):
"""
Initialize a KnitNetwork (inherits NetworkX graph with edges, name,
graph attributes.
Parameters
----------
data : input graph
Data to initialize graph. If data=None (default) an empty
graph is created. The data can be an edge list, or any
NetworkX graph object. If the corresponding optional Python
packages are installed the data can also be a NumPy matrix
or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph.
name : string, optional (default='')
An optional name for the graph.
attr : keyword arguments, optional (default= no attributes)
Attributes to add to graph as key=value pairs.
"""

# initialize using original init method
super(KnitDiNetwork, self).__init__(data=data, **attr)

# also copy the MappingNetwork attribute if it is already available
if data and isinstance(data, KnitDiNetwork) and data.MappingNetwork:
self.MappingNetwork = data.MappingNetwork
else:
self.MappingNetwork = None

# also copy or initialize the halfedge dict for finding faces
if data and isinstance(data, KnitDiNetwork) and data.halfedge:
self.halfedge = data.halfedge
else:
self.halfedge = {}

# TEXTUAL REPRESENTATION OF NETWORK ----------------------------------------

def ToString(self):
"""
Return a textual description of the network.
"""

name = "KnitDiNetwork"
nn = len(self.nodes())
ce = len(self.ContourEdges)
wee = len(self.WeftEdges)
wae = len(self.WarpEdges)
data = ("({} Nodes, {} Segment Contours, {} Weft, {} Warp)")
data = data.format(nn, ce, wee, wae)
return name + data

# FIND FACES (CYCLES) OF NETWORK -------------------------------------------

def _sort_node_neighbors(self, key, nbrs, xyz, geo, cbp, nrm, ccw=True):
"""
Sort the neighbors of a network node.
Notes
-----
Based on an implementation inside the COMPAS framework.
For more info see [1]_.
References
----------
.. [1] Van Mele, Tom et al. *COMPAS: A framework for computational research in architecture and structures*.
See: https://github.com/compas-dev/compas/blob/e313502995b0dd86d460f86e622cafc0e29d1b75/src/compas/datastructures/network/duality.py#L132
"""

# if there is only one neighbor we don't need to sort anything
if len(nbrs) == 1:
return nbrs

# initialize the ordered list of neighbors with the first node
ordered_nbrs = nbrs[0:1]

# retrieve coordinates for current node
a = xyz[key]

# compute local orientation if geometrybase data is present
if cbp and nrm:
a_geo = geo[key]
lclpln = RhinoPlane(a_geo, nrm[key])
lcl_a = lclpln.RemapToPlaneSpace(a_geo)[1]
a = (lcl_a.X, lcl_a.Y, lcl_a.Z)
lcl_xyz = {}
for nbr in nbrs:
nbr_cp = lclpln.ClosestPoint(geo[nbr])
lcl_nbr = lclpln.RemapToPlaneSpace(nbr_cp)[1]
nbr_xyz = (lcl_nbr.X, lcl_nbr.Y, lcl_nbr.Z)
lcl_xyz[nbr] = nbr_xyz
xyz = lcl_xyz

# loop over all neighbors except the first one (which is our basis for
# the ordered list)
for i, nbr in enumerate(nbrs[1:]):
c = xyz[nbr]
pos = 0
b = xyz[ordered_nbrs[pos]]
while not is_ccw_xy(a, b, c):
pos += 1
if pos > i:
break
b = xyz[ordered_nbrs[pos]]
if pos == 0:
pos -= 1
b = xyz[ordered_nbrs[pos]]
while is_ccw_xy(a, b, c):
pos -= 1
if pos < -len(ordered_nbrs):
break
b = xyz[ordered_nbrs[pos]]
pos += 1
ordered_nbrs.insert(pos, nbr)

# return the ordered neighbors in cw or ccw order
if not ccw:
return ordered_nbrs[::-1]
return ordered_nbrs

def _sort_neighbors(self, ccw=True):
"""
Sort the neighbors of all network nodes.
Notes
-----
Based on an implementation inside the COMPAS framework.
For more info see [1]_.
References
----------
.. [1] Van Mele, Tom et al. *COMPAS: A framework for computational research in architecture and structures*.
See: https://github.com/compas-dev/compas/blob/e313502995b0dd86d460f86e622cafc0e29d1b75/src/compas/datastructures/network/duality.py#L121
"""

# initialize sorted neighbors dict
sorted_neighbors = {}

# get dictionary of all coordinates by node index
xyz = {k: (d["x"], d["y"], d["z"]) for k, d in self.nodes_iter(True)}
geo = {k: d["geo"] for k, d in self.nodes_iter(True)}

# compute local orientation data when geometry base is present
try:
geometrybase = self.graph["geometrybase"]
except KeyError:
geometrybase = None

if not geometrybase:
cbp = None
nrm = None
elif isinstance(geometrybase, RhinoMesh):
cbp = {k: geometrybase.ClosestMeshPoint(geo[k], 0) \
for k in self.nodes_iter()}
nrm = {k: geometrybase.NormalAt(cbp[k]) \
for k in self.nodes_iter()}
elif isinstance(geometrybase, RhinoNurbsSurface):
cbp = {k: geometrybase.ClosestPoint(geo[k])[1:] \
for k in self.nodes_iter()}
nrm = {k: geometrybase.NormalAt(cbp[k][0], cbp[k][1]) \
for k in self.nodes_iter()}

# loop over all nodes in network
for key in self.nodes_iter():
nbrs = self[key].keys()
sorted_neighbors[key] = self._sort_node_neighbors(key, nbrs, xyz, geo, cbp, nrm, ccw=ccw)

# set the sorted neighbors list as an attribute to the nodes
for key, nbrs in sorted_neighbors.items():
self.node[key]["sorted_neighbors"] = nbrs[::-1]

# return the sorted neighbors dict
return sorted_neighbors

def _find_first_node_neighbor(self, key):
"""
Find the first neighbor for a given node in the network.
Notes
-----
Based on an implementation inside the COMPAS framework.
For more info see [1]_.
References
----------
.. [1] Van Mele, Tom et al. *COMPAS: A framework for computational research in architecture and structures*.
See: https://github.com/compas-dev/compas/blob/e313502995b0dd86d460f86e622cafc0e29d1b75/src/compas/datastructures/network/duality.py#L103
"""

# get all node neighbors
nbrs = self[key].keys()
# if there is only one neighbor, we have already found our candidate
if len(nbrs) == 1:
return nbrs[0]
ab = [-1.0, -1.0, 0.0]
rhino_ab = RhinoVector3d(*ab)
a = self.NodeCoordinates(key)
b = [a[0] + ab[0], a[1] + ab[1], 0]
angles = []
for nbr in nbrs:
c = self.NodeCoordinates(nbr)
ac = [c[0] - a[0], c[1] - a[1], 0]
rhino_ac = RhinoVector3d(*ac)
alpha = RhinoVector3d.VectorAngle(rhino_ab, rhino_ac)
if is_ccw_xy(a, b, c, True):
alpha = (2 * math.pi) - alpha
angles.append(alpha)
return nbrs[angles.index(min(angles))]

def _find_edge_cycle(self, u, v):
"""
Find a cycle based on the given edge.
Notes
-----
Based on an implementation inside the COMPAS framework.
For more info see [1]_.
References
----------
.. [1] Van Mele, Tom et al. *COMPAS: A framework for computational research in architecture and structures*.
See: https://github.com/compas-dev/compas/blob/09153de6718fb3d49a4650b89d2fe91ea4a9fd4a/src/compas/datastructures/network/duality.py#L161
"""
cycle = [u]
while True:
cycle.append(v)
nbrs = self.node[v]["sorted_neighbors"]
nbr = nbrs[nbrs.index(u) - 1]
u, v = v, nbr
if v == cycle[0]:
break
return cycle

def FindCycles(self):
"""
Finds the cycles (faces) of this network by utilizing a wall-follower
mechanism.
Notes
-----
Based on an implementation inside the COMPAS framework.
For more info see [1]_.
References
----------
.. [1] Van Mele, Tom et al. *COMPAS: A framework for computational research in architecture and structures*.
See: https://github.com/compas-dev/compas/blob/09153de6718fb3d49a4650b89d2fe91ea4a9fd4a/src/compas/datastructures/network/duality.py#L20
"""

# initialize the halfedge dict of the directed network
for u, v in self.edges_iter():
try:
self.halfedge[u][v] = None
except KeyError:
self.halfedge[u] = {}
self.halfedge[u][v] = None
try:
self.halfedge[v][u] = None
except KeyError:
self.halfedge[v] = {}
self.halfedge[v][u] = None

# sort the all the neighbors for each node of the network
self._sort_neighbors()

# find start node
u = sorted(self.nodes_iter(data=True), key=lambda n: (n[1]["y"], n[1]["x"]))[0][0]

# initialize found and cycles dict
cycles = {}
found = {}
ckey = 0

# find the very first cycle
v = self._find_first_node_neighbor(u)
cycle = self._find_edge_cycle(u, v)
frozen = frozenset(cycle)
found[frozen] = ckey
cycles[ckey] = cycle

for a, b in pairwise(cycle + cycle[:1]):
self.halfedge[a][b] = ckey
ckey += 1

for u, v in self.edges_iter():
if self.halfedge[u][v] is None:
cycle = self._find_edge_cycle(u, v)
frozen = frozenset(cycle)
if frozen not in found:
found[frozen] = ckey
cycles[ckey] = cycle
ckey += 1
for a, b in pairwise(cycle + cycle[:1]):
self.halfedge[a][b] = found[frozen]
if self.halfedge[v][u] is None:
cycle = self._find_edge_cycle(v, u)
frozen = frozenset(cycle)
if frozen not in found:
found[frozen] = ckey
cycles[ckey] = cycle
ckey += 1
for a, b in pairwise(cycle + cycle[:1]):
self.halfedge[a][b] = found[frozen]

return cycles
15 changes: 15 additions & 0 deletions modules/Cockatoo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
"""
Cockatoo library and class interface for automatic generation of cnc-knitting
patterns from 3D surfaces and meshes.
Author: Max Eschenbach
License: Apache License 2.0
Version: 200503
"""

# PYTHON STANDARD LIBRARY IMPORTS ----------------------------------------------
from __future__ import absolute_import

Expand All @@ -7,15 +16,21 @@
from . import Exceptions
from .KnitNetworkBase import KnitNetworkBase
from .KnitNetwork import KnitNetwork
from .KnitDiNetwork import KnitDiNetwork
from .KnitMappingNetwork import KnitMappingNetwork

# AUTHORSHIP -------------------------------------------------------------------

__author__ = """Max Eschenbach ([email protected])"""

# ALL DICTIONARY ---------------------------------------------------------------
__all__ = [
"Autoknit",
"Environment",
"Exceptions",
"KnitNetworkBase",
"KnitNetwork",
"KnitDiNetwork",
"KnitMappingNetwork",
]

Expand Down

0 comments on commit 3c4836b

Please sign in to comment.