PropertyBinding.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. import { warn, error } from '../utils.js';
  2. // Characters [].:/ are reserved for track binding syntax.
  3. const _RESERVED_CHARS_RE = '\\[\\]\\.:\\/';
  4. const _reservedRe = new RegExp( '[' + _RESERVED_CHARS_RE + ']', 'g' );
  5. // Attempts to allow node names from any language. ES5's `\w` regexp matches
  6. // only latin characters, and the unicode \p{L} is not yet supported. So
  7. // instead, we exclude reserved characters and match everything else.
  8. const _wordChar = '[^' + _RESERVED_CHARS_RE + ']';
  9. const _wordCharOrDot = '[^' + _RESERVED_CHARS_RE.replace( '\\.', '' ) + ']';
  10. // Parent directories, delimited by '/' or ':'. Currently unused, but must
  11. // be matched to parse the rest of the track name.
  12. const _directoryRe = /*@__PURE__*/ /((?:WC+[\/:])*)/.source.replace( 'WC', _wordChar );
  13. // Target node. May contain word characters (a-zA-Z0-9_) and '.' or '-'.
  14. const _nodeRe = /*@__PURE__*/ /(WCOD+)?/.source.replace( 'WCOD', _wordCharOrDot );
  15. // Object on target node, and accessor. May not contain reserved
  16. // characters. Accessor may contain any character except closing bracket.
  17. const _objectRe = /*@__PURE__*/ /(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace( 'WC', _wordChar );
  18. // Property and accessor. May not contain reserved characters. Accessor may
  19. // contain any non-bracket characters.
  20. const _propertyRe = /*@__PURE__*/ /\.(WC+)(?:\[(.+)\])?/.source.replace( 'WC', _wordChar );
  21. const _trackRe = new RegExp( ''
  22. + '^'
  23. + _directoryRe
  24. + _nodeRe
  25. + _objectRe
  26. + _propertyRe
  27. + '$'
  28. );
  29. const _supportedObjectNames = [ 'material', 'materials', 'bones', 'map' ];
  30. class Composite {
  31. constructor( targetGroup, path, optionalParsedPath ) {
  32. const parsedPath = optionalParsedPath || PropertyBinding.parseTrackName( path );
  33. this._targetGroup = targetGroup;
  34. this._bindings = targetGroup.subscribe_( path, parsedPath );
  35. }
  36. getValue( array, offset ) {
  37. this.bind(); // bind all binding
  38. const firstValidIndex = this._targetGroup.nCachedObjects_,
  39. binding = this._bindings[ firstValidIndex ];
  40. // and only call .getValue on the first
  41. if ( binding !== undefined ) binding.getValue( array, offset );
  42. }
  43. setValue( array, offset ) {
  44. const bindings = this._bindings;
  45. for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {
  46. bindings[ i ].setValue( array, offset );
  47. }
  48. }
  49. bind() {
  50. const bindings = this._bindings;
  51. for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {
  52. bindings[ i ].bind();
  53. }
  54. }
  55. unbind() {
  56. const bindings = this._bindings;
  57. for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {
  58. bindings[ i ].unbind();
  59. }
  60. }
  61. }
  62. // Note: This class uses a State pattern on a per-method basis:
  63. // 'bind' sets 'this.getValue' / 'setValue' and shadows the
  64. // prototype version of these methods with one that represents
  65. // the bound state. When the property is not found, the methods
  66. // become no-ops.
  67. /**
  68. * This holds a reference to a real property in the scene graph; used internally.
  69. */
  70. class PropertyBinding {
  71. /**
  72. * Constructs a new property binding.
  73. *
  74. * @param {Object} rootNode - The root node.
  75. * @param {string} path - The path.
  76. * @param {?Object} [parsedPath] - The parsed path.
  77. */
  78. constructor( rootNode, path, parsedPath ) {
  79. /**
  80. * The object path to the animated property.
  81. *
  82. * @type {string}
  83. */
  84. this.path = path;
  85. /**
  86. * An object holding information about the path.
  87. *
  88. * @type {Object}
  89. */
  90. this.parsedPath = parsedPath || PropertyBinding.parseTrackName( path );
  91. /**
  92. * The object owns the animated property.
  93. *
  94. * @type {?Object}
  95. */
  96. this.node = PropertyBinding.findNode( rootNode, this.parsedPath.nodeName );
  97. /**
  98. * The root node.
  99. *
  100. * @type {Object3D|Skeleton}
  101. */
  102. this.rootNode = rootNode;
  103. // initial state of these methods that calls 'bind'
  104. this.getValue = this._getValue_unbound;
  105. this.setValue = this._setValue_unbound;
  106. }
  107. /**
  108. * Factory method for creating a property binding from the given parameters.
  109. *
  110. * @static
  111. * @param {Object} root - The root node.
  112. * @param {string} path - The path.
  113. * @param {?Object} [parsedPath] - The parsed path.
  114. * @return {PropertyBinding|Composite} The created property binding or composite.
  115. */
  116. static create( root, path, parsedPath ) {
  117. if ( ! ( root && root.isAnimationObjectGroup ) ) {
  118. return new PropertyBinding( root, path, parsedPath );
  119. } else {
  120. return new PropertyBinding.Composite( root, path, parsedPath );
  121. }
  122. }
  123. /**
  124. * Replaces spaces with underscores and removes unsupported characters from
  125. * node names, to ensure compatibility with parseTrackName().
  126. *
  127. * @param {string} name - Node name to be sanitized.
  128. * @return {string} The sanitized node name.
  129. */
  130. static sanitizeNodeName( name ) {
  131. return name.replace( /\s/g, '_' ).replace( _reservedRe, '' );
  132. }
  133. /**
  134. * Parses the given track name (an object path to an animated property) and
  135. * returns an object with information about the path. Matches strings in the following forms:
  136. *
  137. * - nodeName.property
  138. * - nodeName.property[accessor]
  139. * - nodeName.material.property[accessor]
  140. * - uuid.property[accessor]
  141. * - uuid.objectName[objectIndex].propertyName[propertyIndex]
  142. * - parentName/nodeName.property
  143. * - parentName/parentName/nodeName.property[index]
  144. * - .bone[Armature.DEF_cog].position
  145. * - scene:helium_balloon_model:helium_balloon_model.position
  146. *
  147. * @static
  148. * @param {string} trackName - The track name to parse.
  149. * @return {Object} The parsed track name as an object.
  150. */
  151. static parseTrackName( trackName ) {
  152. const matches = _trackRe.exec( trackName );
  153. if ( matches === null ) {
  154. throw new Error( 'PropertyBinding: Cannot parse trackName: ' + trackName );
  155. }
  156. const results = {
  157. // directoryName: matches[ 1 ], // (tschw) currently unused
  158. nodeName: matches[ 2 ],
  159. objectName: matches[ 3 ],
  160. objectIndex: matches[ 4 ],
  161. propertyName: matches[ 5 ], // required
  162. propertyIndex: matches[ 6 ]
  163. };
  164. const lastDot = results.nodeName && results.nodeName.lastIndexOf( '.' );
  165. if ( lastDot !== undefined && lastDot !== - 1 ) {
  166. const objectName = results.nodeName.substring( lastDot + 1 );
  167. // Object names must be checked against an allowlist. Otherwise, there
  168. // is no way to parse 'foo.bar.baz': 'baz' must be a property, but
  169. // 'bar' could be the objectName, or part of a nodeName (which can
  170. // include '.' characters).
  171. if ( _supportedObjectNames.indexOf( objectName ) !== - 1 ) {
  172. results.nodeName = results.nodeName.substring( 0, lastDot );
  173. results.objectName = objectName;
  174. }
  175. }
  176. if ( results.propertyName === null || results.propertyName.length === 0 ) {
  177. throw new Error( 'PropertyBinding: can not parse propertyName from trackName: ' + trackName );
  178. }
  179. return results;
  180. }
  181. /**
  182. * Searches for a node in the hierarchy of the given root object by the given
  183. * node name.
  184. *
  185. * @static
  186. * @param {Object} root - The root object.
  187. * @param {string|number} nodeName - The name of the node.
  188. * @return {?Object} The found node. Returns `null` if no object was found.
  189. */
  190. static findNode( root, nodeName ) {
  191. if ( nodeName === undefined || nodeName === '' || nodeName === '.' || nodeName === - 1 || nodeName === root.name || nodeName === root.uuid ) {
  192. return root;
  193. }
  194. // search into skeleton bones.
  195. if ( root.skeleton ) {
  196. const bone = root.skeleton.getBoneByName( nodeName );
  197. if ( bone !== undefined ) {
  198. return bone;
  199. }
  200. }
  201. // search into node subtree.
  202. if ( root.children ) {
  203. const searchNodeSubtree = function ( children ) {
  204. for ( let i = 0; i < children.length; i ++ ) {
  205. const childNode = children[ i ];
  206. if ( childNode.name === nodeName || childNode.uuid === nodeName ) {
  207. return childNode;
  208. }
  209. const result = searchNodeSubtree( childNode.children );
  210. if ( result ) return result;
  211. }
  212. return null;
  213. };
  214. const subTreeNode = searchNodeSubtree( root.children );
  215. if ( subTreeNode ) {
  216. return subTreeNode;
  217. }
  218. }
  219. return null;
  220. }
  221. // these are used to "bind" a nonexistent property
  222. _getValue_unavailable() {}
  223. _setValue_unavailable() {}
  224. // Getters
  225. _getValue_direct( buffer, offset ) {
  226. buffer[ offset ] = this.targetObject[ this.propertyName ];
  227. }
  228. _getValue_array( buffer, offset ) {
  229. const source = this.resolvedProperty;
  230. for ( let i = 0, n = source.length; i !== n; ++ i ) {
  231. buffer[ offset ++ ] = source[ i ];
  232. }
  233. }
  234. _getValue_arrayElement( buffer, offset ) {
  235. buffer[ offset ] = this.resolvedProperty[ this.propertyIndex ];
  236. }
  237. _getValue_toArray( buffer, offset ) {
  238. this.resolvedProperty.toArray( buffer, offset );
  239. }
  240. // Direct
  241. _setValue_direct( buffer, offset ) {
  242. this.targetObject[ this.propertyName ] = buffer[ offset ];
  243. }
  244. _setValue_direct_setNeedsUpdate( buffer, offset ) {
  245. this.targetObject[ this.propertyName ] = buffer[ offset ];
  246. this.targetObject.needsUpdate = true;
  247. }
  248. _setValue_direct_setMatrixWorldNeedsUpdate( buffer, offset ) {
  249. this.targetObject[ this.propertyName ] = buffer[ offset ];
  250. this.targetObject.matrixWorldNeedsUpdate = true;
  251. }
  252. // EntireArray
  253. _setValue_array( buffer, offset ) {
  254. const dest = this.resolvedProperty;
  255. for ( let i = 0, n = dest.length; i !== n; ++ i ) {
  256. dest[ i ] = buffer[ offset ++ ];
  257. }
  258. }
  259. _setValue_array_setNeedsUpdate( buffer, offset ) {
  260. const dest = this.resolvedProperty;
  261. for ( let i = 0, n = dest.length; i !== n; ++ i ) {
  262. dest[ i ] = buffer[ offset ++ ];
  263. }
  264. this.targetObject.needsUpdate = true;
  265. }
  266. _setValue_array_setMatrixWorldNeedsUpdate( buffer, offset ) {
  267. const dest = this.resolvedProperty;
  268. for ( let i = 0, n = dest.length; i !== n; ++ i ) {
  269. dest[ i ] = buffer[ offset ++ ];
  270. }
  271. this.targetObject.matrixWorldNeedsUpdate = true;
  272. }
  273. // ArrayElement
  274. _setValue_arrayElement( buffer, offset ) {
  275. this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
  276. }
  277. _setValue_arrayElement_setNeedsUpdate( buffer, offset ) {
  278. this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
  279. this.targetObject.needsUpdate = true;
  280. }
  281. _setValue_arrayElement_setMatrixWorldNeedsUpdate( buffer, offset ) {
  282. this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
  283. this.targetObject.matrixWorldNeedsUpdate = true;
  284. }
  285. // HasToFromArray
  286. _setValue_fromArray( buffer, offset ) {
  287. this.resolvedProperty.fromArray( buffer, offset );
  288. }
  289. _setValue_fromArray_setNeedsUpdate( buffer, offset ) {
  290. this.resolvedProperty.fromArray( buffer, offset );
  291. this.targetObject.needsUpdate = true;
  292. }
  293. _setValue_fromArray_setMatrixWorldNeedsUpdate( buffer, offset ) {
  294. this.resolvedProperty.fromArray( buffer, offset );
  295. this.targetObject.matrixWorldNeedsUpdate = true;
  296. }
  297. _getValue_unbound( targetArray, offset ) {
  298. this.bind();
  299. this.getValue( targetArray, offset );
  300. }
  301. _setValue_unbound( sourceArray, offset ) {
  302. this.bind();
  303. this.setValue( sourceArray, offset );
  304. }
  305. /**
  306. * Creates a getter / setter pair for the property tracked by this binding.
  307. */
  308. bind() {
  309. let targetObject = this.node;
  310. const parsedPath = this.parsedPath;
  311. const objectName = parsedPath.objectName;
  312. const propertyName = parsedPath.propertyName;
  313. let propertyIndex = parsedPath.propertyIndex;
  314. if ( ! targetObject ) {
  315. targetObject = PropertyBinding.findNode( this.rootNode, parsedPath.nodeName );
  316. this.node = targetObject;
  317. }
  318. // set fail state so we can just 'return' on error
  319. this.getValue = this._getValue_unavailable;
  320. this.setValue = this._setValue_unavailable;
  321. // ensure there is a value node
  322. if ( ! targetObject ) {
  323. warn( 'PropertyBinding: No target node found for track: ' + this.path + '.' );
  324. return;
  325. }
  326. if ( objectName ) {
  327. let objectIndex = parsedPath.objectIndex;
  328. // special cases were we need to reach deeper into the hierarchy to get the face materials....
  329. switch ( objectName ) {
  330. case 'materials':
  331. if ( ! targetObject.material ) {
  332. error( 'PropertyBinding: Can not bind to material as node does not have a material.', this );
  333. return;
  334. }
  335. if ( ! targetObject.material.materials ) {
  336. error( 'PropertyBinding: Can not bind to material.materials as node.material does not have a materials array.', this );
  337. return;
  338. }
  339. targetObject = targetObject.material.materials;
  340. break;
  341. case 'bones':
  342. if ( ! targetObject.skeleton ) {
  343. error( 'PropertyBinding: Can not bind to bones as node does not have a skeleton.', this );
  344. return;
  345. }
  346. // potential future optimization: skip this if propertyIndex is already an integer
  347. // and convert the integer string to a true integer.
  348. targetObject = targetObject.skeleton.bones;
  349. // support resolving morphTarget names into indices.
  350. for ( let i = 0; i < targetObject.length; i ++ ) {
  351. if ( targetObject[ i ].name === objectIndex ) {
  352. objectIndex = i;
  353. break;
  354. }
  355. }
  356. break;
  357. case 'map':
  358. if ( 'map' in targetObject ) {
  359. targetObject = targetObject.map;
  360. break;
  361. }
  362. if ( ! targetObject.material ) {
  363. error( 'PropertyBinding: Can not bind to material as node does not have a material.', this );
  364. return;
  365. }
  366. if ( ! targetObject.material.map ) {
  367. error( 'PropertyBinding: Can not bind to material.map as node.material does not have a map.', this );
  368. return;
  369. }
  370. targetObject = targetObject.material.map;
  371. break;
  372. default:
  373. if ( targetObject[ objectName ] === undefined ) {
  374. error( 'PropertyBinding: Can not bind to objectName of node undefined.', this );
  375. return;
  376. }
  377. targetObject = targetObject[ objectName ];
  378. }
  379. if ( objectIndex !== undefined ) {
  380. if ( targetObject[ objectIndex ] === undefined ) {
  381. error( 'PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined.', this, targetObject );
  382. return;
  383. }
  384. targetObject = targetObject[ objectIndex ];
  385. }
  386. }
  387. // resolve property
  388. const nodeProperty = targetObject[ propertyName ];
  389. if ( nodeProperty === undefined ) {
  390. const nodeName = parsedPath.nodeName;
  391. error( 'PropertyBinding: Trying to update property for track: ' + nodeName +
  392. '.' + propertyName + ' but it wasn\'t found.', targetObject );
  393. return;
  394. }
  395. // determine versioning scheme
  396. let versioning = this.Versioning.None;
  397. this.targetObject = targetObject;
  398. if ( targetObject.isMaterial === true ) {
  399. versioning = this.Versioning.NeedsUpdate;
  400. } else if ( targetObject.isObject3D === true ) {
  401. versioning = this.Versioning.MatrixWorldNeedsUpdate;
  402. }
  403. // determine how the property gets bound
  404. let bindingType = this.BindingType.Direct;
  405. if ( propertyIndex !== undefined ) {
  406. // access a sub element of the property array (only primitives are supported right now)
  407. if ( propertyName === 'morphTargetInfluences' ) {
  408. // potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
  409. // support resolving morphTarget names into indices.
  410. if ( ! targetObject.geometry ) {
  411. error( 'PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.', this );
  412. return;
  413. }
  414. if ( ! targetObject.geometry.morphAttributes ) {
  415. error( 'PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.morphAttributes.', this );
  416. return;
  417. }
  418. if ( targetObject.morphTargetDictionary[ propertyIndex ] !== undefined ) {
  419. propertyIndex = targetObject.morphTargetDictionary[ propertyIndex ];
  420. }
  421. }
  422. bindingType = this.BindingType.ArrayElement;
  423. this.resolvedProperty = nodeProperty;
  424. this.propertyIndex = propertyIndex;
  425. } else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
  426. // must use copy for Object3D.Euler/Quaternion
  427. bindingType = this.BindingType.HasFromToArray;
  428. this.resolvedProperty = nodeProperty;
  429. } else if ( Array.isArray( nodeProperty ) ) {
  430. bindingType = this.BindingType.EntireArray;
  431. this.resolvedProperty = nodeProperty;
  432. } else {
  433. this.propertyName = propertyName;
  434. }
  435. // select getter / setter
  436. this.getValue = this.GetterByBindingType[ bindingType ];
  437. this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];
  438. }
  439. /**
  440. * Unbinds the property.
  441. */
  442. unbind() {
  443. this.node = null;
  444. // back to the prototype version of getValue / setValue
  445. // note: avoiding to mutate the shape of 'this' via 'delete'
  446. this.getValue = this._getValue_unbound;
  447. this.setValue = this._setValue_unbound;
  448. }
  449. }
  450. PropertyBinding.Composite = Composite;
  451. PropertyBinding.prototype.BindingType = {
  452. Direct: 0,
  453. EntireArray: 1,
  454. ArrayElement: 2,
  455. HasFromToArray: 3
  456. };
  457. PropertyBinding.prototype.Versioning = {
  458. None: 0,
  459. NeedsUpdate: 1,
  460. MatrixWorldNeedsUpdate: 2
  461. };
  462. PropertyBinding.prototype.GetterByBindingType = [
  463. PropertyBinding.prototype._getValue_direct,
  464. PropertyBinding.prototype._getValue_array,
  465. PropertyBinding.prototype._getValue_arrayElement,
  466. PropertyBinding.prototype._getValue_toArray,
  467. ];
  468. PropertyBinding.prototype.SetterByBindingTypeAndVersioning = [
  469. [
  470. // Direct
  471. PropertyBinding.prototype._setValue_direct,
  472. PropertyBinding.prototype._setValue_direct_setNeedsUpdate,
  473. PropertyBinding.prototype._setValue_direct_setMatrixWorldNeedsUpdate,
  474. ], [
  475. // EntireArray
  476. PropertyBinding.prototype._setValue_array,
  477. PropertyBinding.prototype._setValue_array_setNeedsUpdate,
  478. PropertyBinding.prototype._setValue_array_setMatrixWorldNeedsUpdate,
  479. ], [
  480. // ArrayElement
  481. PropertyBinding.prototype._setValue_arrayElement,
  482. PropertyBinding.prototype._setValue_arrayElement_setNeedsUpdate,
  483. PropertyBinding.prototype._setValue_arrayElement_setMatrixWorldNeedsUpdate,
  484. ], [
  485. // HasToFromArray
  486. PropertyBinding.prototype._setValue_fromArray,
  487. PropertyBinding.prototype._setValue_fromArray_setNeedsUpdate,
  488. PropertyBinding.prototype._setValue_fromArray_setMatrixWorldNeedsUpdate,
  489. ]
  490. ];
  491. export { PropertyBinding };
粤ICP备19079148号