MaterialXInterfaceValidation.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import { MtlXLibrary } from './MaterialXNodeLibrary.js';
  2. import { MaterialXLogCodes } from './MaterialXLog.js';
  3. import registryData from './MaterialXNodeInterfaceRegistry.js';
  4. const SKIP_ELEMENTS = new Set( [ 'materialx', 'input', 'output' ] );
  5. const CONTAINER_ELEMENTS = new Set( [ 'nodegraph' ] );
  6. const LIBRARY_FALLBACK_OUTPUT = 'out';
  7. function formatInputElement( inputNodeX ) {
  8. const attrs = [];
  9. for ( const name of [ 'name', 'type', 'nodename', 'nodegraph', 'interfacename', 'output', 'value' ] ) {
  10. const value = inputNodeX.getAttribute( name );
  11. if ( value !== null && value !== '' ) {
  12. attrs.push( `${name}="${value}"` );
  13. }
  14. }
  15. return `<input ${attrs.join( ' ' )}>`;
  16. }
  17. function formatNodeElement( nodeX ) {
  18. const attrs = [];
  19. for ( const name of [ 'name', 'type', 'nodedef' ] ) {
  20. const value = nodeX.getAttribute( name );
  21. if ( value !== null && value !== '' ) {
  22. attrs.push( `${name}="${value}"` );
  23. }
  24. }
  25. return `<${nodeX.element} ${attrs.join( ' ' )}>`;
  26. }
  27. function scoreNodedefCandidate( candidateName, nodeX ) {
  28. const candidate = registryData.resolved[ candidateName ];
  29. if ( ! candidate ) return - 1;
  30. let score = 0;
  31. const nodeType = nodeX.type;
  32. const outputType = candidate.outputs.out || Object.values( candidate.outputs )[ 0 ];
  33. if ( nodeType && outputType === nodeType ) {
  34. score += 4;
  35. }
  36. for ( const child of nodeX.children ) {
  37. if ( child.element !== 'input' ) continue;
  38. const expectedType = candidate.inputs[ child.name ];
  39. if ( ! expectedType || ! child.type ) continue;
  40. if ( expectedType === child.type ) {
  41. score += 3;
  42. } else if ( typesCompatible( expectedType, child.type ) ) {
  43. score += 1;
  44. } else {
  45. score -= 2;
  46. }
  47. }
  48. return score;
  49. }
  50. function resolveInterface( nodeX ) {
  51. const nodedefName = nodeX.getAttribute( 'nodedef' );
  52. if ( nodedefName && registryData.resolved[ nodedefName ] ) {
  53. return { source: 'nodedef', name: nodedefName, ...registryData.resolved[ nodedefName ] };
  54. }
  55. const candidates = registryData.byNode[ nodeX.element ] || [];
  56. if ( candidates.length > 0 ) {
  57. let selected = candidates[ 0 ];
  58. let bestScore = - Infinity;
  59. for ( const candidateName of candidates ) {
  60. const score = scoreNodedefCandidate( candidateName, nodeX );
  61. if ( score > bestScore ) {
  62. bestScore = score;
  63. selected = candidateName;
  64. }
  65. }
  66. const resolved = registryData.resolved[ selected ];
  67. if ( resolved ) {
  68. return { source: 'nodedef', name: selected, ...resolved };
  69. }
  70. }
  71. const libraryEntry = MtlXLibrary[ nodeX.element ];
  72. if ( libraryEntry ) {
  73. const inputs = Object.fromEntries( libraryEntry.params.map( ( param ) => [ param, null ] ) );
  74. const outputs = nodeX.type ? { [ LIBRARY_FALLBACK_OUTPUT ]: nodeX.type } : {};
  75. return { source: 'library', name: nodeX.element, node: nodeX.element, inputs, outputs };
  76. }
  77. return null;
  78. }
  79. function resolveReferencedNode( inputNodeX ) {
  80. const materialX = inputNodeX.materialX;
  81. const referencePath = inputNodeX.referencePath;
  82. if ( ! referencePath ) return null;
  83. return materialX.getMaterialXNode( referencePath ) || null;
  84. }
  85. function resolveSourceOutputType( sourceNodeX, outputName ) {
  86. if ( sourceNodeX.element === 'output' ) {
  87. return {
  88. interface: null,
  89. outputType: sourceNodeX.type,
  90. outputName: sourceNodeX.name || 'out',
  91. };
  92. }
  93. const sourceInterface = resolveInterface( sourceNodeX );
  94. if ( ! sourceInterface ) return { interface: null, outputType: null, outputName };
  95. if ( outputName ) {
  96. if ( sourceInterface.outputs[ outputName ] ) {
  97. return {
  98. interface: sourceInterface,
  99. outputType: sourceInterface.outputs[ outputName ],
  100. outputName,
  101. };
  102. }
  103. return {
  104. interface: sourceInterface,
  105. outputType: null,
  106. outputName,
  107. };
  108. }
  109. if ( sourceNodeX.type && sourceNodeX.type !== 'multioutput' ) {
  110. return {
  111. interface: sourceInterface,
  112. outputType: sourceNodeX.type,
  113. outputName: 'out',
  114. };
  115. }
  116. if ( sourceInterface.outputs.out ) {
  117. return {
  118. interface: sourceInterface,
  119. outputType: sourceInterface.outputs.out,
  120. outputName: 'out',
  121. };
  122. }
  123. const declaredOutputs = Object.entries( sourceInterface.outputs );
  124. if ( declaredOutputs.length === 1 ) {
  125. const [ singleName, singleType ] = declaredOutputs[ 0 ];
  126. return {
  127. interface: sourceInterface,
  128. outputType: singleType,
  129. outputName: singleName,
  130. };
  131. }
  132. if ( sourceNodeX.type ) {
  133. return {
  134. interface: sourceInterface,
  135. outputType: sourceNodeX.type,
  136. outputName: null,
  137. };
  138. }
  139. return { interface: sourceInterface, outputType: null, outputName: null };
  140. }
  141. function typesCompatible( expectedType, actualType ) {
  142. if ( ! expectedType || ! actualType ) return true;
  143. if ( expectedType === actualType ) return true;
  144. if ( ( expectedType === 'float' && actualType === 'integer' ) || ( expectedType === 'integer' && actualType === 'float' ) ) {
  145. return true;
  146. }
  147. return false;
  148. }
  149. function validatePortConnection( inputNodeX, parentNodeX, log ) {
  150. const sourceNodeX = resolveReferencedNode( inputNodeX );
  151. if ( ! sourceNodeX ) return;
  152. const { outputType } = resolveSourceOutputType( sourceNodeX, inputNodeX.output );
  153. const inputSnippet = formatInputElement( inputNodeX );
  154. if ( inputNodeX.output && outputType === null ) {
  155. log.add(
  156. MaterialXLogCodes.INVALID_OUTPUT_CONNECTION,
  157. `No output found for port connection: ${inputSnippet}`,
  158. parentNodeX.name,
  159. );
  160. return;
  161. }
  162. const inputType = inputNodeX.type;
  163. if ( inputType && outputType && typesCompatible( inputType, outputType ) === false ) {
  164. log.add(
  165. MaterialXLogCodes.TYPE_MISMATCH,
  166. `Mismatched types in port connection: ${inputSnippet}`,
  167. parentNodeX.name,
  168. );
  169. }
  170. }
  171. function validateNodeInputs( nodeX, log ) {
  172. if ( nodeX.element === 'materialx' ) {
  173. for ( const child of nodeX.children ) {
  174. validateNodeInputs( child, log );
  175. }
  176. return;
  177. }
  178. if ( SKIP_ELEMENTS.has( nodeX.element ) ) return;
  179. if ( CONTAINER_ELEMENTS.has( nodeX.element ) ) {
  180. for ( const child of nodeX.children ) {
  181. validateNodeInputs( child, log );
  182. }
  183. return;
  184. }
  185. const nodeInterface = resolveInterface( nodeX );
  186. if ( nodeInterface ) {
  187. for ( const child of nodeX.children ) {
  188. if ( child.element !== 'input' ) continue;
  189. const inputName = child.name;
  190. if ( ! inputName ) continue;
  191. if ( nodeInterface.inputs[ inputName ] === undefined ) {
  192. log.add(
  193. MaterialXLogCodes.UNKNOWN_INPUT,
  194. `Node interface error: Input '${inputName}' doesn't match declaration: ${formatNodeElement( nodeX )}`,
  195. nodeX.name,
  196. );
  197. } else if (
  198. child.type &&
  199. nodeInterface.inputs[ inputName ] &&
  200. child.hasReference === false &&
  201. typesCompatible( nodeInterface.inputs[ inputName ], child.type ) === false
  202. ) {
  203. log.add(
  204. MaterialXLogCodes.TYPE_MISMATCH,
  205. `Input '${inputName}' type '${child.type}' doesn't match nodedef type '${nodeInterface.inputs[ inputName ]}' on ${formatNodeElement( nodeX )}`,
  206. nodeX.name,
  207. );
  208. }
  209. if ( child.hasReference ) {
  210. validatePortConnection( child, nodeX, log );
  211. }
  212. }
  213. } else if ( MtlXLibrary[ nodeX.element ] === undefined && nodeX.element !== 'surfacematerial' ) {
  214. // Defer unsupported-node warnings to compile-time behavior.
  215. for ( const child of nodeX.children ) {
  216. if ( child.element === 'input' && child.hasReference ) {
  217. validatePortConnection( child, nodeX, log );
  218. }
  219. }
  220. }
  221. for ( const child of nodeX.children ) {
  222. if ( child.element !== 'input' && child.element !== 'output' ) {
  223. validateNodeInputs( child, log );
  224. }
  225. }
  226. }
  227. function createStrictInterfaceValidator() {
  228. return function validateMaterialXInterfaces( rootNode, log ) {
  229. validateNodeInputs( rootNode, log );
  230. };
  231. }
  232. export { createStrictInterfaceValidator };
粤ICP备19079148号