@@ -13,9 +13,23 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
1313 const animations = gltf . animations
1414 const hasAnimations = animations . length > 0
1515
16+ /** @type {Record<string, Object3D[]> */
17+ const slots = { }
18+
1619 // Collect all objects
1720 const objects = [ ]
18- gltf . scene . traverse ( ( child ) => objects . push ( child ) )
21+ gltf . scene . traverse ( ( child ) => {
22+ objects . push ( child ) ;
23+
24+ // Collect slots
25+ const slot = child . userData ?. prop ;
26+ const hasSlot = ( slot && typeof slot === "string" && slot . length > 0 ) ;
27+ if ( hasSlot )
28+ {
29+ const slotname = sanitizeSlotName ( slot ) ;
30+ slots [ slotname ] ? slots [ slotname ] . push ( child ) : ( slots [ slotname ] = [ child ] ) ;
31+ }
32+ } )
1933
2034 // Browse for duplicates
2135 const duplicates = {
@@ -75,6 +89,11 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
7589 return isVarName ( name ) ? `.${ name } ` : `['${ name } ']`
7690 }
7791
92+ /** Ensure that a slot is a valid variable name e.g. must not contain spaces */
93+ function sanitizeSlotName ( slotname ) {
94+ return slotname . replaceAll ( / [ ^ a - z A - Z 0 - 9 ] / g, '' ) ;
95+ }
96+
7897 const rNbr = ( number ) => {
7998 return parseFloat ( number . toFixed ( Math . round ( options . precision || 2 ) ) )
8099 }
@@ -221,17 +240,18 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
221240 duplicates . geometries [ obj . geometry . uuid + obj . material . name ] &&
222241 duplicates . geometries [ obj . geometry . uuid + obj . material . name ] . count > ( options . instanceall ? 0 : 1 )
223242 let animated = gltf . animations && gltf . animations . length > 0
224- return { type, node, instanced, animated }
243+ const hasSlots = obj . userData ?. prop && typeof obj . userData . prop === "string" && obj . userData . prop . length > 0 ;
244+ return { type, node, instanced, animated, hasSlots }
225245 }
226246
227247 function equalOrNegated ( a , b ) {
228248 return ( a . x === b . x || a . x === - b . x ) && ( a . y === b . y || a . y === - b . y ) && ( a . z === b . z || a . z === - b . z )
229249 }
230250
231251 function prune ( obj , children , result , oldResult , silent ) {
232- let { type, animated } = getInfo ( obj )
252+ let { type, animated, hasSlots } = getInfo ( obj )
233253 // Prune ...
234- if ( ! obj . __removed && ! options . keepgroups && ! animated && ( type === 'group' || type === 'scene' ) ) {
254+ if ( ! obj . __removed && ! options . keepgroups && ! animated && ! hasSlots && ( type === 'group' || type === 'scene' ) ) {
235255 /** Empty or no-property groups
236256 * <group>
237257 * <mesh geometry={nodes.foo} material={materials.bar} />
@@ -379,13 +399,24 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
379399 // Bail out if the object was pruned
380400 if ( pruned !== undefined ) return pruned
381401
382- // Close tag
383- result += `${ children . length ? '>' : '/>' } \n`
402+ // Add custom slots if defined in the object's userData
403+ // E.g. userData: { "prop" : "mySlot" } becomes `{ mySlot }`
404+ const slot = obj . userData ?. prop ;
405+ const hasSlot = ( slot && typeof slot === "string" && slot . length > 0 ) ;
406+ const hasContent = children . length || hasSlot ;
384407
385- // Add children and return
386- if ( children . length ) {
387- if ( type === 'bone' ) result += children + `</primitive>`
388- else result += children + `</${ type } >`
408+
409+ // Close tag if no children
410+ result += `${ hasContent ? '>' : '/>' } \n`
411+
412+ // Add children
413+ if ( children . length ) result += `${ children . trimEnd ( "\n" ) } \n`
414+ // Add custom slot
415+ if ( hasSlot ) result += `{${ sanitizeSlotName ( slot ) } }\n` ;
416+ // Close tag
417+ if ( hasContent ) {
418+ if ( type === 'bone' ) result += `</primitive>`
419+ else result += `</${ type } >`
389420 }
390421 return result
391422 }
@@ -449,10 +480,14 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
449480 } catch ( e ) {
450481 console . log ( 'Error while parsing glTF' , e )
451482 }
483+
484+ const slotParams = Object . keys ( slots ) . length > 0 ? ( Object . keys ( slots ) . join ( ", " ) + ", " ) : "" ;
485+
452486 const header = `/*
453487${ options . header ? options . header : 'Auto-generated by: https://github.com/pmndrs/gltfjsx' } ${
454488 options . size ? `\nFiles: ${ options . size } ` : ''
455489 }
490+
456491${ parseExtras ( gltf . parser . json . asset && gltf . parser . json . asset . extras ) } */`
457492 const hasPrimitives = scene . includes ( '<primitive' )
458493 const result = `${ options . types ? `\nimport * as THREE from 'three'` : '' }
@@ -474,7 +509,7 @@ ${parseExtras(gltf.parser.json.asset && gltf.parser.json.asset.extras)}*/`
474509 ? `
475510 const context = React.createContext(${ options . types ? '{} as ContextType' : '' } )
476511
477- export function Instances({ children, ...props }${ options . types ? ': JSX.IntrinsicElements["group"]' : '' } ) {
512+ export function Instances({ children, ${ slotParams } ...props }${ options . types ? ': JSX.IntrinsicElements["group"]' : '' } ) {
478513 const { nodes } = useGLTF('${ url } '${ options . draco ? `, ${ JSON . stringify ( options . draco ) } ` : '' } )${
479514 options . types ? ' as GLTFResult' : ''
480515 }
@@ -495,7 +530,7 @@ ${parseExtras(gltf.parser.json.asset && gltf.parser.json.asset.extras)}*/`
495530 : ''
496531 }
497532
498- export ${ options . exportdefault ? 'default' : '' } function Model(props${
533+ export ${ options . exportdefault ? 'default' : '' } function Model({ ${ slotParams } ... props } ${
499534 options . types ? ": JSX.IntrinsicElements['group']" : ''
500535 } ) {
501536 ${ hasInstances ? 'const instances = React.useContext(context);' : '' } ${
0 commit comments