lexgui 0.7.11 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,8 +17,8 @@ LX.registerIcon("TimelineLockOpen", '<svg xmlns="http://www.w3.org/2000/svg" wid
17
17
  class Timeline {
18
18
 
19
19
  /**
20
- * @param {string} name = string unique id
21
- * @param {object} options = {skipLock, skipVisibility}
20
+ * @param {String} name = string unique id
21
+ * @param {Object} options = {skipLock, skipVisibility}
22
22
  */
23
23
  constructor( id, options = {} ) {
24
24
 
@@ -104,6 +104,7 @@ class Timeline {
104
104
  this.canvasArea = right;
105
105
  this.canvasArea.root.classList.add("lextimelinearea");
106
106
 
107
+ this.selectedTracks = []; // [track, track] contains selected (highlighted) tracks. That is, tracks with .isSelected == true. Elements in array are not ordered. Only visible tracks should be selected
107
108
  this.selectedItems = []; // [trackInfo, "groupId"], contains the visible items (tracks or groups) of the timeline
108
109
  this.leftPanel = left.addPanel( { className: 'lextimelinepanel', width: "100%", height: "100%" } );
109
110
  this.trackTreesPanel = null;
@@ -183,7 +184,6 @@ class Timeline {
183
184
 
184
185
  /**
185
186
  * @method updateHeader
186
- * @param {*}
187
187
  */
188
188
 
189
189
  updateHeader() {
@@ -315,8 +315,7 @@ class Timeline {
315
315
  if ( this.onAddNewTrackButton ){
316
316
  this.onAddNewTrackButton();
317
317
  }else{
318
- const trackIdx = this.addNewTrack();
319
- this.changeSelectedItems( [trackIdx] );
318
+ this.addNewTrack();
320
319
  }
321
320
  }, { hideName: true, title: "Add Track", icon: "Plus" });
322
321
  }
@@ -335,13 +334,17 @@ class Timeline {
335
334
  this.trackTreesComponent = p.addTree(null, treeTracks, {filter: false, rename: false, draggable: false, onevent: (e) => {
336
335
  switch(e.type) {
337
336
  case LX.TreeEvent.NODE_SELECTED:
337
+ if ( !e.event.shiftKey ){
338
+ this.deselectAllTracks( false ); // no need to update left panel
339
+ }
338
340
  if (e.node.trackData){
339
- this.selectTrack(e.node.trackData.trackIdx);
341
+ const flag = e.event.shiftKey? !e.node.trackData.isSelected : true;
342
+ this.setTrackSelection( e.node.trackData.trackIdx, flag, false, false ); // do callback, do not update left panel
340
343
  }
341
344
  break;
342
345
  case LX.TreeEvent.NODE_VISIBILITY:
343
346
  if (e.node.trackData){
344
- this.setTrackState( e.node.trackData.trackIdx, e.value );
347
+ this.setTrackState( e.node.trackData.trackIdx, e.value, false, false ); // do not update left panel
345
348
  }
346
349
  break;
347
350
  }
@@ -358,6 +361,10 @@ class Timeline {
358
361
  that.setTrackHeight( that.trackHeight );
359
362
  }
360
363
 
364
+ if ( this.selectedTracks.length ){
365
+ this._updateTrackTreeSelection(); // select visible tracks
366
+ }
367
+
361
368
  // setting a name in the addTree function adds an undesired node
362
369
  this.trackTreesComponent.name = "tracksTrees";
363
370
  p.components[this.trackTreesComponent.name] = this.trackTreesComponent;
@@ -406,7 +413,7 @@ class Timeline {
406
413
  }
407
414
 
408
415
  /**
409
- * @param {object} options options for the new track
416
+ * @param {Object} options options for the new track
410
417
  * { id: string, active: bool, locked: bool, }
411
418
  * @returns
412
419
  */
@@ -425,8 +432,8 @@ class Timeline {
425
432
  /**
426
433
  * Finds tracks (wholy and partially) inside the range minY maxY.
427
434
  * (Full) Canvas local coordinates.
428
- * @param {number} minY
429
- * @param {number} maxY
435
+ * @param {Number} minY
436
+ * @param {Number} maxY
430
437
  * @returns array of trackDatas
431
438
  */
432
439
  getTracksInRange( minY, maxY ) {
@@ -466,14 +473,14 @@ class Timeline {
466
473
  /**
467
474
  * @method setAnimationClip
468
475
  * @param {*} animation
469
- * @param {boolean} needsToProcess
470
- * @param {obj} processOptions
476
+ * @param {Boolean} needsToProcess
477
+ * @param {Object} processOptions
471
478
  * [KeyFrameTimeline] - each track should contain an attribute "dim" to indicate the value dimension (e.g. vector3 -> dim=3). Otherwise dimensions will be infered from track's values and times. Default is 1
472
479
  */
473
480
  setAnimationClip( animation, needsToProcess = true ) {
474
481
 
475
482
  this.deselectAllElements();
476
- this.deselectAllTracks();
483
+ this.deselectAllTracks( false ); // no need to update left panel yet
477
484
 
478
485
  this.selectedItems = [];
479
486
 
@@ -802,7 +809,7 @@ class Timeline {
802
809
  * @method setScroll
803
810
  * not delta from last state, but full scroll amount.
804
811
  * @param {Number} scrollY either pixels or [0,1]
805
- * @param {Bool} normalized if true, scrollY is in range[0,1] being 1 fully scrolled. Otherwised scrollY represents pixels
812
+ * @param {Boolean} normalized if true, scrollY is in range[0,1] being 1 fully scrolled. Otherwised scrollY represents pixels
806
813
  * @returns
807
814
  */
808
815
 
@@ -930,7 +937,6 @@ class Timeline {
930
937
  this.movingKeys = false;
931
938
  this.timeBeforeMove = null;
932
939
  this.boxSelection = false; // after mouseup
933
- this.deselectAllTracks();
934
940
  }
935
941
 
936
942
 
@@ -1057,7 +1063,7 @@ class Timeline {
1057
1063
 
1058
1064
  /**
1059
1065
  * @method changeState
1060
- * @param {bool} skipCallback defaults false
1066
+ * @param {Boolean} skipCallback defaults false
1061
1067
  * @description change play/pause state
1062
1068
  **/
1063
1069
  changeState(skipCallback = false) {
@@ -1065,8 +1071,8 @@ class Timeline {
1065
1071
  }
1066
1072
  /**
1067
1073
  * @method setState
1068
- * @param {bool} state
1069
- * @param {bool} skipCallback defaults false
1074
+ * @param {Boolean} state
1075
+ * @param {Boolean} skipCallback defaults false
1070
1076
  * @description change play/pause state
1071
1077
  **/
1072
1078
  setState(state, skipCallback = false) {
@@ -1081,8 +1087,8 @@ class Timeline {
1081
1087
 
1082
1088
  /**
1083
1089
  * @method setLoopMode
1084
- * @param {bool} loopState
1085
- * @param {bool} skipCallback defaults false
1090
+ * @param {Boolean} loopState
1091
+ * @param {Boolean} skipCallback defaults false
1086
1092
  * @description change loop mode of the timeline
1087
1093
  */
1088
1094
  setLoopMode(loopState, skipCallback = false){
@@ -1116,11 +1122,14 @@ class Timeline {
1116
1122
  }
1117
1123
 
1118
1124
  /**
1119
- * @param {*} itemsToAdd [ trackIdx ], array of numbers identifying tracks by their index
1120
- * @param {*} itemsToRemove [ trackIdx ], array of numbers identifying tracks by their index
1125
+ * @param {Array} itemsToAdd [ trackIdx ], array of numbers identifying tracks by their index
1126
+ * @param {Array} itemsToRemove [ trackIdx ], array of numbers identifying tracks by their index
1121
1127
  */
1122
1128
  changeSelectedItems( itemsToAdd = null, itemsToRemove = null, skipCallback = false ) {
1123
1129
 
1130
+ this.deselectAllElements();
1131
+ this.deselectAllTracks( false ); // no need to update left panel. It is going to be rebuilt anyways
1132
+
1124
1133
  const tracks = this.animationClip.tracks;
1125
1134
 
1126
1135
  if ( itemsToRemove ){
@@ -1144,7 +1153,6 @@ class Timeline {
1144
1153
  }
1145
1154
  }
1146
1155
 
1147
-
1148
1156
  this.updateLeftPanel();
1149
1157
 
1150
1158
  if(this.onItemSelected && !skipCallback){
@@ -1154,7 +1162,7 @@ class Timeline {
1154
1162
 
1155
1163
  /**
1156
1164
  * It will find the first occurrence of trackId in animationClip.tracks
1157
- * @param {string} trackId
1165
+ * @param {String} trackId
1158
1166
  * @returns
1159
1167
  */
1160
1168
  getTrack( trackId ){
@@ -1168,38 +1176,83 @@ class Timeline {
1168
1176
  }
1169
1177
 
1170
1178
  /**
1171
- * Only affects render visualisation
1172
- * @method selectTrack
1173
- * @param {int} trackIdx
1174
- * // NOTE: to select a track from outside of the timeline, a this.trackTreesComponent.innerTree.select(item) needs to be called.
1175
- */
1176
- selectTrack( trackIdx ) {
1179
+ * @param {Boolean} updateTrackTree whether the track tree needs a refresh
1180
+ * @returns
1181
+ */
1182
+ deselectAllTracks( updateTrackTree = true ) {
1177
1183
 
1178
1184
  if( !this.animationClip ){
1179
1185
  return;
1180
1186
  }
1181
1187
 
1182
- this.deselectAllTracks();
1183
-
1184
- let track = this.animationClip.tracks[ trackIdx ];
1185
- track.isSelected = true;
1186
-
1187
- if( this.onSelectTrack ){
1188
- this.onSelectTrack(track);
1188
+ const tracks = this.animationClip.tracks;
1189
+ for(let i = 0; i < tracks.length; i++){
1190
+ tracks[ i ].isSelected = false;
1191
+ }
1192
+
1193
+ this.selectedTracks.length = 0;
1194
+
1195
+ if ( updateTrackTree ){
1196
+ this._updateTrackTreeSelection();
1189
1197
  }
1190
1198
  }
1191
1199
 
1192
- // Only affects render visualisation
1193
- deselectAllTracks() {
1200
+ /**
1201
+ * @param {Int} trackIdx
1202
+ * @param {Boolean} isSelected new "selected" state of the track
1203
+ * @param {Boolean} skipCallback whether to call onSetTrackSelection
1204
+ * @param {Boolean} updateTrackTree whether track tree panel needs a refresh
1205
+ * @returns
1206
+ */
1207
+ setTrackSelection( trackIdx, isSelected, skipCallback = false, updateTrackTree = true ){
1208
+ const track = this.animationClip.tracks[ trackIdx ];
1209
+ const oldValue = track.isSelected;
1210
+ track.isSelected = isSelected;
1211
+
1212
+ const idx = this.selectedTracks.indexOf( track );
1213
+ if ( ( idx == -1 && !isSelected ) || ( idx > -1 && isSelected ) ){
1214
+ return;
1215
+ }
1216
+
1217
+ if ( idx == -1 ){
1218
+ this.selectedTracks.push( track );
1219
+ }else{
1220
+ this.selectedTracks.splice( idx, 1 );
1221
+ }
1194
1222
 
1195
- if( !this.animationClip ){
1196
- return;
1223
+ if( this.onSetTrackSelection && !skipCallback ){
1224
+ this.onSetTrackSelection(track, oldValue );
1197
1225
  }
1198
1226
 
1199
- const tracks = this.animationClip.tracks;
1200
- for(let i = 0; i < tracks.length; i++){
1201
- tracks[ i ].isSelected = false;
1227
+ if ( updateTrackTree ){
1228
+ this._updateTrackTreeSelection();
1229
+ }
1230
+ }
1231
+
1232
+ /**
1233
+ * updates trackTreesComponent's nodes, to match the selectedTracks
1234
+ */
1235
+ _updateTrackTreeSelection(){
1236
+ const data = this.trackTreesComponent.innerTree.data;
1237
+ const selected = this.trackTreesComponent.innerTree.selected;
1238
+ selected.length = 0;
1239
+
1240
+ const addToSelection = (nodes) =>{
1241
+ for( let i = 0; i < nodes.length; ++i ){
1242
+ if ( nodes[i].trackData && nodes[i].trackData.isSelected ){
1243
+ selected.push( nodes[i] );
1244
+ }
1245
+ if ( nodes[i].children ){
1246
+ addToSelection( nodes[i].children );
1247
+ }
1248
+ }
1202
1249
  }
1250
+
1251
+ // update innerTree (visible) selected nodes
1252
+ if ( this.selectedTracks.length ){
1253
+ addToSelection( data );
1254
+ }
1255
+ this.trackTreesComponent.innerTree.refresh();
1203
1256
  }
1204
1257
 
1205
1258
  deselectAllElements(){
@@ -1208,22 +1261,53 @@ class Timeline {
1208
1261
 
1209
1262
  /**
1210
1263
  * @method setTrackState
1211
- * @param {int} trackIdx
1212
- * @param {boolean} isEnbaled
1264
+ * @param {Int} trackIdx
1265
+ * @param {Boolean} isEnbaled
1266
+ * @param {Boolean} skipCallback onSetTrackState
1267
+ * @param {Boolean} updateTrackTree updates eye icon of the track, if it is visible in the timeline
1213
1268
  */
1214
- setTrackState(trackIdx, isEnbaled = true, skipCallback = false) {
1269
+ setTrackState(trackIdx, isEnbaled = true, skipCallback = false, updateTrackTree = true ) {
1215
1270
  const track = this.animationClip.tracks[trackIdx];
1216
1271
 
1217
1272
  const oldState = track.active;
1218
1273
  track.active = isEnbaled;
1219
1274
 
1220
- if(this.onSetTrackState && !skipCallback)
1275
+ if ( this.onSetTrackState && !skipCallback ){
1221
1276
  this.onSetTrackState(track, oldState);
1277
+ }
1278
+
1279
+ if ( updateTrackTree && !this.skipVisibility ){
1280
+ // TODO: a bit of an overkill. Maybe searching the node in the tree is less expensive
1281
+ this.updateLeftPanel();
1282
+ }
1222
1283
  }
1223
1284
 
1224
1285
  /**
1225
- * @param {Number} trackIdx index of track in the animation (not local index)
1226
- * @param {Bool} combineWithPrevious whether to create a new entry or unify changes into a single undo entry
1286
+ *
1287
+ * @param {Int} trackIdx
1288
+ * @param {Boolean} isLocked
1289
+ * @param {Boolean} skipCallback onSetTrackLock
1290
+ * @param {Boolean} updateTrackTree updates lock icon of the track, if it is visible in the timeline
1291
+ */
1292
+ setTrackLock(trackIdx, isLocked = false, skipCallback = false, updateTrackTree = true ){
1293
+ const track = this.animationClip.tracks[trackIdx];
1294
+
1295
+ const oldState = track.locked;
1296
+ track.locked = isLocked;
1297
+
1298
+ if ( this.onSetTrackLock && !skipCallback ){
1299
+ this.onSetTrackLock( track, oldState );
1300
+ }
1301
+
1302
+ if ( updateTrackTree && !this.skipLock ){
1303
+ // TODO: a bit of an overkill. Maybe searching the node in the tree is less expensive
1304
+ this.updateLeftPanel();
1305
+ }
1306
+ }
1307
+
1308
+ /**
1309
+ * @param {Int} trackIdx index of track in the animation (not local index)
1310
+ * @param {Boolean} combineWithPrevious whether to create a new entry or unify changes into a single undo entry
1227
1311
  */
1228
1312
  saveState( trackIdx, combineWithPrevious = false ) {
1229
1313
  if ( !this.historySaveEnabler ){ return; }
@@ -1339,10 +1423,7 @@ class Timeline {
1339
1423
  'icon': (track.locked ? 'TimelineLock' : 'TimelineLockOpen'),
1340
1424
  'swap': (track.locked ? 'TimelineLockOpen' : 'TimelineLock'),
1341
1425
  'callback': (node, swapValue, event) => {
1342
- node.trackData.locked = !node.trackData.locked;
1343
- if(this.onLockTrack){
1344
- this.onLockTrack(node.trackData, node);
1345
- }
1426
+ this.setTrackLock( node.trackData.trackIdx, !node.trackData.locked, false, false ); // do not update left panel
1346
1427
  }
1347
1428
  }]});
1348
1429
  }
@@ -1352,7 +1433,7 @@ class Timeline {
1352
1433
 
1353
1434
  /**
1354
1435
  *
1355
- * @param {obj} options set some values for the track instance (groups and trackIdx not included)
1436
+ * @param {Object} options set some values for the track instance (groups and trackIdx not included)
1356
1437
  * @returns
1357
1438
  */
1358
1439
  instantiateTrack(options = {}, clone = false) {
@@ -1361,7 +1442,7 @@ class Timeline {
1361
1442
  id: options.id ?? ( Math.floor(performance.now().toString()) + "_" + Math.floor(Math.random() * 0xffff) ), //must be unique, at least inside a group
1362
1443
  active: options.active ?? true,
1363
1444
  locked: options.locked ?? false,
1364
- isSelected: false, // render only
1445
+ isSelected: false, // render and lexgui tree
1365
1446
  trackIdx: -1,
1366
1447
  data: options.data ?? null // user defined data
1367
1448
  }
@@ -1369,7 +1450,7 @@ class Timeline {
1369
1450
 
1370
1451
  /**
1371
1452
  * Generates an animationClip using either the parameters set in the animation argument or using default values
1372
- * @param {obj} animation data with which to generate an animationClip
1453
+ * @param {Object} animation data with which to generate an animationClip
1373
1454
  * @returns
1374
1455
  */
1375
1456
  instantiateAnimationClip(options, clone = false) {
@@ -1421,8 +1502,8 @@ class KeyFramesTimeline extends Timeline {
1421
1502
 
1422
1503
  static ADDKEY_VALUESINARRAYS = 0x01; // addkeyframes as [ [k0v0, k0v1...], [k1v0, k1v1...] ] instead of [k0v0,k0v1,k1v0,k1v1]
1423
1504
  /**
1424
- * @param {string} name unique string
1425
- * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
1505
+ * @param {String} name unique string
1506
+ * @param {Object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
1426
1507
  */
1427
1508
  constructor(name, options = {}) {
1428
1509
 
@@ -1460,10 +1541,7 @@ class KeyFramesTimeline extends Timeline {
1460
1541
  'icon': (track.locked ? 'TimelineLock' : 'TimelineLockOpen'),
1461
1542
  'swap': (track.locked ? 'TimelineLockOpen' : 'TimelineLock'),
1462
1543
  'callback': (node, swapValue, event) => {
1463
- node.trackData.locked = !node.trackData.locked;
1464
- if(this.onLockTrack){
1465
- this.onLockTrack(node.trackData, node);
1466
- }
1544
+ this.setTrackLock( node.trackData.trackIdx, !node.trackData.locked, false, false ); // do not update left panel
1467
1545
  }
1468
1546
  }]});
1469
1547
  }
@@ -1483,7 +1561,7 @@ class KeyFramesTimeline extends Timeline {
1483
1561
 
1484
1562
  /**
1485
1563
  * OVERRIDE
1486
- * @param {obj} options track information that wants to be set to the new track
1564
+ * @param {Object} options track information that wants to be set to the new track
1487
1565
  * id, dim, values, times, selected, edited, hovered
1488
1566
  * @returns
1489
1567
  */
@@ -1528,7 +1606,7 @@ class KeyFramesTimeline extends Timeline {
1528
1606
 
1529
1607
  /**
1530
1608
  * Generates an animationClip using either the parameters set in the animation argument or using default values
1531
- * @param {obj} animation data with which to generate an animationClip
1609
+ * @param {Object} animation data with which to generate an animationClip
1532
1610
  * @returns
1533
1611
  */
1534
1612
  instantiateAnimationClip(animation, clone = false) {
@@ -1617,12 +1695,13 @@ class KeyFramesTimeline extends Timeline {
1617
1695
 
1618
1696
  /**
1619
1697
  * OVERRIDE
1620
- * @param {*} itemsToAdd [ trackIdx, "groupId" ], array of strings and/or number identifying groups and/or tracks
1621
- * @param {*} itemsToRemove [ trackIdx, "groupId" ], array of strings and/or number identifying groups and/or tracks
1698
+ * @param {Array} itemsToAdd [ trackIdx, "groupId" ], array of strings and/or number identifying groups and/or tracks
1699
+ * @param {Array} itemsToRemove [ trackIdx, "groupId" ], array of strings and/or number identifying groups and/or tracks
1622
1700
  */
1623
1701
  changeSelectedItems( itemsToAdd = null, itemsToRemove = null, skipCallback = false ) {
1624
1702
 
1625
1703
  this.deselectAllElements();
1704
+ this.deselectAllTracks( false ); // no need to update left panel. It is going to be rebuilt anyways
1626
1705
 
1627
1706
  const tracks = this.animationClip.tracks;
1628
1707
  const tracksPerGroup = this.animationClip.tracksPerGroup;
@@ -1662,8 +1741,8 @@ class KeyFramesTimeline extends Timeline {
1662
1741
  }
1663
1742
 
1664
1743
  /**
1665
- * @param {string} groupId unique identifier
1666
- * @param {array} groupTracks [ "trackID", trackIdx ] array of strings and/or numbers of the existing tracks to include in this group. A track can only be part of 1 group
1744
+ * @param {String} groupId unique identifier
1745
+ * @param {Array} groupTracks [ "trackID", trackIdx ] array of strings and/or numbers of the existing tracks to include in this group. A track can only be part of 1 group
1667
1746
  * if groupTracks == null, groupId is removed from the list
1668
1747
  */
1669
1748
  setTracksGroup( groupId, groupTracks = null ){
@@ -1731,7 +1810,7 @@ class KeyFramesTimeline extends Timeline {
1731
1810
  }
1732
1811
 
1733
1812
  /**
1734
- * @param {string} groupId
1813
+ * @param {String} groupId
1735
1814
  * @returns array of tracks or null
1736
1815
  */
1737
1816
  getTracksGroup( groupId ){
@@ -1740,8 +1819,8 @@ class KeyFramesTimeline extends Timeline {
1740
1819
 
1741
1820
  /**
1742
1821
  * OVERRIDE
1743
- * @param {string} trackId
1744
- * @param {string} groupId optionl. If not set, it will find the first occurrence of trackId in animationClip.tracks
1822
+ * @param {String} trackId
1823
+ * @param {String} groupId optionl. If not set, it will find the first occurrence of trackId in animationClip.tracks
1745
1824
  * @returns
1746
1825
  */
1747
1826
  getTrack( trackId, groupId = null ){
@@ -1759,8 +1838,8 @@ class KeyFramesTimeline extends Timeline {
1759
1838
 
1760
1839
  /**
1761
1840
  *
1762
- * @param {number} size pixels, height of keyframe
1763
- * @param {number} sizeHovered optional, size in pixels when hovered
1841
+ * @param {Number} size pixels, height of keyframe
1842
+ * @param {Number} sizeHovered optional, size in pixels when hovered
1764
1843
  */
1765
1844
  setKeyframeSize( size, sizeHovered = null ){
1766
1845
  this.keyframeSizeHovered = sizeHovered ?? size;
@@ -2300,7 +2379,7 @@ class KeyFramesTimeline extends Timeline {
2300
2379
 
2301
2380
  /**
2302
2381
  * updates an existing track with new values and times.
2303
- * @param {Integer} trackIdx index of track in the animationClip
2382
+ * @param {Int} trackIdx index of track in the animationClip
2304
2383
  * @param {*} newTrack object with two arrays: values and times. These will be set to the selected track
2305
2384
  * @returns
2306
2385
  */
@@ -2322,8 +2401,8 @@ class KeyFramesTimeline extends Timeline {
2322
2401
  * removes equivalent sequential keys either because of equal times or values
2323
2402
  * (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
2324
2403
  * @param {Int} trackIdx index of track in the animation
2325
- * @param {Bool} onlyEqualTime if true, removes only keyframes with equal times. Otherwise, values are ALSO compared through the class threshold
2326
- * @param {Bool} skipCallback if false, triggers "onOptimizeTracks" after optimizing
2404
+ * @param {Boolean} onlyEqualTime if true, removes only keyframes with equal times. Otherwise, values are ALSO compared through the class threshold
2405
+ * @param {Boolean} skipCallback if false, triggers "onOptimizeTracks" after optimizing
2327
2406
  */
2328
2407
  optimizeTrack(trackIdx, onlyEqualTime = false, skipCallback = false ) {
2329
2408
  if ( !this.animationClip ){ return; }
@@ -2333,6 +2412,11 @@ class KeyFramesTimeline extends Timeline {
2333
2412
  values = track.values,
2334
2413
  stride = track.dim,
2335
2414
  threshold = this.optimizeThreshold;
2415
+
2416
+ if ( track.locked ){
2417
+ return;
2418
+ }
2419
+
2336
2420
  let cmpFunction = (v, p, n, t) => { return Math.abs(v - p) >= t || Math.abs(v - n) >= t };
2337
2421
  let lastSavedIndex = 0;
2338
2422
  const lastIndex = times.length-1;
@@ -2675,7 +2759,7 @@ class KeyFramesTimeline extends Timeline {
2675
2759
 
2676
2760
  pasteKeyFrameValue( track, index ) {
2677
2761
 
2678
- if(this.clipboard.value.type != track.type){
2762
+ if(track.locked || this.clipboard.value.type != track.type){
2679
2763
  return;
2680
2764
  }
2681
2765
 
@@ -2720,6 +2804,10 @@ class KeyFramesTimeline extends Timeline {
2720
2804
  const values = clipboardInfo.values;
2721
2805
  const track = this.animationClip.tracks[trackIdx];
2722
2806
 
2807
+ if( track.locked ){
2808
+ continue;
2809
+ }
2810
+
2723
2811
  this.saveState(track.trackIdx, trackCount++);
2724
2812
  this.historySaveEnabler = false;
2725
2813
  this.addKeyFrames( track.trackIdx, values, times, -globalStart + pasteTime, KeyFramesTimeline.ADDKEY_VALUESINARRAYS );
@@ -2737,11 +2825,11 @@ class KeyFramesTimeline extends Timeline {
2737
2825
 
2738
2826
  /**
2739
2827
  *
2740
- * @param {int} trackIdx
2741
- * @param {array} newValues array of values for each keyframe. It should be a flat array of size track.dim*numKeyframes. Check ADDKEY_VALUESINARRAYS flag
2742
- * @param {array of numbers} newTimes must be ordered ascendently
2743
- * @param {number} timeOffset
2744
- * @param {int} flags
2828
+ * @param {Int} trackIdx
2829
+ * @param {Array} newValues array of values for each keyframe. It should be a flat array of size track.dim*numKeyframes. Check ADDKEY_VALUESINARRAYS flag
2830
+ * @param {Array of numbers} newTimes must be ordered ascendently
2831
+ * @param {Number} timeOffset
2832
+ * @param {Int} flags
2745
2833
  * KeyFramesTimeline.ADDKEY_VALUESINARRAYS: if set, newValues is an array of arrays, one for each entry [ [1,2,3], [5,6,7] ]. Times is still a flat array of values [ 0, 0.2 ]
2746
2834
 
2747
2835
  * @returns
@@ -2749,7 +2837,7 @@ class KeyFramesTimeline extends Timeline {
2749
2837
  addKeyFrames( trackIdx, newValues, newTimes, timeOffset = 0, flags = 0x00 ){
2750
2838
  const track = this.animationClip.tracks[trackIdx];
2751
2839
 
2752
- if ( !newTimes.length ){ return; }
2840
+ if ( !newTimes.length || track.locked ){ return null; }
2753
2841
 
2754
2842
  const valueDim = track.dim;
2755
2843
  const trackTimes = track.times;
@@ -2839,6 +2927,7 @@ class KeyFramesTimeline extends Timeline {
2839
2927
  return;
2840
2928
  }
2841
2929
 
2930
+ const tracks = this.animationClip.tracks;
2842
2931
  const firstTrack = this.lastKeyFramesSelected[0][0];
2843
2932
  let trackToRemove = firstTrack;
2844
2933
  let toDelete = []; // indices to delete of the same track
@@ -2848,6 +2937,12 @@ class KeyFramesTimeline extends Timeline {
2848
2937
  const numSelected = this.lastKeyFramesSelected.length;
2849
2938
  for( let i = 0; i < numSelected; ++i ){
2850
2939
  const [trackIdx, frameIdx] = this.lastKeyFramesSelected[i];
2940
+
2941
+ if ( tracks[trackIdx].locked ){
2942
+ tracks[trackIdx].selected[frameIdx] = false; // unselect
2943
+ continue;
2944
+ }
2945
+
2851
2946
  if ( trackToRemove != trackIdx ){
2852
2947
  this.saveState(trackToRemove, trackToRemove != firstTrack);
2853
2948
 
@@ -2859,7 +2954,7 @@ class KeyFramesTimeline extends Timeline {
2859
2954
  toDelete.length = 0;
2860
2955
  }
2861
2956
 
2862
- toDelete.push(frameIdx)
2957
+ toDelete.push( frameIdx );
2863
2958
  }
2864
2959
 
2865
2960
  this.saveState(trackToRemove, trackToRemove != firstTrack);
@@ -2874,7 +2969,7 @@ class KeyFramesTimeline extends Timeline {
2874
2969
  deleteKeyFrames( trackIdx, indices, skipCallback = false ){
2875
2970
  const track = this.animationClip.tracks[trackIdx];
2876
2971
 
2877
- if ( !indices.length ){
2972
+ if ( !indices.length || track.locked ){
2878
2973
  return false;
2879
2974
  }
2880
2975
 
@@ -2923,9 +3018,9 @@ class KeyFramesTimeline extends Timeline {
2923
3018
 
2924
3019
  /**
2925
3020
  * Binary search. Relies on track.times being a sorted array
2926
- * @param {object} track
2927
- * @param {number} time
2928
- * @param {number} mode on of the possible values
3021
+ * @param {Object} track
3022
+ * @param {Number} time
3023
+ * @param {Number} mode on of the possible values
2929
3024
  * - -1 = nearest frame with t[f] <= time
2930
3025
  * - 0 = nearest frame
2931
3026
  * - 1 = nearest frame with t[f] >= time
@@ -2967,9 +3062,9 @@ class KeyFramesTimeline extends Timeline {
2967
3062
 
2968
3063
  /**
2969
3064
  * get the nearest keyframe to "time" given a maximum threshold.
2970
- * @param {object} track
2971
- * @param {number} time
2972
- * @param {number} threshold must be positive value
3065
+ * @param {Object} track
3066
+ * @param {Number} time
3067
+ * @param {Number} threshold must be positive value
2973
3068
  * @returns returns a postive/zero value if there is a frame inside the threshold range. Otherwise, -1
2974
3069
  */
2975
3070
  getCurrentKeyFrame( track, time, threshold = 0.0 ) {
@@ -2987,10 +3082,10 @@ class KeyFramesTimeline extends Timeline {
2987
3082
 
2988
3083
  /**
2989
3084
  * Returns the interval of frames between minTime and maxTime (both included)
2990
- * @param {object} track
2991
- * @param {number} minTime
2992
- * @param {number} maxTime
2993
- * @param {number} threshold must be positive value
3085
+ * @param {Object} track
3086
+ * @param {Number} minTime
3087
+ * @param {Number} maxTime
3088
+ * @param {Number} threshold must be positive value
2994
3089
  * @returns an array with two values [ minFrame, maxFrame ]. Otherwise null
2995
3090
  */
2996
3091
  getKeyFramesInRange( track, minTime, maxTime, threshold = 0.0 ) {
@@ -3123,12 +3218,12 @@ class KeyFramesTimeline extends Timeline {
3123
3218
 
3124
3219
  const track = this.animationClip.tracks[trackIdx];
3125
3220
 
3126
- if(track.locked ){
3127
- return;
3128
- }
3129
-
3130
3221
  this.unHoverAll();
3131
3222
  this.deselectAllKeyFrames();
3223
+
3224
+ if( track.locked ){
3225
+ return;
3226
+ }
3132
3227
 
3133
3228
  this.saveState(track.trackIdx);
3134
3229
 
@@ -3154,8 +3249,8 @@ class ClipsTimeline extends Timeline {
3154
3249
  static CLONEREASON_TRACKCLONE = 4;
3155
3250
 
3156
3251
  /**
3157
- * @param {string} name
3158
- * @param {object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
3252
+ * @param {String} name
3253
+ * @param {Object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
3159
3254
  */
3160
3255
  constructor(name, options = {}) {
3161
3256
 
@@ -3170,7 +3265,7 @@ class ClipsTimeline extends Timeline {
3170
3265
 
3171
3266
  /**
3172
3267
  * Generates an animationClip using either the parameters set in the animation argument or using default values
3173
- * @param {obj} animation data with which to generate an animationClip
3268
+ * @param {Object} animation data with which to generate an animationClip
3174
3269
  * @returns
3175
3270
  */
3176
3271
  instantiateAnimationClip(animation, clone = false) {
@@ -3192,7 +3287,7 @@ class ClipsTimeline extends Timeline {
3192
3287
 
3193
3288
  /**
3194
3289
  *
3195
- * @param {obj} options set some values for the track instance (groups and trackIdx not included)
3290
+ * @param {Object} options set some values for the track instance (groups and trackIdx not included)
3196
3291
  * @returns
3197
3292
  */
3198
3293
  instantiateTrack(options = {}, clone = false) {
@@ -3293,6 +3388,7 @@ class ClipsTimeline extends Timeline {
3293
3388
  changeSelectedItems( ) {
3294
3389
 
3295
3390
  this.deselectAllElements();
3391
+ this.deselectAllTracks( false ); // no need to update left
3296
3392
 
3297
3393
  this.selectedItems = this.animationClip.tracks.slice();
3298
3394
 
@@ -3924,10 +4020,10 @@ class ClipsTimeline extends Timeline {
3924
4020
 
3925
4021
  /**
3926
4022
  *
3927
- * @param {obj} clip clip to be added
3928
- * @param {int} trackIdx (optional) track where to put the clip. -1 will find the first free slot. ***WARNING*** Must call getClipsInRange, before calling this function with a valid trackdIdx
3929
- * @param {float} offsetTime (optional) offset time of current time
3930
- * @param {float} searchStartTrackIdx (optional) if trackIdx is set to -1, this idx will be used as the starting point to find a valid track
4023
+ * @param {Object} clip clip to be added
4024
+ * @param {Int} trackIdx (optional) track where to put the clip. -1 will find the first free slot. ***WARNING*** Must call getClipsInRange, before calling this function with a valid trackdIdx
4025
+ * @param {Number} offsetTime (optional) offset time of current time
4026
+ * @param {Number} searchStartTrackIdx (optional) if trackIdx is set to -1, this idx will be used as the starting point to find a valid track
3931
4027
  * @returns a zero/positive value if successful. Otherwise, -1
3932
4028
  */
3933
4029
  addClip( clip, trackIdx = -1, offsetTime = 0, searchStartTrackIdx = 0 ) {
@@ -4174,8 +4270,8 @@ class ClipsTimeline extends Timeline {
4174
4270
  /**
4175
4271
  * User defined. Used when copying and pasting
4176
4272
  * @param {Array of clips} clipsToClone array of original clips. Do not modify clips in this array
4177
- * @param {float} timeOffset Value of time that should be added (or subtracted) from the timing attributes
4178
- * @param {int} reason Flag to signal the reason of the clone
4273
+ * @param {Number} timeOffset Value of time that should be added (or subtracted) from the timing attributes
4274
+ * @param {Int} reason Flag to signal the reason of the clone
4179
4275
  * @returns {Array of clips}
4180
4276
  */
4181
4277
  cloneClips( clipsToClone, timeOffset, reason = 0 ){