<file_sep>import bpy
import bmesh

import copy
import mathutils
import random
import time
from datetime import datetime
from mathutils import Euler
from math import sqrt




### 3D Context creation. ###

def view3d_find( return_area = False ):
    # returns first 3d view, normally we get from context
    for area in bpy.context.window.screen.areas:
        if area.type == 'VIEW_3D':
            v3d = area.spaces[0]
            rv3d = v3d.region_3d
            for region in area.regions:
                if region.type == 'WINDOW':
                    if return_area: return region, rv3d, v3d, area
                    return region, rv3d, v3d
    return None, None

def createOverrideContext():
    region, rv3d, v3d, area = view3d_find(True)

    # Define context override dictionary for overriding the knife_project operator's context
    override = {
        'scene'            : bpy.context.scene,
        'region'           : region,
        'area'             : area,
        'space'            : v3d,
        'active_object'    : bpy.context.object,
        'window'           : bpy.context.window,
        'screen'           : bpy.context.screen,
        'selected_objects' : bpy.context.selected_objects,
        'edit_object'      : bpy.context.object,
        'region_3d'        : rv3d
    }
    
    originalRegion3D = {
        'view_location'     : copy.copy(rv3d.view_location),
        'view_distance'     : rv3d.view_distance,
        'view_rotation'     : copy.copy(rv3d.view_rotation),
        'view_perspective'  : rv3d.view_perspective}
    
    # Set the view to origin with a small distance so that we have a better precision for the knife projection.
    rv3d.view_location = (0,0,0)
    rv3d.view_distance = 1
    
    # Set view to TOP by directly rotating the 3D view region's view_rotation.
    rv3d.view_rotation = Euler( (0,0,0) ).to_quaternion()
    
    # Set the canera to orthographic.
    rv3d.view_perspective = 'ORTHO'
    
    return override, originalRegion3D

def createFaceOverrideContext(position, direction):
    region, rv3d, v3d, area = view3d_find(True)

    # Define context override dictionary for overriding the knife_project operator's context
    override = {
        'scene'            : bpy.context.scene,
        'region'           : region,
        'area'             : area,
        'space'            : v3d,
        'active_object'    : bpy.context.object,
        'window'           : bpy.context.window,
        'screen'           : bpy.context.screen,
        'selected_objects' : bpy.context.selected_objects,
        'edit_object'      : bpy.context.object,
        'region_3d'        : rv3d
    }
    
    print("after creating override")
    
    originalRegion3D = {
        'view_location'     : copy.copy(rv3d.view_location),
        'view_distance'     : rv3d.view_distance,
        'view_rotation'     : copy.copy(rv3d.view_rotation),
        'view_perspective'  : rv3d.view_perspective}
        
    print("after creating originalRegion3D")
    
    # Set the view to origin with a small distance so that we have a better precision for the knife projection.
    rv3d.view_location = position
    rv3d.view_distance = 1
    
    print("after setting position")
    
    print(direction == None)
    print(direction)
    print("direction = " + str(direction))
    # Set view to TOP by directly rotating the 3D view region's view_rotation.
    rotationQuat = direction.to_track_quat('Z', 'Y')
    print("rotationQuat = " + str(rotationQuat))
    print("euler rotation = " + str(rotationQuat.to_euler()))
    rv3d.view_rotation = rotationQuat
    print("after setting rotation")
    
    
    # Set the canera to orthographic.
    rv3d.view_perspective = 'ORTHO'
    
    print("after setting perspective")
    
    return override, originalRegion3D

def setRegion3D(override, originalConfiguration):
    rv3d = override['region_3d']
    
    rv3d.view_location      = originalConfiguration['view_location']
    rv3d.view_distance      = originalConfiguration['view_distance']
    rv3d.view_rotation      = originalConfiguration['view_rotation']
    rv3d.view_perspective   = originalConfiguration['view_perspective']
    
    

### Quick math. ###

def distance(aVert, bVert):
    return sqrt((aVert.x - bVert.x)**2 + (aVert.y - bVert.y)**2 + (aVert.z - bVert.z)**2)

# Compute the length of an edge.
# The edge here is a bmesh edge, regular edges don't carry the whole vertices.
def edgeLength(bmeshEdge):
    return distance(bmeshEdge.verts[0].co, bmeshEdge.verts[1].co)


### Handling meshes. ###

def findEdge(mesh, edgeKey):
    for currentEdge in mesh.edges:
        currentEdgeKey = currentEdge.key
        if (currentEdgeKey[0] == edgeKey[0] and currentEdgeKey[1] == edgeKey[1]):
            return currentEdge
        
def findEdgeIndex(mesh, edgeKey):
    foundEdge = findEdge(mesh, edgeKey)
    if not foundEdge==None:
        return foundEdge.index
    
# Return the tangent of a face.
# The face has to be a flat quad.
def faceTangent(object, faceIndex):
    face = object.data.polygons[faceIndex]
    
    # Take the first edge as the tangent.
    edgeKeys = face.edge_keys[0]
    
    vertA = object.data.vertices[edgeKeys[0]]
    vertB = object.data.vertices[edgeKeys[1]]
    
    tangent = (vertA.co - vertB.co).normalized()
    
    return tangent
        

def buildFaceTuple(objectToBrowse, faceIndex):
    polygon = objectToBrowse.data.polygons[faceIndex]
    
    verticesArray = []
    for currentVertexIndex in polygon.vertices:
        # Make a deep copy of the vertex to be sure it doesn't get modified accidentaly.
        currentCoordinates = objectToBrowse.data.vertices[currentVertexIndex].co
        verticesArray.append((currentCoordinates.x, currentCoordinates.y, currentCoordinates.z))
    
    return (faceIndex, verticesArray)


# The face tuple should contain first the index of the face, then an array of vertices.
def checkIsSameFace(objectToBrowse, faceTuple):
    faceIndex = faceTuple[0]
    verticesArray = faceTuple[1]
    
    faceToCheck = objectToBrowse.data.polygons[faceIndex]
    
    polygonMatches = True
    
    for currentVertexIndexFromPolygon in faceToCheck.vertices:
        currentVertexFromPolygon = objectToBrowse.data.vertices[currentVertexIndexFromPolygon]
        foundSameVertice = False
        for currentVertexFromArray in verticesArray:
            if  currentVertexFromArray[0] == currentVertexFromPolygon.co.x and currentVertexFromArray[1] == currentVertexFromPolygon.co.y and currentVertexFromArray[2] == currentVertexFromPolygon.co.z:
                foundSameVertice = True
        # If one of the vertices doesn't match, then the polygon doesn't match.
        if foundSameVertice == False:
            polygonMatches = False
    
    # If all the vertices in the polygon matched, then polygonMatches is still True.
    return polygonMatches


# Function to find a face in an object according to the position of the vertices it contains.
# This function is necessary because faces tend to change index when others are created.
def findFaceByVertices(objectToBrowse, verticesArray):
    foundFace = None
    
    dataToBrowse = objectToBrowse.data
    
    for currentPolygon in dataToBrowse.polygons:
        if checkIsSameFace(objectToBrowse, (currentPolygon.index, verticesArray)):
            foundFace = currentPolygon.index
    
    return foundFace



# Material related functions.

# Switch the selected material slot of the current object taking a string in.
def switchMaterialSlotByName(slotName):
    currentIndex = 0
    for currentSlot in bpy.context.object.material_slots:
        if currentSlot.name == slotName:
            bpy.context.object.active_material_index = currentIndex
            return
        currentIndex = currentIndex + 1
        
   
# Assign a material slot by name to the currently selected faces.
def assignMaterialSlotByName(slotName):
    # Switch to the right material slot.
    switchMaterialSlotByName(slotName)
    
    # Make sure we are in edit mode.
    bpy.ops.object.mode_set(mode = 'EDIT')
    
    # Assign the slot.
    bpy.ops.object.material_slot_assign()<file_sep>import bpy
import bmesh

import mathutils
import random
import time
from datetime import datetime
from mathutils import Euler

# Import submodules.
import sys
import os
import importlib
import copy

# Utils.
import utils_2_8
importlib.reload(utils_2_8)
from utils_2_8 import *

insetThickness      = 0.01   # Thickness of the inset operation.
insetDepth          = 0.01   # Depth of the inset operation.
insetRelativeOffset = True # True of the offset value should represent a proportion of offset.

inwardProbability   = 0.5   # Probability of the inset to go in the surface's direction.


def insetGeneric(seed, objectToInset, faceTuple):
    
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    random.seed(seed)
    
    # Find the right face despit faces that have been reindexed.
    #if checkIsSameFace(objectToInset, faceTuple):
        #print("face corresponding")
        #faceToSubdivideIndex = faceTuple[0]
    #else:
        #print("no correspondance")
        #faceToSubdivideIndex = findFaceByVertices(objectToInset, faceTuple[1])
        
    faceToSubdivideIndex = faceTuple[0]
    
    
    bpy.ops.object.mode_set(mode = 'EDIT')
    # Deselect everything.
    bpy.ops.mesh.select_all(action='DESELECT')
    
    # Selecting faces only works when not in edit mode, for some reason.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    # Select only the face to inset.
    objectToInset.data.polygons[faceToSubdivideIndex].select = True
    
    # Enter edit mode for the object in argument.
    bpy.context.view_layer.objects.active = objectToInset
    bpy.ops.object.mode_set(mode = 'EDIT')
    
    # Randomly pick if the inset is to be made inward or outward.
    if random.uniform(0, 1) < inwardProbability:
        finalDepth = -insetDepth
    else:
        finalDepth = insetDepth
    
    # Perform the inset operation.
    bpy.ops.mesh.inset(thickness=insetThickness, depth=finalDepth, use_relative_offset=insetRelativeOffset)
    
    # Refresh data.
    objectToInset.data.validate(verbose=True)
    objectToInset.update_from_editmode()
    
    # Return an array of face tuples for the selected face.
    # The selected face is the one resulting of the inset.
    faceTuplesResult = [buildFaceTuple(objectToInset, currentPolygon.index) for currentPolygon in objectToInset.data.polygons if currentPolygon.select]

    return faceTuplesResult


# Test function
if __name__ == "__main__":
    # Keep track of the selected object.
    originalySelectedObject = bpy.context.active_object
    
    originalySelectedObject.data.validate(verbose=True)
    originalySelectedObject.update_from_editmode()
    
    for currentPolygon in originalySelectedObject.data.polygons:
        if currentPolygon.select:
            insetGeneric(datetime.now(), originalySelectedObject, buildFaceTuple(originalySelectedObject, currentPolygon.index))<file_sep>import bpy
import bmesh

import mathutils
import random
import time
from datetime import datetime
from mathutils import Euler

# Import submodules.
import sys
import os
import importlib
import copy

# Utils.
import utils_2_8
importlib.reload(utils_2_8)
from utils_2_8 import *


# Global parameters.
minCuts = 1 # Minimum number of cuts per subdivision.
maxCuts = 4 # Maximum number of cuts per subdivision.
verticalProbability = 0.5 # Probability of doing a vertical subdivision.

# Conditions
minimumLength = 0.2


# Should be in object mode when calling this function.
def findFirstSelectedPolygon(objectToSubdivide):
    for currentPolygon in objectToSubdivide.data.polygons:
        if currentPolygon.select:
            return currentPolygon


def subdivideGeneric(seed, objectToSubdivide, faceTuple):
    
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    random.seed(seed)
    
    # Find the right face despit faces that have been reindexed.
    #if checkIsSameFace(objectToSubdivide, faceTuple):
        #faceToSubdivideIndex = faceTuple[0]
    #else:
        #faceToSubdivideIndex = findFaceByVertices(objectToSubdivide, faceTuple[1])

    faceToSubdivideIndex = faceTuple[0]
    faceToSubdivide = objectToSubdivide.data.polygons[faceToSubdivideIndex]
    
    # Sanity check.
    if len(faceToSubdivide.edge_keys) != 4:
        print("Face does not have 4 edges.")
        return
    
    # Decide whether the subidivision is to happen vertically of horizontally.
    if random.uniform(0, 1) < verticalProbability:
        verticalSubdivision = True
    else:
        verticalSubdivision = False
    
    if verticalSubdivision:
        aEdgeKey = faceToSubdivide.edge_keys[0]
    else:
        aEdgeKey = faceToSubdivide.edge_keys[1]
    
    # Minimum length test.
    point0Coordinates = objectToSubdivide.data.vertices[aEdgeKey[0]].co
    point1Coordinates = objectToSubdivide.data.vertices[aEdgeKey[1]].co
    edgeLength = distance(point0Coordinates, point1Coordinates)
    if edgeLength < minimumLength:
        return None
    
    bpy.ops.object.mode_set(mode = 'EDIT')
    # Deselect everything.
    bpy.ops.mesh.select_all(action='DESELECT')
    
    # Selecting faces only works when not in edit mode, for some reason.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    # Select only the face to inset.
    objectToSubdivide.data.polygons[faceToSubdivideIndex].select = True
    
    # Enter edit mode for the object in argument.
    bpy.context.view_layer.objects.active = objectToSubdivide
    bpy.ops.object.mode_set(mode = 'EDIT')
    
    bpy.ops.mesh.inset(thickness=0.0001)
    
    bpy.ops.object.mode_set(mode = 'OBJECT')
    faceToSubdivide_inset = findFirstSelectedPolygon(objectToSubdivide)
    
    if verticalSubdivision:
        aEdgeKey_inset = faceToSubdivide_inset.edge_keys[0]
        bEdgeKey_inset = faceToSubdivide_inset.edge_keys[2]
    else:
        aEdgeKey_inset = faceToSubdivide_inset.edge_keys[1]
        bEdgeKey_inset = faceToSubdivide_inset.edge_keys[3]
    
    # Find the chosen edges in the object's data.
    aEdge_inset = findEdge(objectToSubdivide.data, aEdgeKey_inset)
    bEdge_inset = findEdge(objectToSubdivide.data, bEdgeKey_inset)
    
    # Deselect all the faces.
    bpy.ops.object.mode_set(mode = 'EDIT')
    bpy.ops.mesh.select_all(action='DESELECT')
    
    # Find the index of the edges to select.
    # We get the index rather than the edge itself because of references issues.
    aEdgeIndex_inset = findEdgeIndex(objectToSubdivide.data, aEdgeKey_inset)
    bEdgeIndex_inset = findEdgeIndex(objectToSubdivide.data, bEdgeKey_inset)
    
    # Select the chosen edges. Be careful, edges can only be selected properly when in object mode.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    objectToSubdivide.data.edges[aEdgeIndex_inset].select = True
    objectToSubdivide.data.edges[bEdgeIndex_inset].select = True

    bpy.ops.object.mode_set(mode = 'EDIT')
    # Randomly choose how many cuts are going to be applied.
    numberOfCuts = random.randint(minCuts, maxCuts)
    # Apply the subdivision.
    subdivisionResult = bpy.ops.mesh.subdivide(number_cuts=numberOfCuts, quadcorner='INNERVERT')
    
    # Refresh data.
    objectToSubdivide.data.validate(verbose=True)
    objectToSubdivide.update_from_editmode()
    
    # We always want to exit functions in object mode.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    
    # Return an array of face tuples for the selected faces.
    # The selected faces are the ones resulting of the subdivision.
    faceTuplesResult = [buildFaceTuple(objectToSubdivide, currentPolygon.index) for currentPolygon in objectToSubdivide.data.polygons if currentPolygon.select]

    return faceTuplesResult


# Test function
if __name__ == "__main__":
    # Keep track of the selected object.
    originalySelectedObject = bpy.context.active_object
    
    originalySelectedObject.data.validate(verbose=True)
    originalySelectedObject.update_from_editmode()
    
    for currentPolygon in originalySelectedObject.data.polygons:
        if currentPolygon.select:
            subdivideGeneric(datetime.now(), originalySelectedObject, buildFaceTuple(originalySelectedObject, currentPolygon.index))<file_sep>import bpy
import bmesh
import mathutils
import random

# Probabilities.
randomSeed = 0 # Seed for randomization, different everytime when set to negative.
rectangleProbability = 0.5 # Probability to pop a rectangle rather than a sphere.
poppingNewEdge = 1.0 # Probability for each edge to enter a recursive cycle if the recursivity limit has not been hit.
notchType45 = 0.5 # Probability to pop a 45 degrees notch.
outerProbability = 0.5 # Probability to have a notch coming outward rather than inward.
relativeWidthMin = 0.1 # Minimum length of a notch relative to the edge it is created from.
relativeWidthMax = 0.9 # Maximum length of a notch relative to the edge it is created from.
relativeDepthWidthRatioMax = 0.3 # Maximum depth of a notch relative to it's length.

# Rectangles probabilities.
maximumRatioDifference = 0.2 # Maximum ratio between height and width, smaller values make for a bigger ratio. Should belong to [0 ; 1].
verticalProbability = 0.5 # Probability of poping a vertical rectangle if maximumRatioDifference is different from 1.

# Circle probabilities.
minmumCircleEdges = 3 # Minimum possible edges in a circle.
maximumCircleEdges = 6 # Maximum possible edges in a circle.

# Global settings.
recursivity = 0 # Recursivity limit.
squareRadius = 10 # Drives the number of shapes generated.



# ___________    ->    _____       _____
#                           |_____|
def edgeToNotch90(originalBmesh, edge, relativeWidth, relativeDepth, outer):
    # Remember the original 2 vertices.
    vertA = edge.verts[0]
    vertB = edge.verts[1]
    
    # First delete the original edge (but not the associated verts).
    edgeToDelete = [edge]
    bmesh.ops.delete(originalBmesh, geom=edgeToDelete, context=4)
    
    # Local coordinate system.
    vectorU = vertB.co - vertA.co
    vectorV = mathutils.Vector((-vectorU.y, vectorU.x, 0))
    
    if (outer):
        vectorV = -vectorV
    
    # Temp values
    thinnestOffset = 0.05
    widthOffset = random.uniform(thinnestOffset, (1.0 - thinnestOffset) - relativeWidth)
    
    # Outer part.
    # C vert.
    vertC = originalBmesh.verts.new(vertA.co + widthOffset * vectorU)
    # F vert.
    vertF = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth) * vectorU)
    
    # Inner part.
    # D vert.
    vertD = originalBmesh.verts.new(vertA.co + widthOffset * vectorU + relativeDepth * vectorV)
    # E vert.
    vertE = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth) * vectorU + relativeDepth * vectorV)
    
    # Link vertices as edges.
    newEdges = []
    newEdges.append(originalBmesh.edges.new((vertA, vertC)))
    newEdges.append(originalBmesh.edges.new((vertC, vertD)))
    newEdges.append(originalBmesh.edges.new((vertD, vertE)))
    newEdges.append(originalBmesh.edges.new((vertE, vertF)))
    newEdges.append(originalBmesh.edges.new((vertF, vertB)))
    
    return newEdges




# ___________    ->    _____       _____
#                           \_____/
def edgeToNotch45(originalBmesh, edge, relativeWidth, relativeDepth, outer):
    
    originalBmesh.edges.ensure_lookup_table()
    
    # Remember the original 2 vertices.
    vertA = edge.verts[0]
    vertB = edge.verts[1]
    
    # First delete the original edge (but not the associated verts).
    edgeToDelete = [edge]
    bmesh.ops.delete(originalBmesh, geom=edgeToDelete, context=4)
    
    originalBmesh.edges.ensure_lookup_table()
    
    # Local coordinate system.
    vectorU = vertB.co - vertA.co
    vectorV = mathutils.Vector((-vectorU.y, vectorU.x, 0))
    
    if (outer):
        vectorV = -vectorV
    
    # Temp values
    thinnestOffset = 0.05
    widthOffset = random.uniform(thinnestOffset, (1.0 - thinnestOffset) - relativeWidth)
    
    # Outer part.
    # C vert.
    vertC = originalBmesh.verts.new(vertA.co + widthOffset * vectorU)
    # F vert.
    vertF = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth) * vectorU)
    
    # Inner part.
    # D vert.
    vertD = originalBmesh.verts.new(vertA.co + (widthOffset + relativeDepth) * vectorU + relativeDepth * vectorV)
    # E vert.
    vertE = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth - relativeDepth) * vectorU + relativeDepth * vectorV)
    
    # Link vertices as edges.
    newEdges = []
    newEdges.append(originalBmesh.edges.new((vertA, vertC)))
    newEdges.append(originalBmesh.edges.new((vertC, vertD)))
    newEdges.append(originalBmesh.edges.new((vertD, vertE)))
    newEdges.append(originalBmesh.edges.new((vertE, vertF)))
    newEdges.append(originalBmesh.edges.new((vertF, vertB)))
    
    return newEdges
    
    
def genericEdgeTransformation(originalBmesh, edgeToTransform, recursionDepth):
    # Decide if there is going to be a recursion pass for this edge.
    if(random.uniform(0, 1) < poppingNewEdge):
        
        # Temp variables.
        currentWidth = random.uniform(relativeWidthMin, relativeWidthMax)
        currentDepth = random.uniform(0.01, currentWidth * relativeDepthWidthRatioMax)
        outer = random.uniform(0,1) < outerProbability
        
        # Decide what the next recursive function is.
        if(random.uniform(0, 1) < notchType45):
            returnedEdges = edgeToNotch45(originalBmesh, edgeToTransform, currentWidth, currentDepth, outer)
        else:
            returnedEdges = edgeToNotch90(originalBmesh, edgeToTransform, currentWidth, currentDepth, outer)
        
        # Recursion.
        if recursionDepth > 0:
            for currentEdge in returnedEdges:
                genericEdgeTransformation(originalBmesh, currentEdge, recursionDepth - 1)
        
        originalBmesh.edges.ensure_lookup_table()
        

# Loops trough edges to add detail recursively.
# The shape to change must be selected before entering the function.
def genericShapeTransformation(recursionDepth):
    
    # Enter edit mode.
    bpy.ops.object.mode_set(mode = 'EDIT')

    # Delete the center face.
    bpy.ops.mesh.delete(type='ONLY_FACE')

    # Set the selection mode to edges.
    bpy.ops.mesh.select_mode(type="EDGE")

    # Get the active mesh
    obj = bpy.context.edit_object
    me = obj.data

    # Get a BMesh representation
    bm = bmesh.from_edit_mesh(me)

    bpy.ops.mesh.select_all(action='DESELECT')

    # Browse edges.
    bm.edges.ensure_lookup_table()

    print("Initial edges = " + str(len(bm.edges)))
        
    for currentEdge in range(0, len(bm.edges)):
        genericEdgeTransformation(bm, bm.edges[currentEdge], recursionDepth)
            
        
    print("Final edges = " + str(len(bm.edges)))

    bm.edges.ensure_lookup_table()

    # Select random vertices.
    # Set the selection mpde to edges.
    bpy.ops.mesh.select_mode(type="VERT")
    bpy.ops.mesh.select_all(action='DESELECT')
    bpy.ops.mesh.select_random()


    # Show the updates in the viewport
    # and recalculate n-gon tessellation.
    bmesh.update_edit_mesh(me, True)


    # Exit edit mode.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    
    
        
# Generates a circle shape of 6 edges with recursive details.
def generateCircleCuttingShape(position, dimension, edgeCount, recursionDepth):
    
    # Generate a plane.
    bpy.ops.mesh.primitive_circle_add(radius=0.4, vertices=edgeCount, location=(position))
    
    # Resize object in edit mode.
    bpy.ops.object.mode_set(mode = 'EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.transform.resize(value=(dimension[0], dimension[1], 1.0), constraint_orientation='GLOBAL')
    bpy.ops.object.mode_set(mode = 'OBJECT')


    # Generic function to add details on edges.
    genericShapeTransformation(recursionDepth)    
    
    
# Generates a rectangular shape with recursive details.
def generateRectangleCuttingShape(position, dimension, recursionDepth):

    # Generate a plane.
    bpy.ops.mesh.primitive_plane_add(radius=0.4, view_align=False, enter_editmode=False, location=(position))
    
    # Resize object in edit mode.
    bpy.ops.object.mode_set(mode = 'EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.transform.resize(value=(dimension[0], dimension[1], 1.0), constraint_orientation='GLOBAL')
    bpy.ops.object.mode_set(mode = 'OBJECT')
    
    # Generic function to add details on edges.
    genericShapeTransformation(recursionDepth)
    
    
    
    # Main

print("starting main")

# Intialize random seed
if randomSeed >= 0 :
    random.seed(randomSeed)

#generateSquareCuttingShape(position=(0, 0, 0), recursionDepth=recursivity)

for xCoords in range(-squareRadius, squareRadius):
    for yCoords in range(-squareRadius, squareRadius):
        if random.uniform(0,1) < rectangleProbability :
            # Generate a rectangle of random dimension.
            downscaleValue = random.uniform(maximumRatioDifference, 1.0)
            # Apply downscale on either x or y.
            if random.uniform(0,1) < verticalProbability :
                generateRectangleCuttingShape(position=(xCoords, yCoords, 0), dimension=((downscaleValue, 1)), recursionDepth=recursivity)
            else:
                generateRectangleCuttingShape(position=(xCoords, yCoords, 0), dimension=((1, downscaleValue)), recursionDepth=recursivity)
                
        else:
            # Generate a circle of random vertices.
            verticesCount = random.uniform(minmumCircleEdges, maximumCircleEdges)
            generateCircleCuttingShape(position=(xCoords, yCoords, 0), dimension=((1.0, 1.0)), edgeCount=verticesCount, recursionDepth=recursivity)<file_sep>import bpy
import bmesh

import mathutils
import random
import time
from datetime import datetime

# Import submodules.
import sys
import os
import importlib
import copy

blend_dir = os.path.dirname(bpy.data.filepath)
if blend_dir not in sys.path:
   sys.path.append(blend_dir)
   
# First inset.
firstInsetDepth = -0.03
firstInsetThickness = 0.0

# Surface cutting.  
import cut_surface0_2_8
importlib.reload(cut_surface0_2_8)
from cut_surface0_2_8 import *

# Subdivisions.
import subdivide_surface_2_8
importlib.reload(subdivide_surface_2_8)
from subdivide_surface_2_8 import *

# Recursive insets.
import inset_surface_2_8
importlib.reload(inset_surface_2_8)
from inset_surface_2_8 import *

    
subdivisionProbability = 1.8
insetProbability = 0.1


# Parameters for other modules.
subdivide_surface_2_8.minimumLength = 0.05
# Modify cutting settings.
cut_surface0_2_8.bevelOffset = 0.001
cut_surface0_2_8.cuttingShapeMargin = 0.9
cut_surface0_2_8.cleanFaceMargin = 0.7
# Modify cutting shapes generation.
generate_cuttingShape0_2_8.roundProbability = 0.0

# Recursive settings.
recursiveDepth = 3
subdivisionOverCutProbability = 0.7

# Batches generation.
batchSize = 5
seedOffsetForBatches = 6



def subdivideFaces(seed, objectToBrowse, facesTuples):
    
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    random.seed(seed)
    
    totalFacesTuples = []
    for currentFaceTuple in facesTuples:
        if random.uniform(0, 1) < subdivisionProbability:
            generatedFacesTuples = subdivideGeneric(seed, objectToBrowse, currentFaceTuple)
            if(generatedFacesTuples is not None):
                totalFacesTuples.extend(generatedFacesTuples)
                
    # Sort the arrays from in descending order according to the index, to avoid index offsetting side effects when modifying a face.
    totalFacesTuples = sorted(totalFacesTuples, key= lambda faceTuple: faceTuple[0], reverse = True)
    print("totalFacesTuples (after subdivision) = " + str([currentTuple[0] for currentTuple in totalFacesTuples]))
    
    
    resultFacesTuples = []
    for currentFaceTuple in totalFacesTuples:
        if random.uniform(0, 1) < insetProbability:
            generatedFacesTuples = insetGeneric(seed, objectToBrowse, currentFaceTuple)
            resultFacesTuples.extend(generatedFacesTuples)
        else:
            resultFacesTuples.append(currentFaceTuple)
    
    # Sort the arrays from in descending order according to the index, to avoid index offsetting side effects when modifying a face.
    resultFacesTuples = sorted(resultFacesTuples, key= lambda faceTuple: faceTuple[0], reverse = True)
    print("resultFacesTuples (after inset) = " + str([currentTuple[0] for currentTuple in resultFacesTuples]))
        
    return resultFacesTuples


def subdivideOrCut(seed, objectToBrowse, facesTuples):
    
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    random.seed(seed)
    
    print("subdivideOrCut facesTuples = " + str([currentTuple[0] for currentTuple in facesTuples]))
    
    # Final result to return.
    finalFacesTuples = []
    
    for currentFaceTuple in facesTuples:
        resultingFacesTuples = []
        
        # Randomly pick a subdivision or cut operation.
        subdivisionOverCut = random.uniform(0, 1) < subdivisionOverCutProbability
        
        if subdivisionOverCut:
            resultingFacesTuples = subdivideFaces(random.randint(0, 100000), objectToBrowse, [currentFaceTuple])
        else:
            resultingFacesTuples = [genericCutPlate(random.randint(0, 100000), objectToBrowse, currentFaceTuple)]
        # This might be necessary to refresh the object, check this.
        bpy.ops.object.mode_set(mode = 'OBJECT')
            
        # Add the resulting faces to the final total array.
        if resultingFacesTuples == None:
            finalFacesTuples.append(currentFaceTuple)
        else:
            finalFacesTuples.extend(resultingFacesTuples)
    
    # Filter out empty elements.
    finalFacesTuples = [currentTuple for currentTuple in finalFacesTuples if not (currentTuple == [] or currentTuple == None) ]
    
    print("finalFacesTuples before sort = " + str([currentTuple[0] for currentTuple in finalFacesTuples]))
    
    # Sort the arrays from in descending order according to the index, to avoid index offsetting side effects when modifying a face.
    finalFacesTuples = sorted(finalFacesTuples, key= lambda faceTuple: faceTuple[0], reverse = True)
    print("finalFacesTuples after sort = " + str([currentTuple[0] for currentTuple in finalFacesTuples]))
    
    return finalFacesTuples



def recursiveGeneration(seed, objectToModify, facesToModify, recursiveDepth):
    
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    random.seed(seed)

    # Subdivide or cut recursively.
    # Traverse the created faces in a descending order according to their index, to avoid issues with index offsetting when creating new faces.
    for currentFaceToModify in facesToModify:
        resultingFaceTuples = subdivideOrCut(random.randint(0, 100000), objectToModify, [currentFaceToModify])
    
        # Faces should have been returned in a descending order for the index.
        if recursiveDepth > 0:
            recursiveGeneration(random.randint(0, 100000), objectToModify, resultingFaceTuples, recursiveDepth - 1)

   
 
def generateBatch(squareSize):

    #datetime.now()
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    #random.seed(1)
    random.seed(datetime.now())
    
    facesCount = 0
    for xCoords in range(0, squareSize):
        for yCoords in range(0, squareSize):
            
            bpy.ops.mesh.primitive_plane_add(size=0.95, align='WORLD', enter_editmode=False, location=(xCoords, yCoords, 0))
            
            #bpy.ops.transform.rotate(value=1.1055, orient_axis='X', orient_type='GLOBAL', orient_matrix=((-0.527618, 0.849482, 6.25849e-07), (-0.432952, -0.268908, -0.860373), (0.730871, 0.453949, -0.509665)), orient_matrix_type='VIEW', mirror=True)
            bpy.ops.transform.rotate(value=random.uniform(0, 4), orient_axis='Z', orient_type='VIEW', orient_matrix=((0.993576, 0.113164, 4.95464e-07), (-0.0669248, 0.587603, -0.806377), (0.0912528, -0.801197, -0.591402)), orient_matrix_type='VIEW')
            #bpy.ops.transform.rotate(value=-1.5708, orient_axis='X', orient_type='GLOBAL', orient_matrix=((-0.527618, 0.849482, 6.25849e-07), (-0.432952, -0.268908, -0.860373), (0.730871, 0.453949, -0.509665)), orient_matrix_type='VIEW', mirror=True)
            
            position = (1,0,0)
            bpy.ops.transform.translate(value=(-position[0], -position[1], -position[2]), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL')
            
            bpy.ops.object.transform_apply() # TODO: remove this later, only for temporary edit mode normal direction test.
            
        
            # Keep track of the selected object.
            originalySelectedObject = bpy.context.active_object
            
            # Find the first and only polygon in it.
            firstPolygonTuple = buildFaceTuple(originalySelectedObject, originalySelectedObject.data.polygons[0].index)
            firstPolygon = originalySelectedObject.data.polygons[0]
            
            # Recursive generation.
            recursiveGeneration(seedOffsetForBatches + facesCount, originalySelectedObject, [firstPolygonTuple], recursiveDepth)
            facesCount = facesCount + 1
            
            bpy.ops.object.mode_set(mode = 'OBJECT')
    


#    bpy.ops.object.mode_set(mode = 'EDIT')
    
    override, originalRegion3D = createOverrideContext()
    
    # Reset the view to it's original configuration.
    setRegion3D(override, originalRegion3D)
    

def applyToSelectedFaces():
    
    if not bpy.context.mode == 'EDIT_MESH':
        print("applyToMesh function should be performed in edit mode.")
        return
    
    # Keep a reference to the object to modify.
    objectToModify = bpy.context.edit_object
    
    
    # Preliminary operations, before any recursion.
    # Inset.
    bpy.ops.mesh.inset(thickness=firstInsetThickness, depth=firstInsetDepth)
    
    # Apply the details material to the faces inside the inset.
    assignMaterialSlotByName("hardsurface_details_material")
    
    # Populate an array of currently selected faces.
    # First the mandatory object mode to have up to date select values for faces.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    
    # The faces should have 4 vertices exactly.
    # Sort the array from in descending order according to the index, to avoid index offsetting side effects when modifying a face.
    selectedFacesIndexes = [currentFace.index for currentFace in objectToModify.data.polygons if currentFace.select and len(currentFace.vertices) == 4]
    selectedFacesIndexes = sorted(selectedFacesIndexes, reverse = True)
    
    totalFaces = len(selectedFacesIndexes)
    counter = 0
    
    for currentFaceIndex in selectedFacesIndexes:
        # Progression.
        counter = counter + 1
        print(str(counter) + " of " + str(totalFaces) + " faces.")
        
        # Build tuple.
        firstPolygonTuple = buildFaceTuple(objectToModify, objectToModify.data.polygons[currentFaceIndex].index)
        
        # Recursive generation.
        recursiveGeneration(counter, objectToModify, [firstPolygonTuple], recursiveDepth)
    
    # Go back in edit mode.
    bpy.ops.object.mode_set(mode = 'EDIT')
    
    
# Test function
if __name__ == "__main__":
    
    print("////// Recusivity manager launch //////")
    
#    generateBatch(batchSize)
    applyToSelectedFaces()<file_sep># Procedural_hardsurface
Blender scripts related to procedural details generation for "hard surface" geometry.

The scripts are in a really primitive state for now, you will have to run them manually:
- Open the scripting layout of Blender
- In the text editor window: "Text"->"Open", then select the script corresponding to your Blender version
- Finally, click "Run Script" -> new meshes will be added in the 3D view
<file_sep>import bpy
import bmesh

import mathutils
import random
from datetime import datetime


# Import submodules.
import sys
import os
import importlib
import copy

blend_dir = os.path.dirname(bpy.data.filepath)
if blend_dir not in sys.path:
   sys.path.append(blend_dir)

# Utils.
import utils_2_8
importlib.reload(utils_2_8)
from utils_2_8 import *



# Probabilities.
randomSeed = 0 # Seed for randomization, different everytime when set to negative.
poppingNewEdge = 1.0 # Probability for each edge to enter a recursive cycle if the recursivity limit has not been hit.
notchType45 = 0.5 # Probability to pop a 45 degrees notch.
outerProbability = 0.5 # Probability to have a notch coming outward rather than inward.
relativeWidthMin = 0.1 # Minimum length of a notch relative to the edge it is created from.
relativeWidthMax = 0.9 # Maximum length of a notch relative to the edge it is created from.
relativeDepthWidthRatioMax = 0.3 # Maximum depth of a notch relative to it's length.
thinnestOffset = 0.05 # The minimum distance between the beginning of a notch in the end of the initial edge.

# Rounding probabilities.
roundProbability = 0.0 # Probability of turning a 90 degrees angle into a curve.
outerRoundProbability = 0.5 # Probability of the curve to face outward.
roundSegments = 5 # Number of vertices composing the curve.

# Rectangles probabilities.
rectangleProbability = 0.5 # Probability to pop a rectangle rather than a sphere.
maximumRatioDifference = 0.2 # Maximum ratio between height and width, smaller values make for a bigger ratio. Should belong to [0 ; 1].
verticalProbability = 0.5 # Probability of poping a vertical rectangle if maximumRatioDifference is different from 1.

# Circle probabilities.
minmumCircleEdges = 3 # Minimum possible edges in a circle.
maximumCircleEdges = 6 # Maximum possible edges in a circle.


# Global settings.
recursivity = 0 # Recursivity limit.
symetry = False # When True, all edges will have the same details generated, even recursively.


# This function generates a square of 1 unit on the sides, facing up.
# Then returns it.
def generateUnitSquare():
	verts = [   (-0.5, -0.5, 0),
				(0.5, -0.5, 0),
				(0.5, 0.5, 0),
				(-0.5, 0.5, 0)] 

	mesh = bpy.data.meshes.new("UnitPlaneMesh")  # add a new mesh
	obj = bpy.data.objects.new("UnitPlane", mesh)  # add a new object using the mesh

	scene = bpy.context.scene
	#scene.objects.link(obj)  # put the object into the scene (link)
#    scene.objects.active = obj  # set as the active object in the scene
#    obj.select = True  # select object

#    mesh = bpy.context.object.data
	bm = bmesh.new()

	for v in verts:
		bm.verts.new(v)  # add a new vert

	# Refresh bmesh.
	bm.verts.ensure_lookup_table()   
	bm.edges.ensure_lookup_table()
		
	bm.edges.new((bm.verts[0], bm.verts[1]))
	bm.edges.new((bm.verts[1], bm.verts[2]))
	bm.edges.new((bm.verts[2], bm.verts[3]))
	bm.edges.new((bm.verts[3], bm.verts[0]))

	# make the bmesh the object's mesh
	bm.to_mesh(mesh)  
	bm.free()  # always do this when finished
	
	return obj


# ___________    ->    _____	   _____
#   						|_____|

def edgeToNotch90(originalBmesh, edge, relativeWidth, widthOffset, relativeDepth, outer):
	
	# Remember the original 2 vertices.
	vertA = edge.verts[0]
	vertB = edge.verts[1]
	
	# First delete the original edge (but not the associated verts).
	edgeToDelete = [edge]
	bmesh.ops.delete(originalBmesh, geom=edgeToDelete, context='EDGES')
	
	# Local coordinate system.
	vectorU = vertB.co - vertA.co
	vectorV = mathutils.Vector((-vectorU.y, vectorU.x, 0))
	
	if (outer):
		vectorV = -vectorV
	
	# Outer part.
	# C vert.
	vertC = originalBmesh.verts.new(vertA.co + widthOffset * vectorU)
	# F vert.
	vertF = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth) * vectorU)
	
	# Inner part.
	# D vert.
	vertD = originalBmesh.verts.new(vertA.co + widthOffset * vectorU + relativeDepth * vectorV)
	# E vert.
	vertE = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth) * vectorU + relativeDepth * vectorV)

	# Link vertices as edges.
	newEdges = []
	newEdges.append(originalBmesh.edges.new((vertA, vertC)))
	newEdges.append(originalBmesh.edges.new((vertC, vertD)))
	newEdges.append(originalBmesh.edges.new((vertD, vertE)))
	newEdges.append(originalBmesh.edges.new((vertE, vertF)))
	newEdges.append(originalBmesh.edges.new((vertF, vertB)))
	
	return newEdges




# ___________    ->    _____	   _____
#   						\_____/

def edgeToNotch45(originalBmesh, edge, relativeWidth, widthOffset, relativeDepth, outer):
	
	originalBmesh.edges.ensure_lookup_table()
	
	# Remember the original 2 vertices.
	vertA = edge.verts[0]
	vertB = edge.verts[1]
	
	# First delete the original edge (but not the associated verts).
	edgeToDelete = [edge]
	bmesh.ops.delete(originalBmesh, geom=edgeToDelete, context='EDGES')
	
	originalBmesh.edges.ensure_lookup_table()
	
	# Local coordinate system.
	vectorU = vertB.co - vertA.co
	vectorV = mathutils.Vector((-vectorU.y, vectorU.x, 0))
	
	if (outer):
		vectorV = -vectorV
	
	# Outer part.
	# C vert.
	vertC = originalBmesh.verts.new(vertA.co + widthOffset * vectorU)
	# F vert.
	vertF = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth) * vectorU)
	
	# Inner part.
	# D vert.
	vertD = originalBmesh.verts.new(vertA.co + (widthOffset + relativeDepth) * vectorU + relativeDepth * vectorV)
	# E vert.
	vertE = originalBmesh.verts.new(vertA.co + (widthOffset + relativeWidth - relativeDepth) * vectorU + relativeDepth * vectorV)
	
	# Link vertices as edges.
	newEdges = []
	newEdges.append(originalBmesh.edges.new((vertA, vertC)))
	newEdges.append(originalBmesh.edges.new((vertC, vertD)))
	newEdges.append(originalBmesh.edges.new((vertD, vertE)))
	newEdges.append(originalBmesh.edges.new((vertE, vertF)))
	newEdges.append(originalBmesh.edges.new((vertF, vertB)))
	
	return newEdges

def vertToRound(originalBmesh, vertList, outerRound):
	
	originalBmesh.edges.ensure_lookup_table()
	
	if outerRound:
		chosenProfile = 0.125
	else:
		chosenProfile = 0.5
	
	# Rounding operation.
	bmesh.ops.bevel(originalBmesh, geom=vertList, offset_type='OFFSET', offset=1.05, segments=roundSegments, profile=chosenProfile, vertex_only=True, clamp_overlap=True)
	
	originalBmesh.edges.ensure_lookup_table()
	
	
def genericEdgeTransformation(seed, originalBmesh, edgeToTransform, recursionDepth):
	
	# Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
	random.seed(seed)

	# Useful for the final depth computation.
	edgeToTransformLength = edgeLength(edgeToTransform)

	# Variables to return.
	currentDepth = None
	outer = None
	
	# Decide if there is going to be a recursion pass for this edge.
	if(random.uniform(0, 1) < poppingNewEdge):
		
		# Temp variables.
		currentWidth = random.uniform(relativeWidthMin, relativeWidthMax)
		widthOffset = random.uniform(thinnestOffset, (1.0 - thinnestOffset) - currentWidth)
		currentDepth = random.uniform(0.01, currentWidth * relativeDepthWidthRatioMax)
		outer = random.uniform(0,1) < outerProbability
		
		# Decide what the next recursive function is.
		if(random.uniform(0, 1) < notchType45):
			returnedEdges = edgeToNotch45(originalBmesh, edgeToTransform, currentWidth, widthOffset, currentDepth, outer)
		else:
			returnedEdges = edgeToNotch90(originalBmesh, edgeToTransform, currentWidth, widthOffset, currentDepth, outer)
		
			# Only for 90 degrees notches will we round some vertices up.
			# Count vertices that are encountered twice, therefore all the vertices that do not belong to the sides of this edge selection.
			vertDictionary = {}
			for currentEdge in returnedEdges:
				for currentVert in currentEdge.verts:
					if currentVert in vertDictionary:
						vertDictionary[currentVert] = vertDictionary[currentVert] + 1
					else:
						vertDictionary[currentVert] = 1
			
			# Select only the inner vertices.   		 
			innerVertices = [currentVert for currentVert, currentCount in vertDictionary.items() if currentCount >= 2]
			# Of the inner vertices, select only some randomly.
			innerVerticesRandom = [currentVert for currentVert in innerVertices if random.uniform(0,1) < roundProbability]
			
			#Randomly choose if an the rounding will be an inner or outer one.
			outerRound = random.uniform(0,1) < outerRoundProbability
			# Apply the rounding to the randomly selected vertices.
			vertToRound(originalBmesh, innerVerticesRandom, outerRound)
		
		# Recursion.
		if recursionDepth > 0:
			for currentEdge in returnedEdges:
				if symetry:
					# When symetry is enabled, take the same seed for every edge.
					futureSeed = seed
				else:
					# When symetry is disabled, randomize the seed for every edge.
					futureSeed = random.randint(0, 1000000)
			
				genericEdgeTransformation(futureSeed, originalBmesh, currentEdge, recursionDepth - 1)
		
		originalBmesh.edges.ensure_lookup_table()

		# The actual depth length is the proportional depth * the edge length.
		currentDepth = currentDepth * edgeToTransformLength
		
		if outer:
			return currentDepth
		else:
			return -currentDepth
		
		

# Loops trough edges to add detail recursively.
# The shape to change must be selected before entering the function.
def genericShapeTransformation(seed, recursionDepth):
	# Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
	random.seed(seed)
	
	# Enter edit mode.
	bpy.ops.object.mode_set(mode = 'EDIT')

	# Delete the center face.
	bpy.ops.mesh.delete(type='ONLY_FACE')

	# Set the selection mode to edges.
	bpy.ops.mesh.select_mode(type="EDGE")

	# Get the active mesh
	obj = bpy.context.edit_object
	me = obj.data

	# Get a BMesh representation
	bm = bmesh.from_edit_mesh(me)

	bpy.ops.mesh.select_all(action='DESELECT')

	# Browse edges.
	bm.edges.ensure_lookup_table()

#    print("Initial edges = " + str(len(bm.edges)))
	
	# Keep track of the maximum depth (negative or positive) encountered on each edge.
	edgesDepth = []
	
	for currentEdge in range(0, len(bm.edges)):
		if symetry:
			# When symetry is enabled, take the same seed for every edge.
			futureSeed = seed
		else:
			# When symetry is disabled, randomize the seed for every edge.
			futureSeed = random.randint(0, 1000000)
			
		currentEdgeDepth = genericEdgeTransformation(futureSeed, bm, bm.edges[currentEdge], recursionDepth)
		edgesDepth.append(currentEdgeDepth)

	# Some of the previous operations can cause some vertices to be at the same position, remove duplicates.
	bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
	
#    print("Final edges = " + str(len(bm.edges)))

	bm.edges.ensure_lookup_table()


	# Show the updates in the viewport
	# and recalculate n-gon tessellation.
	bmesh.update_edit_mesh(me, True)


	# Exit edit mode.
	bpy.ops.object.mode_set(mode = 'OBJECT')

	return edgesDepth
	
		
# Generates a circle shape of 6 edges with recursive details.
def generateCircleCuttingShape(seed, position, dimension, edgeCount, recursionDepth):
	
	# Generate a plane.
	bpy.ops.mesh.primitive_circle_add(radius=0.4, vertices=edgeCount, location=(position))

	# Keep track of the created object.
	createdShape = bpy.context.active_object
	createdShape.name = "circle_cuttingShape"
	createdShape.data.name = "circle_cuttingShape_mesh"
	
	# Resize object in edit mode.
	bpy.ops.object.mode_set(mode = 'EDIT')
	bpy.ops.mesh.select_all(action='SELECT')
	bpy.ops.transform.resize(value=(dimension[0], dimension[1], 1.0), orient_type='GLOBAL')
	bpy.ops.object.mode_set(mode = 'OBJECT')

	# Generic function to add details on edges.
	genericShapeTransformation(seed, recursionDepth)
	
	
# Generates a rectangular shape with recursive details.
def generateRectangleCuttingShape(seed, position, dimension, recursionDepth):

	# Generate a plane.
	bpy.ops.mesh.primitive_plane_add(size=1.0, align='WORLD', enter_editmode=False, location=(position))
	
	# Keep track of the created object.
	createdShape = bpy.context.active_object
	createdShape.name = "rectangle_cuttingShape"
	createdShape.data.name = "rectangle_cuttingShape_mesh"
	
	# Resize object in edit mode.
	bpy.ops.object.mode_set(mode = 'EDIT')
	bpy.ops.mesh.select_all(action='SELECT')
	bpy.ops.transform.resize(value=(dimension[0], dimension[1], 1.0), orient_type='GLOBAL')
	bpy.ops.object.mode_set(mode = 'OBJECT')
	
	# Generic function to add details on edges.
	edgesDepth = genericShapeTransformation(seed, recursionDepth)

	return createdShape, edgesDepth
	
	
def generateGenericCuttingShape(seed, position):
	# Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
	random.seed(seed)
	
	if random.uniform(0,1) < rectangleProbability :
		# Generate a rectangle of random dimension.
		downscaleValue = random.uniform(maximumRatioDifference, 1.0)
		# Apply downscale on either x or y.
		if random.uniform(0,1) < verticalProbability :
			createdShape = generateRectangleCuttingShape(seed=seed, position=position, dimension=((downscaleValue, 1)), recursionDepth=recursivity)
		else:
			createdShape = generateRectangleCuttingShape(seed=seed, position=position, dimension=((1, downscaleValue)), recursionDepth=recursivity)
			
	else:
		# Generate a circle of random vertices.
		verticesCount = random.uniform(minmumCircleEdges, maximumCircleEdges)
		createdShape = generateCircleCuttingShape(seed=seed, position=position, dimension=((1.0, 1.0)), edgeCount=verticesCount, recursionDepth=recursivity)

	return createdShape







# Populate a square with random cutting shapes.
# squareRadius, Drives the number of shapes generated.
# generateNewCollection, When true, generate a new collection for each batch, when false, replaces the old collection content.
def generateCuttingShapesArray(squareRadius = 5, generateNewCollection = False):

	print("generating a collection of hard surface shapes")

	if not generateNewCollection:
		# Delete the old collection
		singletonCollectionIndex = bpy.context.scene.collection.children.find("hardsurface_shapes")
		if singletonCollectionIndex >= 0:
			singletonCollection = bpy.context.scene.collection.children[singletonCollectionIndex]
			# Remove meshes data to avoid orphans.
			for currentObject in singletonCollection.objects:
				bpy.data.meshes.remove(currentObject.data)
			# Remove the collection from the scene.
			bpy.context.scene.collection.children.unlink(singletonCollection)
			bpy.data.collections.remove(singletonCollection)
		
	# Create a new collection.
	testCollection = bpy.data.collections.new("hardsurface_shapes")
	# Link it to the current scene.
	bpy.context.scene.collection.children.link(testCollection)
	# Make the added collection active.
	bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[testCollection.name]

	# Make all collections invisible excepting for the new one.
	for currentChild in bpy.context.scene.collection.children:
		currentChild.hide_viewport = True
	testCollection.hide_viewport = False



	# Intialize seed.
	if randomSeed >= 0 :
		random.seed(randomSeed)
	else:
		# Take the current time to randomize the seed when we want it different each time.
		random.seed(datetime.now())

	#generateGenericShape(seed=randomSeed, position=(0, 0, 0))

	for xCoords in range(-squareRadius, squareRadius):
		for yCoords in range(-squareRadius, squareRadius):
			generateGenericCuttingShape(seed=xCoords + yCoords * squareRadius * 2, position=(xCoords, yCoords, 0))


# Test function
if __name__ == "__main__":
	generateCuttingShapesArray()<file_sep>import bpy
import bmesh

import mathutils
import random
from datetime import datetime
from mathutils import Euler

# Import submodules.
import sys
import os
import importlib
import copy

blend_dir = os.path.dirname(bpy.data.filepath)
if blend_dir not in sys.path:
   sys.path.append(blend_dir)

# Cutting shapes generation.
import generate_cuttingShape0_2_8
importlib.reload(generate_cuttingShape0_2_8)
from generate_cuttingShape0_2_8 import *

# Utils.
import utils_2_8
importlib.reload(utils_2_8)
from utils_2_8 import *


cuttingShapeMargin = 0.9
cleanFaceMargin = 0.9
bevelOffset = 0.01


def knifeProject(surfaceToCut, surfaceCuter, position, tbnMatrix):
    
    bpy.ops.object.mode_set(mode = 'OBJECT')
    
    # Check arguments.
    if surfaceToCut == None or surfaceCuter == None:
        print("knife project tentative with wrong arguments:")
        print("surfaceToCut = " + str(surfaceToCut))
        print("surfaceCuter = " + str(surfaceCuter))
        return
    
    # The TBN is converted to euler angles to circle around Blender's poor matrix update system.
    eulerFromTbn = tbnMatrix.to_euler()
    
    # Select only the surfaceCuter.
    bpy.ops.object.select_all(action='DESELECT')
    surfaceCuter.select_set(True)
    # Rotate it.
    bpy.ops.transform.rotate(value=eulerFromTbn.z, orient_axis='Z')
    bpy.ops.transform.rotate(value=eulerFromTbn.y, orient_axis='Y')
    bpy.ops.transform.rotate(value=eulerFromTbn.x, orient_axis='X')
    # And translate it.
    bpy.ops.transform.translate(value=(position[0], position[1], position[2]), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL')
    
    print("position = " + str(position))
    
    
    
    # Arrange selection for knife cutting operation.
    bpy.context.view_layer.objects.active = surfaceToCut
    surfaceCuter.select_set(True)
    
    direction = tbnMatrix[2]
    # Create override context for the cuts.
    override, originalRegion3D = createFaceOverrideContext(position, direction)

    # Force redraw the scene - this is considered unsavory but is necessary here.
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
    
    # The knife project has to be used in edit mode.
    bpy.ops.object.mode_set(mode = 'EDIT')

    # Knife cutting operation.
    bpy.ops.mesh.knife_project(override)
    
    # Go back to object mode. This forces an update of the mesh's internal data.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    
    # Reset the view to it's configuration before the knife project.
    setRegion3D(override, originalRegion3D)
    
    # Keep the selected face resulting from the knife project.
    resultingPolygon = None
    for currentPolygon in surfaceToCut.data.polygons:
        if currentPolygon.select:
            resultingPolygon = currentPolygon
    
    return resultingPolygon


# Adds a crease in a surface, to simulate metal plates joining.
# For this function the edge to crease (resulting from the cut) must be selected.
def addCutCrease(surfaceToCrease):
    bpy.context.view_layer.objects.active = surfaceToCrease
    bpy.ops.object.mode_set(mode = 'EDIT')
    
    # Set the edges of the crease as sharp.
    bpy.ops.mesh.mark_sharp()
    
    # Create a bevel, the cutting shape of only one segment becomes 3 segments after this.
    bpy.ops.mesh.bevel(offset_type='OFFSET', offset=bevelOffset, offset_pct=0, segments=2, profile=0.5, vertex_only=False, clamp_overlap=True, loop_slide=True, mark_seam=False, mark_sharp=True)
    
    
    # Select less to only keep the middle segment selected.
    bpy.ops.mesh.select_less()
    
    # Set the bottom of the crease as sharp.
    bpy.ops.mesh.mark_sharp()
    
    # Lower the middle segment to create the crease.
    bpy.ops.transform.shrink_fatten(value=bevelOffset)

    
    # Go back to object mode.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    
    
# Computes the borders of a given face in world space. This includes the size of the parent object.
# The face should be a rectangular quad.
# Warning: This algorithm assumes that the face is axis aligned.
def rectangleBorders(parentObject, face, tbnMatrix):
    minX = sys.float_info.max
    maxX = -sys.float_info.max
    minY = sys.float_info.max
    maxY = -sys.float_info.max
    
    for currentVertexIndex in face.vertices:
        currentVertex = face.id_data.vertices[currentVertexIndex]
        currentWorldCoords = parentObject.matrix_world @ currentVertex.co
        currentTangentCoords = tbnMatrix @ currentWorldCoords
        if currentTangentCoords.x < minX:
            minX = currentTangentCoords.x
        if currentTangentCoords.x > maxX:
            maxX = currentTangentCoords.x
        if currentTangentCoords.y < minY:
            minY = currentTangentCoords.y
        if currentTangentCoords.y > maxY:
            maxY = currentTangentCoords.y
    
    return (minX, maxX, minY, maxY)


# Basicaly computes the mean position of the rectangles 4 vertices to return it's center.
def rectangleCenter(parentObject, face):
    meanPosition = mathutils.Vector((0,0,0))
    for currentVertexIndex in face.vertices:
        # Find the vertex.
        currentVertex = face.id_data.vertices[currentVertexIndex]
        # Compute it's world coordinates.
        currentWorldCoords = parentObject.matrix_world @ currentVertex.co
        # Add it to the mean position.
        meanPosition = meanPosition + currentWorldCoords
        
    return meanPosition / len(face.vertices)
        
        
    

# Cuts a random shape in a surface, then gives it a crease to make it look like a plate.
def cutPlate(seed, objectToCut, cuttingShape, position, tbnMatrix):
    
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    random.seed(seed)

    # Use the cutting shape to cut the currently selected surface.
    resultingFace = knifeProject(objectToCut, cuttingShape, position, tbnMatrix)

    # Delete the no longer needed cutting shape.
    dataToRemove = cuttingShape.data
    bpy.data.objects.remove(cuttingShape)
    bpy.data.meshes.remove(dataToRemove)
    
    # Crease the cut surface.
    addCutCrease(objectToCut)

    return resultingFace


# The responsibility of this function is to generate the cutting shape and place it correctly for the cutPlate function.
# It should then cut the inner rectangle to enable recursivity.
def genericCutPlate(seed, objectToCut, faceTuple):
    
    print()
    print("new cut")
    
    if objectToCut == None:
        print("Tried to cut a None object")
        return
        
    if faceTuple == None:
        print("Tried to cut a None tuple")
        return
    
    # Find the face in the object.
    faceToCut = objectToCut.data.polygons[faceTuple[0]]
    
    if faceToCut == None:
        print("Tried to cut a None face")
        return
    
    # Compute normal and tangent of the current face to cut.
    normalForCuts = faceToCut.normal.copy()
    tangentForCuts = faceTangent(objectToCut, faceToCut.index).copy()
    # Compute the biTangent thanks to the normal and tangent.
    biTangentForCuts = normalForCuts.cross(tangentForCuts)
    
    # Assemble a TBN matrix from the normal, tangent and bitangent of the face.
    tbnMatrix = mathutils.Matrix([
    [tangentForCuts.x,      tangentForCuts.y,   tangentForCuts.z    ],
    [biTangentForCuts.x,    biTangentForCuts.y, biTangentForCuts.z  ],
    [normalForCuts.x,       normalForCuts.y,    normalForCuts.z     ]
    ])
    
    # Initialize the random seed, this is important in order to generate exactly the same content for a given seed.
    random.seed(seed)
    
    # Compute the dimension of the face to cut.
    rectBorders = rectangleBorders(objectToCut, faceToCut, tbnMatrix)
    
    # Compute the center of the face to cut.
    faceCenter = rectangleCenter(objectToCut, faceToCut)
    print("faceCenter = " + str(faceCenter))
    
    rectDimension = (rectBorders[0], rectBorders[1], rectBorders[2], rectBorders[3])
    
    faceWidth   = rectDimension[1] - rectDimension[0]
    faceHeight  = rectDimension[3] - rectDimension[2]

    # Generate a cutting shape
    cuttingShapeDimension = (faceWidth * 0.5, faceHeight * 0.5)
    cuttingShape, edgesDepth = generateRectangleCuttingShape(seed=random.randint(0, 100000), position=(0,0,0), dimension=cuttingShapeDimension, recursionDepth=0)
    
    cuttingShapeOuterBounds =  [-cuttingShapeDimension[0]/2 - max(0, edgesDepth[0]), # left
                                -cuttingShapeDimension[1]/2 - max(0, edgesDepth[1]), # bottom
                                cuttingShapeDimension[0]/2 + max(0, edgesDepth[2]), # right
                                cuttingShapeDimension[1]/2 + max(0, edgesDepth[3])] # up

    cuttingShapeOuterBoundsDimension = (cuttingShapeOuterBounds[2] - cuttingShapeOuterBounds[0], # width
                                        cuttingShapeOuterBounds[3] - cuttingShapeOuterBounds[1]) # height
                                        
    cuttingShapeOuterBoundsOffset = ((  cuttingShapeOuterBounds[2] + cuttingShapeOuterBounds[0]) * 0.5, # width
                                    (   cuttingShapeOuterBounds[3] + cuttingShapeOuterBounds[1]) * 0.5) # height
    
    
    # Translation.
    cuttingShapeTranslation = (-cuttingShapeOuterBoundsOffset[0], -cuttingShapeOuterBoundsOffset[1], 0)
    bpy.ops.transform.translate(value=cuttingShapeTranslation, orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL')
    
    
    # The cutting shape should be re-scaled and translated to fit the face to cut.
    # Scale it uniformly however.
    cuttingShapeScaleFactor = cuttingShapeMargin * min(faceWidth / cuttingShapeOuterBoundsDimension[0], faceHeight / cuttingShapeOuterBoundsDimension[1])
    # Correct center for the resize.
    bpy.context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
    # Resize operation.
    bpy.ops.transform.resize(value=(cuttingShapeScaleFactor, cuttingShapeScaleFactor, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL')

    
    # After this rescale, the inner bounds can be calculated
    # Compute the inner bounds of the cutting shape.
    cuttingShapeInnerBounds =  [(-cuttingShapeDimension[0]/2 - min(0, edgesDepth[0])) * cuttingShapeScaleFactor + cuttingShapeTranslation[0], # left
                                (-cuttingShapeDimension[1]/2 - min(0, edgesDepth[1])) * cuttingShapeScaleFactor + cuttingShapeTranslation[1] , # bottom
                                ( cuttingShapeDimension[0]/2 + min(0, edgesDepth[2])) * cuttingShapeScaleFactor + cuttingShapeTranslation[0], # right
                                ( cuttingShapeDimension[1]/2 + min(0, edgesDepth[3])) * cuttingShapeScaleFactor + cuttingShapeTranslation[1]] # up

    cuttingShapeInnerBoundsDimension = (cuttingShapeInnerBounds[2] - cuttingShapeInnerBounds[0], # width
                                        cuttingShapeInnerBounds[3] - cuttingShapeInnerBounds[1]) # height
                                        
    cuttingShapeInnerBoundsOffset = (   (cuttingShapeInnerBounds[2] + cuttingShapeInnerBounds[0]) * 0.5, # width
                                        (cuttingShapeInnerBounds[3] + cuttingShapeInnerBounds[1]) * 0.5) # height
    
    # Cut the plate with the tech-ish shape.
    resultingFace = cutPlate(seed, objectToCut, cuttingShape, faceCenter, tbnMatrix)
    
    ## Cut the surface again to have a clean surface to work with for recursivity.
    # Generate a plane cutting shape.
    bpy.ops.mesh.primitive_plane_add(size=1, align='WORLD', enter_editmode=True, location=((0,0,0)))
    # Re-scale to fit the plane in the inner bounds.
    cleanFaceScale = (cuttingShapeInnerBoundsDimension[0] * cleanFaceMargin, cuttingShapeInnerBoundsDimension[1] * cleanFaceMargin, 1)
    bpy.ops.transform.resize(value=cleanFaceScale, orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL')
    # Translate to fit the plane in the inner bounds.
    cleanFaceTranslation = (cuttingShapeInnerBoundsOffset[0], cuttingShapeInnerBoundsOffset[1], 0)
    bpy.ops.transform.translate(value=cleanFaceTranslation, orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL')

    # Go back to object mode.
    bpy.ops.object.mode_set(mode = 'OBJECT')
 
    # The cutting plane is the active object.
    cuttingShape = bpy.context.active_object
    
    # Use the cutting shape to cut the currently selected surface.
    resultingFace = knifeProject(objectToCut, cuttingShape, faceCenter, tbnMatrix)
    
    # Delete the no longer needed cutting shape.
    dataToRemove = cuttingShape.data
    bpy.data.objects.remove(cuttingShape)
    bpy.data.meshes.remove(dataToRemove)
    
    
    if not resultingFace == None:
        resultingFaceTuple = buildFaceTuple(objectToCut, resultingFace.index)
        return resultingFaceTuple


# Test function
if __name__ == "__main__":
    
    # Delete everything in the scene.
    bpy.ops.object.mode_set(mode = 'OBJECT')
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=False, confirm=False)

    # Create a new plane.
    bpy.ops.mesh.primitive_plane_add(align='WORLD', enter_editmode=True, location=(0, 0, 0))
#    bpy.ops.transform.resize(value=(1, 2.0, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, False, False), mirror=True, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1)
    
    bpy.ops.object.mode_set(mode = 'OBJECT')

    
    # Keep track of the selected object.
    originalySelectedObject = bpy.context.active_object

    # Cut a plate in the selected object.
    resultingFaceTuple = genericCutPlate(datetime.now(), originalySelectedObject, buildFaceTuple(originalySelectedObject, originalySelectedObject.data.polygons[0].index))
    resultingFaceTuple = genericCutPlate(datetime.now(), originalySelectedObject, resultingFaceTuple)
    resultingFaceTuple = genericCutPlate(datetime.now(), originalySelectedObject, resultingFaceTuple)
    resultingFaceTuple = genericCutPlate(datetime.now(), originalySelectedObject, resultingFaceTuple)
#    resultingFaceTuple = genericCutPlate(0, originalySelectedObject, resultingFaceTuple)
#    resultingFaceTuple = genericCutPlate(0, originalySelectedObject, resultingFaceTuple)
    bpy.ops.object.mode_set(mode = 'EDIT')
#datetime.now()
