Shape Grammar





Coursework for Design Computation II


Implementation of Shape Grammar using Rhino and GH Python. A shape grammar is a set of shape rules applied in discrete steps to generate designs of shapes. The example demostrated is based on the following Shape Grammar:

1. Every square has an anchor and a heading (as a vector). These two properties define the direction of rotation of the square (clockwise or counter-clockwise).

2. A new square is generated by rotating the original square by -90 or 90 degrees, according to its direction of rotation.

3. Generate a new square based on rule 2, but move the anchor to the center of the edge ad scale the size of the new square by 2/3.

4. Eliminate a square from the existing set, randomly choose another square from the set as the next origin.



Program: Design Computation
Date: 2018







Demonstration of shape growth




GH Python Code:
__author__ = "Vincent Mai"
__version__ = "2018.11.10"

"""
Design Computation II
Shape Grammar
""" 
import random
import rhinoscriptsyntax as rs
import Rhino.Geometry as rg

class Shape(object):
    def __init__(self):
        self.squares = set()
        self.frontier = set()

    def initialShape(self, num, size):
        """
        generate initial state of the shape object by the specified
        square numbers and size, each with a random coner anchor,
        a random rotational direction and a random heading.
        """
        gridSize = int(num**0.5)+3
        centerLocs = [(x*size, y*size) for x in range(gridSize) for y in range(gridSize)]
        centers = random.sample(centerLocs, num)
        for center in centers:
            x, y = random.choice((-1,1)), random.choice((-1, 1))
            anchorDir = (x*0.5*size, y*0.5*size)
            anchor = (center[0]+anchorDir[0], center[1]+anchorDir[1])
            rotation = random.choice(('cw', 'ccw'))
            # generate heading based on clockwise or counter clockwise rotation
            if anchorDir[0] == anchorDir[1]:
                heading = (0, -anchorDir[1]) if rotation == 'cw' else (-anchorDir[0], 0)
            else:
                heading = (-anchorDir[0], 0) if rotation == 'cw' else (0, -anchorDir[1])
            # initializing square objects
            newSquare = Square(anchor, heading, rotation, center, size)
            self.squares.add(newSquare)
            self.frontier.add(newSquare)
            
    def grow(self):
        """
        grow frontier squares by random choice of shape grammar or delete
        squares from existing squares
        """
        tempFrontier = set()
        for square in self.frontier:
            grammar = random.randint(0,20)
            if grammar < 10:
                tempFrontier.add(shape.rule1(square))
            elif grammar < 15:
                tempFrontier.add(shape.rule3())
            else:
                tempFrontier.add(shape.rule2(square))
        self.frontier = tempFrontier
        self.squares |= self.frontier
        print(len(tempFrontier))

    def rule1(self, square):
        """
        adding an adjacent square to an edge of the original square
        based on the rotation of the original square
        """
        hx, hy = square.heading
        # rotate heading cw if original square is ccw, and vice versa
        if square.rotation == 'cw': 
            newHeading = (-hy, hx)
        else: 
            newHeading = (hy, -hx)
        move = (square.heading[0]+newHeading[0], square.heading[1]+newHeading[1])
        newCenter = (square.anchor[0]+move[0], square.anchor[1]+move[1])
        newSquare = Square(square.anchor, newHeading, square.rotation, newCenter, square.size)
        return newSquare


    def rule2(self, square):
        """
        adding an adjacent square scaled by 2/3 and anchored to the midpoint
        of one of the edges
        """
        hx, hy = square.heading
        if square.rotation == 'cw': 
            newHeading = (-hy*2/3, hx*2/3)
        else: 
            newHeading = (hy*2/3, -hx*2/3)
        newAnchor = (square.anchor[0]+square.heading[0], square.anchor[1]+square.heading[1])
        newSize = square.size*2/3
        move = (square.heading[0]*2/3+newHeading[0], square.heading[1]*2/3+newHeading[1])
        newCenter = (newAnchor[0]+move[0], newAnchor[1]+move[1])
        
        newSquare = Square(newAnchor, newHeading, square.rotation, newCenter, newSize)
        return newSquare

    def rule3(self):
        """
        eliminate
        """
        self.squares.remove(random.sample(self.squares, 1)[0])
        return random.sample(self.squares, 1)[0]


class Square(object):
    def __init__(self, anchor, heading, rotation, center, size):
        self.anchor = anchor
        self.heading = heading
        self.rotation = rotation
        self.center = center
        self.size = size
        


if reset:
    centerList = []
    anchorList = []
    headingList = []
    sizeList = []
    random.seed(0)
    shape = Shape()
    shape.initialShape(num, size)

else:
    centerList = []
    anchorList = []
    headingList = []
    sizeList = []

    for i in range(1):
        shape.grow()
        
    for square in shape.squares:
        centerList.append(rs.CreatePoint(square.center[0], square.center[1]))
        anchorList.append(rs.CreatePoint(square.anchor[0], square.anchor[1]))
        headingList.append(rs.CreateVector(square.heading[0], square.heading[1]))
        sizeList.append(square.size)

centers = centerList
anchors = anchorList
headings = headingList
sizes = sizeList