1 """Functions used by the CameraTracker node"""
2
3 import nuke
4 import nukescripts
5 import os
6
7 import camerapresets
8
9
10
11
12
14 """Populate the export menu on a CameraTracker node."""
15
16
17 entries = [
18
19 ( 'Camera', 'nukescripts.cameratracker.createCamera(nuke.thisNode())', "Create a Camera linked to the calculated projection."),
20 ( 'Camera rig', 'nukescripts.cameratracker.createCameraRig(nuke.thisNode())', "Create a multi-view rig with a Camera per view."),
21 ( 'Scene', 'nukescripts.cameratracker.createScene(nuke.thisNode())', "Create a Scene with a Camera and PointCloud for the camera solve."),
22 ( 'Scene+', 'nukescripts.cameratracker.createEverything(nuke.thisNode())', "Create a Scene with a Camera, PointCloud, ScanlineRender, and LensDistortion (Undistort) node."),
23 ( 'Point cloud', 'nukescripts.cameratracker.createPointCloud(nuke.thisNode())', "Create a PointCloud for the camera solve."),
24 ( 'Distortion', 'nukescripts.cameratracker.createLensDistortion(nuke.thisNode())',
25 "Create a LensDistortion node preconfigured for distortion using the same settings as the CameraTracker. " +
26 "You can use this node to distort your CG or other elements before comping them back over the input footage."),
27 ( 'Undistortion', 'nukescripts.cameratracker.createUndistortion(nuke.thisNode())',
28 "Create a LensDistortion node preconfigured for undistortion using the same settings as the CameraTracker. " +
29 "You can apply this node to your input footage to make it match CG elements rendered using the calculated camera."),
30 ( 'Cards', 'nukescripts.cameratracker.createCards(nuke.thisNode())', "Create a group of cards.")
31
32 ]
33
34 k = cameraTracker['exportMenu']
35 k.setValues([ "%s\t%s" % (script, label) for label, script, _ in entries ])
36
37
38 tooltipLines = [ "Create new camera and point cloud nodes based on the results of the solve. The available options are:", "" ]
39 for label, _, tooltip in entries:
40 tooltipLines.append("<b>%s:</b> %s" % (label, tooltip))
41
42 k.setTooltip(os.linesep.join(tooltipLines))
43
44
52
53
54
55
56
58 """Create a camera node based on the projection calculated by the solver."""
59 x = solver.xpos()
60 y = solver.ypos()
61 w = solver.screenWidth()
62 h = solver.screenHeight()
63 m = int(x + w/2)
64 numviews = len( nuke.views() )
65 link = False
66 linkKnob = solver.knob("linkOutput")
67 if linkKnob:
68 link = bool(linkKnob.getValue())
69
70 camera = nuke.createNode('Camera', '', False)
71 camera.setInput(0,None)
72 camera.setXYpos(m - int(camera.screenWidth()/2), y + w)
73 if link:
74 camera.knob("focal").setExpression(solver.name() + ".focalLength")
75 camera.knob("haperture").setExpression(solver.name() + ".aperture.x")
76 camera.knob("vaperture").setExpression(solver.name() + ".aperture.y")
77 camera.knob("translate").setExpression(solver.name() + ".camTranslate")
78 camera.knob("rotate").setExpression(solver.name() + ".camRotate")
79 camera.knob("win_translate").setExpression(solver.name() + ".windowTranslate")
80 camera.knob("win_scale").setExpression(solver.name() + ".windowScale")
81 else:
82 camera.knob("focal").fromScript(solver.knob("focalLength").toScript(False))
83 camera.knob("translate").fromScript(solver.knob("camTranslate").toScript(False))
84 camera.knob("rotate").fromScript(solver.knob("camRotate").toScript(False))
85 camera.knob("win_translate").fromScript(solver.knob("windowTranslate").toScript(False))
86 camera.knob("win_scale").fromScript(solver.knob("windowScale").toScript(False))
87 for i in xrange(numviews):
88 camera.knob("haperture").setValue(solver.knob("aperture").getValue(0,i+1),0,0,i+1)
89 camera.knob("vaperture").setValue(solver.knob("aperture").getValue(1,i+1),0,0,i+1)
90
91
93 """Create a multi-view rig with a camera per view."""
94 numviews = len( nuke.views() )
95 if numviews < 2:
96 nuke.message("Creating a camera rig requires multiple views.\n" +
97 "You can add additional views in your <em>Project Settings</em>, on the <em>Views</em> tab.")
98 return
99
100 x = solver.xpos()
101 y = solver.ypos()
102 w = solver.screenWidth()
103 h = solver.screenHeight()
104 m = int(x + w/2)
105 link = False
106 linkKnob = solver.knob("linkOutput")
107 if linkKnob:
108 link = bool(linkKnob.getValue())
109
110 join = nuke.nodes.JoinViews()
111 join.setInput(0,None)
112 join.setXYpos(m + int(w*1.5) + int(w * numviews/2), y + w)
113
114 for i in xrange(numviews):
115 viewStr = nuke.views()[i]
116 camera = nuke.nodes.Camera()
117 camera.setInput(0,None)
118 if link:
119 camera.knob("focal").setExpression(solver.name() + ".focalLength." + viewStr)
120 camera.knob("haperture").setExpression(solver.name() + ".aperture." + viewStr + ".x")
121 camera.knob("vaperture").setExpression(solver.name() + ".aperture." + viewStr + ".y")
122 camera.knob("translate").setExpression(solver.name() + ".camTranslate." + viewStr)
123 camera.knob("rotate").setExpression(solver.name() + ".camRotate." + viewStr)
124 camera.knob("win_translate").setExpression(solver.name() + ".windowTranslate." + viewStr)
125 camera.knob("win_scale").setExpression(solver.name() + ".windowScale." + viewStr)
126 else:
127 if solver.knob("focalLength").isAnimated():
128 camera.knob("focal").copyAnimation(0,solver.knob("focalLength").animation(0,i+1))
129 else:
130 camera.knob("focal").setValue(solver.knob("focalLength").getValue(0,i+1),0,0,i+1)
131 if solver.knob("camTranslate").isAnimated():
132 camera.knob("translate").copyAnimation(0,solver.knob("camTranslate").animation(0,i+1))
133 camera.knob("translate").copyAnimation(1,solver.knob("camTranslate").animation(1,i+1))
134 camera.knob("translate").copyAnimation(2,solver.knob("camTranslate").animation(2,i+1))
135 else:
136 camera.knob("translate").setValue(solver.knob("camTranslate").getValue(0,i+1),0,0,i+1)
137 camera.knob("translate").setValue(solver.knob("camTranslate").getValue(1,i+1),1,0,i+1)
138 camera.knob("translate").setValue(solver.knob("camTranslate").getValue(2,i+1),2,0,i+1)
139 if solver.knob("camRotate").isAnimated():
140 camera.knob("rotate").copyAnimation(0,solver.knob("camRotate").animation(0,i+1))
141 camera.knob("rotate").copyAnimation(1,solver.knob("camRotate").animation(1,i+1))
142 camera.knob("rotate").copyAnimation(2,solver.knob("camRotate").animation(2,i+1))
143 else:
144 camera.knob("rotate").setValue(solver.knob("camRotate").getValue(0,i+1),0,0,i+1)
145 camera.knob("rotate").setValue(solver.knob("camRotate").getValue(1,i+1),1,0,i+1)
146 camera.knob("rotate").setValue(solver.knob("camRotate").getValue(2,i+1),2,0,i+1)
147 camera.knob("win_translate").setValue(solver.knob("windowTranslate").getValue(0,i+1),0,0,i+1)
148 camera.knob("win_scale").setValue(solver.knob("windowScale").getValue(0,i+1),0,0,i+1)
149 camera.knob("haperture").setValue(solver.knob("aperture").getValue(0,i+1),0,0,i+1)
150 camera.knob("vaperture").setValue(solver.knob("aperture").getValue(1,i+1),0,0,i+1)
151 camera.setXYpos(m + 2*w + w*i, y)
152 if numviews==2:
153 if i==0:
154 camera.knob("gl_color").setValue(0xFF0000FF)
155 camera.knob("tile_color").setValue(0xFF0000FF)
156 if i==1:
157 camera.knob("gl_color").setValue(0x00FF00FF)
158 camera.knob("tile_color").setValue(0x00FF00FF)
159 camera.setName( viewStr )
160 join.setInput(i,camera)
161
162
164 """Create a Scene with a Camera and PointCloud for the camera solve."""
165 scene = nuke.createNode('Scene', '', False)
166 camera = nuke.createNode('Camera', '', False)
167 pointCloud = nuke.createNode('CameraTrackerPointCloud', '', False)
168 sw = scene.screenWidth()
169 sh = scene.screenHeight()
170 x = cameraTracker.xpos()
171 y = cameraTracker.ypos()
172 w = cameraTracker.screenWidth()
173 h = cameraTracker.screenHeight()
174 m = int(x + w/2)
175 camera.setXYpos(m + w, y + w + int((h-sh)/2))
176 pointCloud.setXYpos(m - int(pointCloud.screenWidth()/2), y + w)
177 scene.setXYpos(m - int(sw/2), y + w*2 - int((sh-h)/2))
178 camera.setInput(0,None)
179 pointCloud.setInput(0,cameraTracker)
180 scene.setInput(0,camera)
181 scene.setInput(1,pointCloud)
182 numviews = len( nuke.views() )
183 link = False
184 linkKnob = cameraTracker.knob("linkOutput")
185 if linkKnob:
186 link = bool(linkKnob.getValue())
187 if link:
188 camera.knob("focal").setExpression(cameraTracker.name() + ".focalLength")
189 camera.knob("haperture").setExpression(cameraTracker.name() + ".aperture.x")
190 camera.knob("vaperture").setExpression(cameraTracker.name() + ".aperture.y")
191 camera.knob("translate").setExpression(cameraTracker.name() + ".camTranslate")
192 camera.knob("rotate").setExpression(cameraTracker.name() + ".camRotate")
193 camera.knob("win_translate").setExpression(cameraTracker.name() + ".windowTranslate")
194 camera.knob("win_scale").setExpression(cameraTracker.name() + ".windowScale")
195 else:
196 camera.knob("focal").fromScript(cameraTracker.knob("focalLength").toScript(False))
197 camera.knob("translate").fromScript(cameraTracker.knob("camTranslate").toScript(False))
198 camera.knob("rotate").fromScript(cameraTracker.knob("camRotate").toScript(False))
199 camera.knob("win_translate").fromScript(cameraTracker.knob("windowTranslate").toScript(False))
200 camera.knob("win_scale").fromScript(cameraTracker.knob("windowScale").toScript(False))
201 for i in xrange(numviews):
202 camera.knob("haperture").setValue(cameraTracker.knob("aperture").getValue(0,i+1),0,0,i+1)
203 camera.knob("vaperture").setValue(cameraTracker.knob("aperture").getValue(1,i+1),0,0,i+1)
204 return [scene, camera, pointCloud]
205
206
208 """Create a Scene with a Camera, PointCloud, ScanlineRender, and LensDistortion (Undistort) node."""
209 [scene, camera, pointCloud] = createScene(cameraTracker);
210 lensDistort = createUndistortion(cameraTracker)
211 scanline = nuke.createNode('ScanlineRender', '', False)
212
213
214 dummyCamera = nuke.createNode('Camera', '', False)
215
216 cameraToSceneDot = nuke.createNode('Dot', '', False)
217 cameraToScanlineRenderDot = nuke.createNode('Dot', '', False)
218 lensToScanlineDot = nuke.createNode('Dot', '', False)
219
220 sw = dummyCamera.screenWidth()
221 sh = dummyCamera.screenHeight()
222 x = cameraTracker.xpos()
223 y = cameraTracker.ypos()
224 w = cameraTracker.screenWidth()
225 h = cameraTracker.screenHeight()
226
227
228 dw = 12
229 dh = 12
230 m = int(x + w/2)
231 hspacing = int(w*1.5)
232 vspacing = w
233
234 nuke.delete(dummyCamera);
235
236 camera.setXYpos( m - hspacing - int(sw/2), y + vspacing + int((h-sh)/2))
237 pointCloud.setXYpos( m - int(w/2), y + vspacing)
238 lensDistort.setXYpos( m + hspacing - int(w/2), y + vspacing)
239 scene.setXYpos( m - int(sw/2), y + 2*vspacing + int((h-sh)/2))
240 scanline.setXYpos( m - int(w/2), y + 3*vspacing)
241
242 cameraToSceneDot.setXYpos( m - hspacing - int(dw/2), y + 2*vspacing + int((h-dh)/2) )
243 cameraToScanlineRenderDot.setXYpos( m - hspacing - int(dw/2), y + 3*vspacing + int((h-dh)/2) )
244 lensToScanlineDot.setXYpos( m + hspacing - int(dw/2), y + 3*vspacing + int((h-dh)/2) )
245
246 cameraToSceneDot.setInput(0, camera)
247 cameraToScanlineRenderDot.setInput(0, cameraToSceneDot)
248 lensToScanlineDot.setInput(0, lensDistort)
249
250 camera.setInput(0,None)
251 pointCloud.setInput(0,cameraTracker)
252
253
254 if cameraTracker.inputs() > 0 and cameraTracker.input(0) != None:
255 lensDistort.setInput(0, cameraTracker.input(0))
256 else:
257 lensDistort.setInput(0, None)
258 scene.setInput(0,cameraToSceneDot)
259 scene.setInput(1,pointCloud)
260 scanline.setInput(0, lensToScanlineDot)
261 scanline.setInput(1, scene)
262 scanline.setInput(2, cameraToScanlineRenderDot)
263
264
270
271
274
275
278
279
281 """Create a LensDistortion node which matches the settings calculated by the CameraTracker."""
282 _clearSelection()
283
284 lensDistort = nuke.createNode('LensDistortion', '', False)
285 lensDistort.setInput(0, cameraTracker.input(0))
286 link = cameraTracker["linkOutput"].getValue()
287 _copyKnob(cameraTracker, "lensType", lensDistort, "lensType", link)
288 _copyKnob(cameraTracker, "distortion1", lensDistort, "distortion1", link)
289 _copyKnob(cameraTracker, "distortion2", lensDistort, "distortion2", link)
290 _copyKnob(cameraTracker, "distortionCenter", lensDistort, "distortionCenter", link)
291 _copyKnob(cameraTracker, "anamorphicSqueeze", lensDistort, "anamorphicSqueeze", link)
292 _copyKnob(cameraTracker, "asymmetricDistortion", lensDistort, "asymmetricDistortion", link)
293 lensDistort['invertDistortion'].setValue(invertDistortion)
294 _copyKnob(cameraTracker, "filter", lensDistort, "filter", False)
295 _copyKnob(cameraTracker, "cardScale", lensDistort, "cardScale", False)
296 _copyKnob(cameraTracker, "a", lensDistort, "a", False)
297 _copyKnob(cameraTracker, "b", lensDistort, "b", False)
298 _copyKnob(cameraTracker, "c", lensDistort, "c", False)
299
300 lensDistort.selectOnly()
301
302 return lensDistort
303
304
306 numCameras = solver["camTranslate"].getNumKeys()
307
308 if numCameras >= 100:
309 ok = nuke.ask("This will create %d cards, which may take some time.\nAre you sure?" % numCameras)
310 if not ok:
311 return
312
313 if numCameras == 0:
314 nuke.message("You can only create a card set when you have solved cameras.")
315 return
316
317 x = solver.xpos()
318 y = solver.ypos()
319 w = solver.screenWidth()
320 h = solver.screenHeight()
321 m = int(x + w*2)
322 link = False
323 linkKnob = solver.knob("linkOutput")
324 if linkKnob:
325 link = bool(linkKnob.getValue())
326 exprStr = "[python {nuke.toNode('" + solver.fullName() +"')"
327
328 group = nuke.createNode("Group", '', False)
329 group.begin()
330 group.setName("Cards")
331 group.setXYpos(m + w, y + w)
332 if numCameras>0:
333 scene = nuke.createNode("Scene", '', False)
334 sw = scene.screenWidth()
335 sh = scene.screenHeight()
336 inImg = nuke.createNode("Input", '', False)
337 inImg.setName("img");
338 inImg.setXYpos(m + int(w*(numCameras-1)/2) - int(sw/2), y)
339 out = nuke.createNode("Output", '', False)
340 out.setXYpos(m + int(w*(numCameras-1)/2) - int(sw/2), y + 5*w)
341 out.setInput(0, scene)
342 group.addKnob(nuke.Tab_Knob('cards','Cards'))
343 zDistKnob = nuke.Double_Knob('z','z')
344 zDistKnob.setRange(0,100)
345 zDistKnob.setTooltip("Cards are placed this far from origin. Use this to make a pan & tile dome of this radius.")
346 zDistKnob.setDefaultValue([1])
347 group.addKnob(zDistKnob)
348 for i in xrange(numCameras):
349 frame = solver.knob("camTranslate").getKeyTime(i)
350 hold = nuke.createNode("FrameHold", '', False)
351 hold.setInput(0, inImg)
352 hold.knob("first_frame").setValue(frame)
353 hold.setXYpos(m + w*i - int(w/2), y + w)
354 camera = nuke.createNode("Camera", '', False)
355 camera.setInput(0,None)
356 if link:
357 camera.knob("focal").setExpression( exprStr + ".knob('focalLength').getValueAt(" + str(frame) + ")}]" )
358 camera.knob("haperture").setExpression( exprStr + ".knob('aperture').getValueAt(" + str(frame) + ",0)}]" )
359 camera.knob("vaperture").setExpression( exprStr + ".knob('aperture').getValueAt(" + str(frame) + ",1)}]" )
360 camera.knob("translate").setExpression( exprStr + ".knob('camTranslate').getValueAt(" + str(frame) + ",0)}]",0 )
361 camera.knob("translate").setExpression( exprStr + ".knob('camTranslate').getValueAt(" + str(frame) + ",1)}]",1 )
362 camera.knob("translate").setExpression( exprStr + ".knob('camTranslate').getValueAt(" + str(frame) + ",2)}]",2 )
363 camera.knob("rotate").setExpression( exprStr + ".knob('camRotate').getValueAt(" + str(frame) + ",0)}]",0 )
364 camera.knob("rotate").setExpression( exprStr + ".knob('camRotate').getValueAt(" + str(frame) + ",1)}]",1 )
365 camera.knob("rotate").setExpression( exprStr + ".knob('camRotate').getValueAt(" + str(frame) + ",2)}]",2 )
366 else:
367 camera.knob("focal").setValue( solver.knob("focalLength").getValueAt(frame) )
368 camera.knob("haperture").setValue( solver.knob("aperture").getValueAt(frame,0) )
369 camera.knob("vaperture").setValue( solver.knob("aperture").getValueAt(frame,1) )
370 camera.knob("translate").setValue( solver.knob("camTranslate").getValueAt(frame) )
371 camera.knob("rotate").setValue( solver.knob("camRotate").getValueAt(frame) )
372 camera.setXYpos(m + w*i - int(sw/2), y + 3*w)
373 card = nuke.createNode("Card", '', False)
374 card.setSelected(False)
375 card.knob("lens_in_focal").setExpression( camera.name() + ".focal" )
376 card.knob("lens_in_haperture").setExpression( camera.name() + ".haperture" )
377 card.knob("translate").setExpression( camera.name() + ".translate" )
378 card.knob("rotate").setExpression( camera.name() + ".rotate" )
379 card.knob("z").setExpression( "parent.z" )
380 card.setInput(0, hold)
381 card.setXYpos(m + w*i - int(w/2), y + 2*w)
382 scene.setInput(i*2,camera)
383 scene.setInput(i*2+1,card)
384 scene.setXYpos(m + int(w*(numCameras-1)/2) - int(sw/2), y + 4*w)
385 group.end()
386 group.setInput(0,solver)
387 return group
388
389
390 -def _copyKnob(fromNode, fromKnobName, toNode, toKnobName, link):
397
398
402
403
404
405
406
407
409 """Finds the index of the preset knob and sets the film back size knob accordingly."""
410 filmbackSizeKnob = cameraTracker['filmBackSize']
411 selectedFilmbackSize = nukescripts.camerapresets.getFilmBackSize(selectedPresetIdx)
412 filmbackSizeKnob.setValue( selectedFilmbackSize[0], 0 )
413 filmbackSizeKnob.setValue( selectedFilmbackSize[1], 1 )
414
415
416 filmBackUnits = cameraTracker['filmBackUnits']
417 filmBackUnits.setValue(0)
418
419
420
421
422
423
427
428
429 if not nuke.env['assist']:
430 nuke.addOnCreate(cameratrackerCreateCallback, nodeClass='CameraTracker')
431 nuke.addOnCreate(cameratrackerCreateCallback, nodeClass='CameraTracker1_0')
432
433
434
435
436
438 """
439 Modal dialog for selecting a Linkable node in the script.
440
441 The following class creates a modal dialog with one UI element: an enum of nodes
442 that derive from the LinkableI class. (The LinkableI interface allows us to easily query and import
443 2D data from a variety of sources, particularly the Tracker node.) The user then selects their source node
444 to import the data from. Once Ok'ed, the code below creates a new user track for each LinkableInfo object
445 returned in the node.linkableKnobs() function with an XY co-ordinate, and then copies each animation curve
446 entry.
447 """
448
456
457
464
466 """Copies the x, y animation curves from one XYKnob linkable object to another."""
467 linkKnobSrc = linkableSrc.knob()
468 linkKnobDst = linkableDst.knob()
469 linkableSrcIndices = linkableSrc.indices()
470 linkableDstIndices = linkableDst.indices()
471
472
473 linkKnobDst.setValue(linkableSrc.enabled(), 0)
474
475
476 linkKnobDst.clearAnimated(int(linkableDstIndices[0]))
477 linkKnobDst.clearAnimated(int(linkableDstIndices[1]))
478
479
480 for i in xrange(0, linkKnobSrc.getNumKeys(int(linkableSrcIndices[0]))):
481 t = linkKnobSrc.getKeyTime(i, int(linkableSrcIndices[0]))
482
483
484
485 if i == 0:
486 linkKnobDst.setAnimated(int(linkableDstIndices[0]))
487 linkKnobDst.setAnimated(int(linkableDstIndices[1]))
488
489
490 linkKnobDst.setValueAt( linkKnobSrc.getValueAt(t, int(linkableSrcIndices[0])), t, int(linkableDstIndices[0]) )
491 linkKnobDst.setValueAt( linkKnobSrc.getValueAt(t, int(linkableSrcIndices[1])), t, int(linkableDstIndices[1]) )
492
493
494 if i == 0 and t != 0:
495 linkKnobDst.removeKeyAt(0, int(linkableDstIndices[0]))
496 linkKnobDst.removeKeyAt(0, int(linkableDstIndices[1]))
497
498
500 """Import data from a Tracker node into the CameraTracker node."""
501
502 strSrcNode = LinkableImportPanel().showModalDialog(cameraTracker)
503 if not strSrcNode:
504 return
505
506
507 srcNode = nuke.toNode(strSrcNode)
508
509 linkablesSrc = srcNode.linkableKnobs(nuke.KnobType.eXYKnob)
510 linkablesDst = cameraTracker.linkableKnobs(nuke.KnobType.eXYKnob)
511
512
513
514
515 startIndex = len(linkablesDst)
516
517
518 numValidTracks = 0
519
520 for linkableSrc in linkablesSrc:
521 linkableSrcIndices = linkableSrc.indices()
522
523
524 if len(linkableSrcIndices) != 2:
525 continue
526
527
528
529
530
531 cameraTracker['addUserTrack'].execute()
532
533
534 linkablesDst = cameraTracker.linkableKnobs(nuke.KnobType.eXYKnob)
535
536 if startIndex >= len(linkablesDst):
537 continue;
538
539 linkableDst = linkablesDst[startIndex]
540
541 _copyLinkableXYAnimCurve(linkableSrc, linkableDst)
542
543
544 startIndex = startIndex + 1
545
546
548 """Export data from the CameraTracker node into a Tracker node."""
549 tracker = nuke.createNode('Tracker4', '', True)
550 x = cameraTracker.xpos()
551 y = cameraTracker.ypos()
552 w = cameraTracker.screenWidth()
553 h = cameraTracker.screenHeight()
554 m = int(x + w/2)
555 tracker.setXYpos(m - int(tracker.screenWidth()/2), y + w)
556
557
558 linkablesSrc = cameraTracker.linkableKnobs(nuke.KnobType.eXYKnob)
559 linkablesDst = tracker.linkableKnobs(nuke.KnobType.eXYKnob)
560
561
562
563
564 startIndex = len(linkablesDst)
565
566
567 numValidTracks = 0
568
569 for linkableSrc in linkablesSrc:
570 linkableSrcIndices = linkableSrc.indices()
571
572
573 if len(linkableSrcIndices) < 2:
574 continue
575
576
577
578
579
580 tracker['add_track'].execute()
581
582 linkablesDst = tracker.linkableKnobs(nuke.KnobType.eXYKnob)
583
584 if startIndex >= len(linkablesDst):
585 continue;
586
587 linkableDst = linkablesDst[startIndex]
588
589 _copyLinkableXYAnimCurve(linkableSrc, linkableDst)
590
591
592 startIndex = startIndex + 1
593
594
602
603
611