__commandName__ = 'SplitPatch' __commandDisplayName__ = 'Split patch' # version history # 0.1 16-Mar-2014 1. Original alpha release # 0.2 29-Mar-2014 1. Removed search for verts' bounding box, and replaced with # a simpler check on which verts have moved after a keyboard- # -command nudge. # 2. Fix misreported error when user selects two verts only at # opposite edges of patch seam of 3D patch. Was being misdetected # as the seam of a 3d patch. # 3. Added new UserError for when user selects a FS entity instead of # the component patch. Script can't select the patch itself # without clearing the selection. def execute(): TOLERANCE = 0.1 # only to cover floating-point errors. Set smaller than min grid size class UserError(Exception): pass class Vert: """Holds coords for one patch control vertex.""" def __init__(self, vertex, texcoord): self.vertex = Vector3(vertex) self.texcoord = Vector2(texcoord) class Verts: """A 2d matrix (row/col) implemented as a list where each element is a list of Vert objects in one row of the patch.""" def __init__(self, patch=None): self.verts = [] if patch: for rownum in range(patch.getHeight()): row = [] for colnum in range(patch.getWidth()): v = Vert(patch.ctrlAt(rownum, colnum).vertex, patch.ctrlAt(rownum, colnum).texcoord) row.append(v) self.verts.append(row) class PatchData: """Store all of the information needed to reconstruct a patch exactly.""" def __init__(self, patch): self.cols = patch.getWidth() # int self.rows = patch.getHeight() # int self.verts = Verts(patch) self.shader = patch.getShader() # string self.subdivisionsFixed = patch.subdivionsFixed() # bool self.subdivs = patch.getSubdivisions() # Subdivisions def replaceverts(self, verts): self.verts = verts self.rows = len(verts.verts) self.cols = len(verts.verts[0]) def getRows(self, first=0, last=None): """Return a Verts object holding the subset of the patch mesh verts in the specified row range.""" if not last: last = self.rows result = Verts() result.verts = self.verts.verts[first:last] return result def getCols(self, first=0, last=None): """Return a Verts object holding the subset of the patch mesh verts in the specified column range.""" if not last: last = self.cols result = Verts() result.verts = [r[first:last] for r in self.verts.verts] return result def getVertex(self, row, col): return self.verts.verts[row][col].vertex def getTexcoord(self, row, col): return self.verts.verts[row][col].texcoord # Utility functions def CheckFSPatch(node): """Check that user hasn't selected a FS patch wrongly.""" node = node.getEntity() class FSChecker(SceneNodeVisitor): def pre(self, child): if child.isPatch(): raise UserError("You've selected a func static entity " \ "containing a patch. The patch splitter " \ "can't deal with that. You need to " \ "select the patch itself (press TAB) " \ "before entering vertex editing mode.") return 0 node.traverse(FSChecker()) def getAndValidateSelection(): sInfo = GlobalSelectionSystem.getSelectionInfo() s = GlobalSelectionSystem.ultimateSelected() if s.isEntity(): CheckFSPatch(s) if sInfo.patchCount != 1 or sInfo.brushCount > 0 or sInfo.componentCount < 2: raise UserError('Bad selection. Select one patch only, and ' \ '2 or more verts from the same (pink) row or column.') patch = s.getPatch() if not patch.isValid(): raise UserError("This isn't a valid patch. It has some invalid " \ "vertices, or maybe all vertices are in the same spot.") return patch def resetPatch(patch, patchdata): """Apply patchdata to the patch, reconstructing it.""" p, pd = patch, patchdata p.setDims(pd.cols, pd.rows) p.setShader(pd.shader) p.setFixedSubdivisions(pd.subdivisionsFixed, pd.subdivs) for row in range(pd.rows): for col in range(pd.cols): p.ctrlAt(row, col).vertex = pd.getVertex(row, col) p.ctrlAt(row, col).texcoord = pd.getTexcoord(row, col) p.controlPointsChanged() def clonePatch(patch): """Clone the existing patch so the new one is automatically part of the same func_static and layer as the original. Return a reference to the new patch.""" patch.setSelected(False) # Clears vertex editing mode patch.setSelected(True) GlobalCommandSystem.execute('CloneSelection') return GlobalSelectionSystem.ultimateSelected().getPatch() def getSelectedVertsLine(patch, patchdata): """Return ('row', 4) or ('col', 2) etc. Selected verts can't be accessed directly, so move them 1 grid unit and work out from changed coords which row or col they sit on. Mutates patch, so uses patchdata to restore state.""" GlobalCommandSystem.execute('SelectNudgeleft') newdata = PatchData(patch) # Capture the displaced verts' coords resetPatch(patch, patchdata) # Then put them back # Moving pink verts will drag nearby green verts. # So sort the verts by the distance they have moved, and determine # the selected line from the maximally displaced verts only. distances = {} for row in range(patchdata.rows): for col in range(patchdata.cols): dist = ( patchdata.getVertex(row, col) - newdata.getVertex(row, col) ).getLength() distances[ (row, col) ] = dist maxDist = max(distances.values()) movers = [k for k in distances.keys() if distances[k] >= maxDist - TOLERANCE] includedRows = set(r[0] for r in movers) includedCols = set(r[1] for r in movers) # Interpret result # Special case: if the user selects the existing seam of a polyhedron like a sphere or cone: if len(includedRows) > 1 and len(includedCols) > 1 \ and (includedRows == set((0, patch.getHeight()-1)) or includedCols == set((0, patch.getWidth()-1))): raise UserError("You've selected the existing seam of a 3d patch. It's already cut there.") elif len(includedRows) > 1 and len(includedCols) > 1: raise UserError("You've selected verts from more than one line.") elif len(includedRows) == 1: lineType, lineNum = 'row', includedRows.pop() elif len(includedCols) == 1: lineType, lineNum = 'col', includedCols.pop() else: raise UserError("Unable to determine selected verts. This shouldn't happen. " \ "Try again with different verts from the same line.") if lineNum == 0 \ or (lineType == 'row' and lineNum == patch.getHeight()-1) \ or (lineType == 'col' and lineNum == patch.getWidth()-1): raise UserError("You've selected the existing edge of the patch. No action has been taken.") if lineNum % 2: raise UserError("You've selected a green line. Patches can be cut only along lines with some pink verts.") return lineType, lineNum # Execution starts here # STEP 1: Validate selection patch = getAndValidateSelection() patchdata = PatchData(patch) # STEP 2: Work out what row or column is selected lineType, lineNum = getSelectedVertsLine(patch, patchdata) print 'RESULT: ', lineType, lineNum # STEP 3: Split the patch newpatch = clonePatch(patch) try: if lineType == 'row': newverts1 = patchdata.getRows(last=lineNum+1) newverts2 = patchdata.getRows(first=lineNum) elif lineType == 'col': newverts1 = patchdata.getCols(last=lineNum+1) newverts2 = patchdata.getCols(first=lineNum) # Make PatchData objects for the 2 new patches. Initialize them with # either of the existing patches then swap out the verts list. newdata1, newdata2 = PatchData(newpatch), PatchData(newpatch) newdata1.replaceverts(newverts1) newdata2.replaceverts(newverts2) # Adjust the patches resetPatch(newpatch, newdata2) resetPatch(patch, newdata1) except Exception as e: # Clean up before reporting error GlobalSelectionSystem.setSelectedAll(False) newpatch.setSelected(True) GlobalCommandSystem.execute('deleteSelected') resetPatch(patch, patchdata) raise # Step 4: Success! leave the new patches selected patch.setSelected(True) newpatch.setSelected(True) if __executeCommand__: try: execute() except Exception as e: GlobalDialogManager.createMessageBox('Patch Splitter', str(e), Dialog.ERROR).run()