Architecture

Architecture python
src/
├── main.py                         # CLI entry: imports dg_editor.show()

├── dg_editor/                      # Core package namespace
│   ├── __init__.py                 # Package entry: exposes ui__main_window as show()
│   ├── config.py                   # VERSION, PATH, DEBUG flag
│   ├── reloader.py                 # Hot-reload orchestrator (DEBUG mode)
│   │
│   ├── ui__main_window.py          # UI:    QTabWidget container, Maya parent wrapping
│   │
│   ├── ui_nodes.py                 # UI:    Node tab: WidgetCreate, WidgetDelete
│   ├── nodes_create.py             # Logic: DSL lexer/parser (name:type syntax)
│   ├── ui_nodes_create_dialog.py   # UI:    Template UI (spinbox, linedit → expression)
│   ├── nodes_create_dialog.py      # Logic: Template processor (placeholder → names)
│   ├── ui_nodes_delete_dialog.py   # UI:    Regex input dialog
│   ├── nodes_delete_dialog.py      # Logic: Scene-wide regex matcher
│   │
│   ├── ui_connect.py               # UI:    Connection tab: from/to attribute matchers
│   ├── ui_connect_dialog.py        # UI:    Regex dialog for attributes
│   ├── connect_dialog.py           # Logic: Attribute-level regex filtering
│   │
│   ├── ui_rename.py                # UI:    Rename tab: prefix/search-replace widgets
│   ├── rename.py                   # Logic: UID-based rename logic (hierarchy-aware)
│   │
│   ├── ui_settings.py              # UI:    Settings tab: undo toggle, font size spinbox
│   ├── settings.py                 # Logic: JSON persistence: bind_lineedit, bind_spinbox
│   │
│   ├── widgets/                    # Reusable UI component library
│   │   ├── __init__.py             # Exposes BaseWidget, BaseDialog
│   │   └── base.py                 # Abstract base classes with layout builders
│   │
│   └── utils.py                    # undo_block decorator

├── build.py                        # Build script: copies src/ to build/dg_editor_x.x.x/
└── install.mel                     # MEL installer: drag-drop shelf button creation

UI modules (ui_*.py) instantiate widgets and handle Qt signals;
Logic modules (*.py without ui_ prefix) contain backend Maya cmds operations and algorithms of the corresponding UI.

Function list

Node Tab

Connection Tab

Rename Tab

Settings Tab

Use Cases

Case 1: IK/FK Spine Rig Setup

Context

Building FK joint chain driven by IK control curves.

IK controls drive FK joints through worldMatrixoffsetParentMatrix connections.

Target Outliner Structure

Outliner python
spine_rig_grp
├── spine_FK_jnt_grp
│   ├── spine_FK_0_jnt
│   ├── spine_FK_1_jnt
│   └── ... (spine_FK_19_jnt)
├── spine_IK_ctrl_grp
│   ├── spine_IK_0_ctrl
│   ├── spine_IK_1_ctrl
│   └── ... (spine_IK_19_ctrl)
└── spine_extras_grp (guides, measurement curves)
# Controls drive joints via worldMatrix connections

Workflow

dg final 1
Create Joints
dg final 2
Create Control Curves
dg final 3
Rename Transforms
dg final 4
Connect Matrix Attribute
Detailed Steps
  1. Generate FK Joint Chain
dg 1 1a
dg 1 1b
dg 1 1c
  1. Position FK Joints (Manual)

(Orient joints along spine path)

  1. Generate IK Control Curves
dg 1 2a v2
dg 1 2b v2
dg 1 2c
  1. Rename Created Control Curves

(The name curve_{id}_shape is applied to the hidden internal Shape node,
and Maya wraps it in an default-named curve{id} Transform node.)

dg 1 3b v2
dg 1 3a
dg 1 3c
  1. Connect via World Matrix
dg 1 4a
dg 1 4b
dg 1 4c
  1. Group Hierarchies (Manual)
dg 1 5a
  • Select all FK joints → Ctrl+G → rename spine_FK_jnt_grp
  • Select all IK controls → Ctrl+G → rename spine_IK_ctrl_grp
  • Select both groups → Ctrl+G → rename spine_rig_grp

Update History

v0.1.1 - Matrix Connect and Snap Operations

Extended connection operations with matrix workflow and transform snapping.

Matrix Connect executes worldMatrix to offsetParentMatrix connections.

Snap handles batch transform matching through regex patterns.

Operation Selector

ui_connect.py python
class WidgetFuncSelect(BaseWidget):
  Connect, Disconnect, MatrixConnect, Snap = range(4)

  def __init__(self, parent=None):
      super(WidgetFuncSelect, self).__init__(parent)

      self.func_combo = QComboBox()
      self.func_combo.addItem("Connect")
      self.func_combo.addItem("Matrix Connect")
      self.func_combo.addItem("Disconnect")
      self.func_combo.addItem("Snap")

Added two new operation types to existing operations.

Execution Branches

ui_connect.py python
elif func_type == WidgetFuncSelect.MatrixConnect:
  cmds.connectAttr(
      "{}.worldMatrix[0]".format(src_node),
      "{}.offsetParentMatrix".format(dst_node),
      force=True
  )
elif func_type == WidgetFuncSelect.Snap:
  cmds.matchTransform(dst_node, src_node, pos=True, rot=True)

Matrix extracts node names from attribute strings for worldMatrix access.

Snap excludes scale—rigging workflows don’t need it usually.

Both use force=True to overwrite existing data.

Disconnect Fallback

ui_connect.py python
elif func_type == WidgetFuncSelect.Disconnect:
  try:
      cmds.disconnectAttr(o, i)
  except:
      try:
          cmds.disconnectAttr(
              "{}.worldMatrix[0]".format(src_node),
              "{}.offsetParentMatrix".format(dst_node)
          )
          cmds.setAttr("{}.offsetParentMatrix".format(dst_node),
                       [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1], type="matrix")
      except:
          cmds.warning("Disconnection failed")

Disconnect fallback tries direct attribute first, then matrix if that fails.
Identity matrix reset needed—otherwise target keeps last transform value after disconnect.

Key Features & Code Snippets

Custom DSL Parser for Node Creation

Interprets name:type expressions for batch node creation. It uses a two-stage system: Lexer tokenizes the input string, parser validates grammar against expected patterns before any Maya commands execute.

Lexer

nodes_create.py python
name_match = re.compile(r'[a-zA-Z0-9_]+')
colon_match = re.compile(r':')
linefeed_match = re.compile(r'(\r\n|\n)+')

# Lexer: character stream → token stream
def _lex(exp):
  while len(exp) > 0:
      # space skipping
      m = space_match.match(exp)
      if m: exp = exp[m.end():]

      # name handling
      m = name_match.match(exp)
      if m:
          yield TokenName(exp[m.start():m.end()])
          exp = exp[m.end():]
          continue
      # ... colon and linefeed handling

The lexical analyzer tokenizes input using pre-compiled regex patterns.

It yields tokens as they’re matched—no need to build the full token list.

Parser

nodes_create.py python
# Parser: token stream → (name, type) pairs
def parser(exp):
  tokens = list(_lex(exp))
  while tokens:
      if (isinstance(tokens[0], TokenName) and 
          isinstance(tokens[1], TokenColon) and 
          isinstance(tokens[2], TokenName)):
          yield tokens[0].text, tokens[2].text
          tokens = tokens[3:]
      else:
          raise NodeCreateExpExc('syntax error')

The parser validates grammar through pattern matching on token streams.
It converts tokens to (name, type) pairs.

Template String Processor with Variable Interpolation

Generates numbered node sequences from placeholder expressions like ctrl_{id}_geo.

Each template gets lexed into tokens, then interpolated against a context dictionary per iteration.

Interpolation

nodes_create_dialog.py python
value_match = re.compile(r'\{([a-zA-Z0-9_]+)\}')

def parse(exp, values):
  """
  exp: "ctrl_{id}_geo"
  values: [{"id": 0}, {"id": 1}, {"id": 2}]
  yields: "ctrl_0_geo", "ctrl_1_geo", "ctrl_2_geo"
  """
  tokens = list(_lex(exp))
  for kv in values:
      name = ''
      for t in tokens:
          if isinstance(t, TokenName):
              name += t.text
          else:
              v = kv.get(t.text)
              if v is None:
                  raise CreateDialogParserExc("Key not found")
              name += str(v)
      yield name

Regex capture group extracts placeholder names, dictionary lookup resolves values.
The generator-based lazy evaluation yields results incrementally instead of building full lists.
It means memory stays low—each name gets yielded and consumed before the next one’s built.

Hierarchical Selection with UID Tracking

Converts selections to Maya UIDs before rename operations.

As string paths break mid-batch when names change, UIDs don’t—they’re persistent identifiers even hierarchy changes.

UID Collection

rename.py python
def _select_uids():
  sel = cmds.ls(sl=True, long=True)
  if not sel: return []

  # Flatten: selected nodes + all descendants
  nodes = [
      n for s in sel
      for all_nodes in ([s], cmds.listRelatives(s, ad=True, pa=True) or [])
      for n in all_nodes
  ]
  uids = cmds.ls(nodes, uid=True)
  return list(set(uids))  # Remove duplicates

List comprehension flattens selection plus descendants into single list.
list(set()) at the end removes duplicates—handles case where user selects both parent and child simultaneously.
The ad=True flag gets all descendants recursively, pa=True returns full paths to avoid name ambiguity.

Hot-Reload Development Environment

Module reload system that triggers on import when DEBUG flag is active.

Eliminates Maya restarts between code changes during development.

reloader.py python
if config.DEBUG:
  print("--- Reloading DG Editor Modules ---")
  for m in modules:
      try:
          importlib.reload(m)
      except Exception as e:
          print(f"Failed to reload {m}: {e}")

this cuts iteration time from ~2 minutes to ~10 seconds.
(edit code → close tool → restart Maya → re-run tool to edit code → re-run tool)

Atomic Undo Decorator

Wraps operations in Maya’s undo chunk API.

Everything inside the decorated function becomes a single undo step.

Decorator

utils.py python
def undo_block(fn):
  @functools.wraps(fn)
  def wrapper(*args, **kwargs):
      enable_undo = settings.get_undo_check()
      if enable_undo:
          cmds.undoInfo(ock=True)
      try:
          return fn(*args, **kwargs)
      finally:
          cmds.undoInfo(cck=True)
  return wrapper

The @functools.wraps preserves original function metadata.

The undo chunk is opened before execution and closed in finally block so chunk closes even if operation raises — this is crucial, as any error that stops it from closing will leave Maya’s undo queue in a bad state.

Usage

ui_nodes.py python
@undo_block
def delete_node(self):
  node_names = self.name_text.toPlainText().splitlines()
  cmds.delete(node_names)

Decorator application.
So delete 500 nodes, they all collapse into one undo step. Hit Ctrl+Z once, all 500 restore.

Persistent Settings Backend

JSON storage system that persists UI state between Maya sessions.

Core layer handles get/set operations, binding functions connect Qt widgets to storage keys with auto-save behavior.

JSON Access

settings.py python
JSON_PATH = os.path.join(config.PATH, "settings.json")

def _get_json():
  if not os.path.isfile(JSON_PATH):
      _save_json(dict())
  with codecs.open(JSON_PATH, "r", encoding="utf-8") as f:
      return json.load(f)

def _save_json(data):
  with codecs.open(JSON_PATH, "w", encoding="utf-8") as f:
      json.dump(data, f)

def _set(key, val):
  data = _get_json()
  data[key] = val
  _save_json(data)

def _get(key, default):
  data = _get_json()
  return data.get(key, default)

File creation on first access if missing. Each set operation does full read-modify-write cycle — not optimized for high-frequency updates but fine for UI widget changes.

Widget Binding

settings.py python
def bind_lineedit(widget, key, default=""):
  current_val = _get(key, default)
  widget.setText(str(current_val))
  widget.editingFinished.connect(lambda: _set(key, widget.text()))

def bind_spinbox(widget, key, default=10):
  current_val = _get(key, default)
  try:
      widget.setValue(float(current_val))
  except ValueError:
      widget.setValue(default)
  widget.valueChanged.connect(lambda val: _set(key, val))

def bind_checkbox(widget, key, default=True):
  # ... similar pattern

def bind_combobox(widget, key, default=None):
  # ... similar pattern

Binding functions follow same pattern—load current value on init, connect signal to auto-save.
editingFinished for line edits triggers on focus loss, not per keystroke.
valueChanged for spinbox fires immediately on change.