Skip to content

Commit 7a6c840

Browse files
committed
Shifter: Guide Tools: adding initial code #588
1 parent fd1095c commit 7a6c840

1 file changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import mgear.pymaya as pm
2+
from mgear.core import icon
3+
4+
5+
def get_comp_root_by_family(guide_loc=None, comp_family="chain"):
6+
"""Return the component root locator name if is part of the family.
7+
8+
9+
Args:
10+
guide_loc (pm.PyNode or str, optional): Any locator in the
11+
component. If None, the current selection is used.
12+
13+
Returns:
14+
str or None: Name of the component root locator if it is a
15+
chain component. None otherwise.
16+
"""
17+
if not guide_loc:
18+
o_sel = pm.selected()
19+
if o_sel:
20+
guide_loc = o_sel[0]
21+
22+
if not guide_loc:
23+
print("Nothing selected. Please select a guide locator.")
24+
return
25+
26+
if not pm.attributeQuery("isGearGuide", node=guide_loc, ex=True):
27+
print("Selected is not a locator of a guide")
28+
return
29+
30+
comp_type = False
31+
comp_root = False
32+
33+
while guide_loc:
34+
if pm.attributeQuery("comp_type", node=guide_loc, ex=True):
35+
comp_type = guide_loc.attr("comp_type").get()
36+
if comp_family in comp_type:
37+
comp_root = guide_loc
38+
break
39+
else:
40+
print(f"Component is not of the family: {comp_family}")
41+
return
42+
elif pm.attributeQuery("ismodel", node=guide_loc, ex=True):
43+
print(f"Guide root selected, not a Component of family type: {comp_family}")
44+
return
45+
46+
guide_loc = guide_loc.getParent()
47+
if not guide_loc:
48+
break
49+
pm.select(guide_loc)
50+
51+
if not comp_root:
52+
return
53+
54+
return comp_root.name()
55+
56+
57+
def _rebuild_display_curve(base_name, centers):
58+
"""Rebuild the chain connection curve keeping alwaysDrawOnTop input.
59+
60+
Args:
61+
base_name (str): Base name of the chain (e.g. 'chain_C0').
62+
centers (list[pm.PyNode]): Ordered nodes for the curve centers.
63+
64+
Returns:
65+
pm.PyNode or None: New curve node or None if not created.
66+
"""
67+
curve_name = "{0}_crv".format(base_name)
68+
old_curves = pm.ls(curve_name)
69+
70+
alw_on_top_attr = None
71+
if old_curves:
72+
always_attr = old_curves[0].attr("alwaysDrawOnTop")
73+
conns = always_attr.connections(p=True)
74+
if conns:
75+
alw_on_top_attr = conns[0]
76+
pm.delete(old_curves)
77+
78+
if len(centers) < 2:
79+
# Not enough points to build a curve
80+
return None
81+
82+
new_crv = icon.connection_display_curve(curve_name, centers)
83+
84+
if alw_on_top_attr:
85+
pm.connectAttr(alw_on_top_attr, new_crv.attr("alwaysDrawOnTop"))
86+
87+
return new_crv
88+
89+
#############################
90+
# Chain tools functions
91+
#############################
92+
93+
94+
def _get_chain_index(node):
95+
"""Return numeric chain index from a locator name.
96+
97+
Expected pattern: <prefix>_<index>_loc
98+
99+
Args:
100+
node (pm.PyNode): Locator node.
101+
102+
Returns:
103+
int: Parsed index or -1 if not found.
104+
"""
105+
name = node.name()
106+
parts = name.split("_")
107+
if len(parts) < 3:
108+
return -1
109+
try:
110+
return int(parts[-2])
111+
except ValueError:
112+
return -1
113+
114+
115+
def add_chain_locator(guide_loc=None):
116+
"""Extend an mGear chain component by adding a new locator.
117+
118+
Args:
119+
guide_loc (pm.PyNode or str, optional): Any locator belonging
120+
to the chain guide. If None, the current selection is used.
121+
122+
Returns:
123+
pm.PyNode or None: The new locator, or None on failure.
124+
"""
125+
# Get component root and ensure it is a CHAIN component
126+
comp_root_name = get_comp_root_by_family(guide_loc)
127+
if not comp_root_name:
128+
return
129+
130+
comp_root = pm.PyNode(comp_root_name)
131+
132+
# Base name from root: "chain_C0_root" -> "chain_C0"
133+
root_name = comp_root.name()
134+
if not root_name.endswith("_root"):
135+
print(
136+
"Component root name does not end with '_root': {0}".format(
137+
root_name
138+
)
139+
)
140+
return
141+
142+
base_name = root_name[:-5] # strip "_root"
143+
144+
# Collect chain locators: "base_index_loc"
145+
loc_pattern = "{0}_*_loc".format(base_name)
146+
all_locs = pm.ls(loc_pattern, type="transform") or []
147+
148+
# Filter only those with a valid numeric index and sort by index
149+
chain_locs = [n for n in all_locs if _get_chain_index(n) >= 0]
150+
if not chain_locs:
151+
print(
152+
"No chain locators found for base '{0}'".format(base_name)
153+
)
154+
return
155+
156+
chain_locs = sorted(chain_locs, key=_get_chain_index)
157+
158+
# Build ordered list from root to last locator
159+
ordered_nodes = [comp_root] + chain_locs
160+
161+
if len(ordered_nodes) <= 2:
162+
print("Not enough nodes to extend the chain.")
163+
return
164+
165+
last_loc = ordered_nodes[-1]
166+
prev_loc = ordered_nodes[-2]
167+
168+
# Compute vector from prev -> last in world space
169+
prev_pos = prev_loc.getTranslation(space="world")
170+
last_pos = last_loc.getTranslation(space="world")
171+
delta = last_pos - prev_pos
172+
new_pos = last_pos + delta
173+
174+
# Determine new index and name
175+
last_index = _get_chain_index(last_loc)
176+
if last_index < 0:
177+
new_index = len(chain_locs)
178+
else:
179+
new_index = last_index + 1
180+
181+
new_name = "{0}_{1}_loc".format(base_name, new_index)
182+
183+
# Duplicate last locator, parent under last, move to extrapolated pos
184+
new_loc = pm.duplicate(last_loc, n=new_name)[0]
185+
pm.parent(new_loc, last_loc)
186+
new_loc.setTranslation(new_pos, space="world")
187+
188+
# Rebuild the connection curve: <base>_crv
189+
centers = ordered_nodes + [new_loc]
190+
_rebuild_display_curve(base_name, centers)
191+
192+
pm.select(new_loc)
193+
print("Added new chain locator: {0}".format(new_loc.name()))
194+
195+
return new_loc
196+
197+
198+
def delete_chain_locator(guide_loc=None):
199+
"""Remove the last locator of an mGear chain component.
200+
201+
Args:
202+
guide_loc (pm.PyNode or str, optional): Any locator belonging
203+
to the chain guide. If None, the current selection is used.
204+
205+
Returns:
206+
pm.PyNode or None: The new last locator after deletion, or
207+
None on failure.
208+
"""
209+
# Get component root and ensure it is a CHAIN component
210+
comp_root_name = get_comp_root_by_family(guide_loc)
211+
if not comp_root_name:
212+
return
213+
214+
comp_root = pm.PyNode(comp_root_name)
215+
216+
# Base name from root: "chain_C0_root" -> "chain_C0"
217+
root_name = comp_root.name()
218+
if not root_name.endswith("_root"):
219+
print(
220+
"Component root name does not end with '_root': {0}".format(
221+
root_name
222+
)
223+
)
224+
return
225+
226+
base_name = root_name[:-5] # strip "_root"
227+
228+
# Collect chain locators: "base_index_loc"
229+
loc_pattern = "{0}_*_loc".format(base_name)
230+
all_locs = pm.ls(loc_pattern, type="transform") or []
231+
232+
# Filter only those with a valid numeric index and sort by index
233+
chain_locs = [n for n in all_locs if _get_chain_index(n) >= 0]
234+
if not chain_locs:
235+
print(
236+
"No chain locators found for base '{0}'".format(base_name)
237+
)
238+
return
239+
240+
chain_locs = sorted(chain_locs, key=_get_chain_index)
241+
242+
# Build ordered list from root to last locator
243+
ordered_nodes = [comp_root] + chain_locs
244+
245+
if len(ordered_nodes) < 2:
246+
print("Not enough nodes in the chain to delete.")
247+
return
248+
249+
# Last node must be a locator, not the root
250+
last_loc = ordered_nodes[-1]
251+
prev_loc = ordered_nodes[-2]
252+
253+
if last_loc == comp_root:
254+
print("Last element is the component root. Nothing to delete.")
255+
return
256+
257+
# Delete the last locator
258+
pm.delete(last_loc)
259+
260+
# Remaining centers for the curve (from root to new last element)
261+
remaining_chain_locs = chain_locs[:-1]
262+
centers = [comp_root] + remaining_chain_locs
263+
264+
# Rebuild the connection curve: <base>_crv
265+
_rebuild_display_curve(base_name, centers)
266+
267+
# If there are remaining locators, select the new last one
268+
if remaining_chain_locs:
269+
new_last = remaining_chain_locs[-1]
270+
pm.select(new_last)
271+
print(
272+
"Deleted last chain locator. New last locator: {0}".format(
273+
new_last.name()
274+
)
275+
)
276+
return new_last
277+
278+
# Only the root remains
279+
pm.select(comp_root)
280+
print(
281+
"Deleted last chain locator. Only component root remains: {0}"
282+
.format(comp_root.name())
283+
)
284+
return comp_root
285+
286+
287+
# Snippets:
288+
# add_chain_locator()
289+
# delete_chain_locator()

0 commit comments

Comments
 (0)