@@ -13,9 +13,23 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
13
13
const animations = gltf . animations
14
14
const hasAnimations = animations . length > 0
15
15
16
+ /** @type {Record<string, Object3D[]> */
17
+ const slots = { }
18
+
16
19
// Collect all objects
17
20
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
+ } )
19
33
20
34
// Browse for duplicates
21
35
const duplicates = {
@@ -75,6 +89,11 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
75
89
return isVarName ( name ) ? `.${ name } ` : `['${ name } ']`
76
90
}
77
91
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
+
78
97
const rNbr = ( number ) => {
79
98
return parseFloat ( number . toFixed ( Math . round ( options . precision || 2 ) ) )
80
99
}
@@ -221,17 +240,18 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
221
240
duplicates . geometries [ obj . geometry . uuid + obj . material . name ] &&
222
241
duplicates . geometries [ obj . geometry . uuid + obj . material . name ] . count > ( options . instanceall ? 0 : 1 )
223
242
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 }
225
245
}
226
246
227
247
function equalOrNegated ( a , b ) {
228
248
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 )
229
249
}
230
250
231
251
function prune ( obj , children , result , oldResult , silent ) {
232
- let { type, animated } = getInfo ( obj )
252
+ let { type, animated, hasSlots } = getInfo ( obj )
233
253
// Prune ...
234
- if ( ! obj . __removed && ! options . keepgroups && ! animated && ( type === 'group' || type === 'scene' ) ) {
254
+ if ( ! obj . __removed && ! options . keepgroups && ! animated && ! hasSlots && ( type === 'group' || type === 'scene' ) ) {
235
255
/** Empty or no-property groups
236
256
* <group>
237
257
* <mesh geometry={nodes.foo} material={materials.bar} />
@@ -380,13 +400,24 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
380
400
// Bail out if the object was pruned
381
401
if ( pruned !== undefined ) return pruned
382
402
383
- // Close tag
384
- result += `${ children . length ? '>' : '/>' } \n`
403
+ // Add custom slots if defined in the object's userData
404
+ // E.g. userData: { "prop" : "mySlot" } becomes `{ mySlot }`
405
+ const slot = obj . userData ?. prop ;
406
+ const hasSlot = ( slot && typeof slot === "string" && slot . length > 0 ) ;
407
+ const hasContent = children . length || hasSlot ;
385
408
386
- // Add children and return
387
- if ( children . length ) {
388
- if ( type === 'bone' ) result += children + `</primitive>`
389
- else result += children + `</${ type } >`
409
+
410
+ // Close tag if no children
411
+ result += `${ hasContent ? '>' : '/>' } \n`
412
+
413
+ // Add children
414
+ if ( children . length ) result += `${ children . trimEnd ( "\n" ) } \n`
415
+ // Add custom slot
416
+ if ( hasSlot ) result += `{${ sanitizeSlotName ( slot ) } }\n` ;
417
+ // Close tag
418
+ if ( hasContent ) {
419
+ if ( type === 'bone' ) result += `</primitive>`
420
+ else result += `</${ type } >`
390
421
}
391
422
return result
392
423
}
@@ -450,10 +481,14 @@ function parse(gltf, { fileName = 'model', ...options } = {}) {
450
481
} catch ( e ) {
451
482
console . log ( 'Error while parsing glTF' , e )
452
483
}
484
+
485
+ const slotParams = Object . keys ( slots ) . length > 0 ? ( Object . keys ( slots ) . join ( ", " ) + ", " ) : "" ;
486
+
453
487
const header = `/*
454
488
${ options . header ? options . header : 'Auto-generated by: https://github.com/pmndrs/gltfjsx' } ${
455
489
options . size ? `\nFiles: ${ options . size } ` : ''
456
490
}
491
+
457
492
${ parseExtras ( gltf . parser . json . asset && gltf . parser . json . asset . extras ) } */`
458
493
const hasPrimitives = scene . includes ( '<primitive' )
459
494
const result = `${ options . types ? `\nimport * as THREE from 'three'` : '' }
@@ -475,7 +510,7 @@ ${parseExtras(gltf.parser.json.asset && gltf.parser.json.asset.extras)}*/`
475
510
? `
476
511
const context = React.createContext(${ options . types ? '{} as ContextType' : '' } )
477
512
478
- export function Instances({ children, ...props }${ options . types ? ': JSX.IntrinsicElements["group"]' : '' } ) {
513
+ export function Instances({ children, ${ slotParams } ...props }${ options . types ? ': JSX.IntrinsicElements["group"]' : '' } ) {
479
514
const { nodes } = useGLTF('${ url } '${ options . draco ? `, ${ JSON . stringify ( options . draco ) } ` : '' } )${
480
515
options . types ? ' as GLTFResult' : ''
481
516
}
@@ -496,7 +531,7 @@ ${parseExtras(gltf.parser.json.asset && gltf.parser.json.asset.extras)}*/`
496
531
: ''
497
532
}
498
533
499
- export ${ options . exportdefault ? 'default' : '' } function Model(props${
534
+ export ${ options . exportdefault ? 'default' : '' } function Model({ ${ slotParams } ... props } ${
500
535
options . types ? ": JSX.IntrinsicElements['group']" : ''
501
536
} ) {
502
537
${ hasInstances ? 'const instances = React.useContext(context);' : '' } ${
0 commit comments