@threlte/gltf 3.0.1 → 3.0.2
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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -5
- package/cli.js +37 -36
- package/package.json +26 -28
- package/src/bin/DRACOLoader.js +43 -40
- package/src/bin/GLTFLoader.js +2212 -2287
- package/src/index.js +46 -74
- package/src/utils/exports.js +1 -1
- package/src/utils/isVarName.js +1 -3
- package/src/utils/parser.js +56 -40
- package/src/utils/transform.js +78 -17
- package/tsconfig.json +30 -0
- package/src/utils/glftLoader.js +0 -4
package/src/index.js
CHANGED
|
@@ -1,82 +1,54 @@
|
|
|
1
1
|
import 'jsdom-global'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import transform from './utils/transform.js'
|
|
5
|
-
// import { GLTFLoader, DRACOLoader } from 'three-stdlib'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
6
4
|
import * as prettier from 'prettier'
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
for (var i = 0; i < buf.length; ++i) view[i] = buf[i]
|
|
24
|
-
return ab
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default function (file, output, options) {
|
|
28
|
-
function getFilePath(file) {
|
|
29
|
-
return `${options.root ?? '/'}${options.root ? path.basename(file) : path.normalize(file)}`
|
|
5
|
+
import { transform } from './utils/transform.js'
|
|
6
|
+
import { parse } from './utils/parser.js'
|
|
7
|
+
import { GLTFLoader } from './bin/GLTFLoader.js'
|
|
8
|
+
import { DRACOLoader } from './bin/DRACOLoader.js'
|
|
9
|
+
|
|
10
|
+
globalThis.self = globalThis
|
|
11
|
+
|
|
12
|
+
const gltfLoader = new GLTFLoader()
|
|
13
|
+
const draco = new DRACOLoader()
|
|
14
|
+
gltfLoader.setDRACOLoader(draco)
|
|
15
|
+
|
|
16
|
+
export default async function (file, output, options) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.stat(file)
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(`${file} does not exist.`)
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (options.transform || options.instance || options.instanceall) {
|
|
40
|
-
const { name } = path.parse(file)
|
|
41
|
-
const transformOut = path.join(name + '-transformed.glb')
|
|
42
|
-
await transform(file, transformOut, options)
|
|
43
|
-
file = transformOut
|
|
44
|
-
}
|
|
45
|
-
resolve()
|
|
46
|
-
|
|
47
|
-
const filePath = getFilePath(file)
|
|
48
|
-
const data = fs.readFileSync(file)
|
|
49
|
-
const arrayBuffer = toArrayBuffer(data)
|
|
23
|
+
// Process GLTF
|
|
24
|
+
if (options.transform || options.instance || options.instanceall) {
|
|
25
|
+
const { name } = path.parse(file)
|
|
26
|
+
const transformOut = path.join(`${name}-transformed.glb`)
|
|
27
|
+
await transform(file, transformOut, options)
|
|
28
|
+
file = transformOut
|
|
29
|
+
}
|
|
50
30
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
stream.end()
|
|
69
|
-
resolve()
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error(error)
|
|
72
|
-
stream.write(raw)
|
|
73
|
-
stream.end()
|
|
74
|
-
reject(error)
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
reject
|
|
78
|
-
)
|
|
31
|
+
const filePath = `${options.root ?? '/'}${options.root ? path.basename(file) : path.normalize(file)}`
|
|
32
|
+
const data = await fs.readFile(file)
|
|
33
|
+
const gltf = await gltfLoader.parseAsync(data.buffer)
|
|
34
|
+
|
|
35
|
+
const raw = parse(filePath, gltf, options)
|
|
36
|
+
|
|
37
|
+
const prettiered = await prettier.format(raw, {
|
|
38
|
+
singleQuote: true,
|
|
39
|
+
trailingComma: 'es5',
|
|
40
|
+
semi: false,
|
|
41
|
+
printWidth: 100,
|
|
42
|
+
parser: 'svelte',
|
|
43
|
+
plugins: ['prettier-plugin-svelte'],
|
|
44
|
+
overrides: [
|
|
45
|
+
{
|
|
46
|
+
files: '*.svelte',
|
|
47
|
+
options: { parser: 'svelte' }
|
|
79
48
|
}
|
|
80
|
-
|
|
49
|
+
],
|
|
50
|
+
singleAttributePerLine: true
|
|
81
51
|
})
|
|
52
|
+
|
|
53
|
+
await fs.writeFile(output, prettiered)
|
|
82
54
|
}
|
package/src/utils/exports.js
CHANGED
package/src/utils/isVarName.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export function isVarName(str) {
|
|
2
2
|
// eslint-disable-next-line no-misleading-character-class
|
|
3
3
|
const regex = new RegExp(
|
|
4
4
|
// eslint-disable-next-line no-misleading-character-class
|
|
@@ -6,5 +6,3 @@ const isVarName = (str) => {
|
|
|
6
6
|
)
|
|
7
7
|
return regex.test(str)
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
export default isVarName
|
package/src/utils/parser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import THREE from 'three'
|
|
2
|
-
import isVarName from './isVarName.js'
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { isVarName } from './isVarName.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {string} fileName
|
|
@@ -7,7 +7,12 @@ import isVarName from './isVarName.js'
|
|
|
7
7
|
* @param {import('./Options.d.ts').Options} options
|
|
8
8
|
* @returns {string}
|
|
9
9
|
*/
|
|
10
|
-
function parse(fileName, gltf, options = {}) {
|
|
10
|
+
export function parse(fileName, gltf, options = {}) {
|
|
11
|
+
if (gltf.isObject3D) {
|
|
12
|
+
// Wrap scene in a GLTF Structure
|
|
13
|
+
gltf = { scene: gltf, animations: [], parser: { json: {} } }
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const url = fileName
|
|
12
17
|
const animations = gltf.animations
|
|
13
18
|
const hasAnimations = animations.length > 0
|
|
@@ -25,38 +30,40 @@ function parse(fileName, gltf, options = {}) {
|
|
|
25
30
|
|
|
26
31
|
function uniqueName(attempt, index = 0) {
|
|
27
32
|
const newAttempt = index > 0 ? attempt + index : attempt
|
|
28
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
Object.values(duplicates.geometries).find(({ name }) => name === newAttempt) === undefined
|
|
35
|
+
) {
|
|
29
36
|
return newAttempt
|
|
30
|
-
else
|
|
37
|
+
} else {
|
|
38
|
+
return uniqueName(attempt, index + 1)
|
|
39
|
+
}
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
gltf.scene.traverse((child) => {
|
|
34
|
-
if (child.isMesh) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
if (!child.isMesh) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (child.material) {
|
|
48
|
+
if (!duplicates.materials[child.material.name]) {
|
|
49
|
+
duplicates.materials[child.material.name] = 1
|
|
50
|
+
} else {
|
|
51
|
+
duplicates.materials[child.material.name]++
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
|
-
})
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
name: uniqueName(name),
|
|
55
|
-
node: 'nodes' + sanitizeName(child.name)
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
duplicates.geometries[key].count++
|
|
55
|
+
if (child.geometry) {
|
|
56
|
+
const key = child.geometry.uuid + child.material?.name ?? ''
|
|
57
|
+
if (!duplicates.geometries[key]) {
|
|
58
|
+
let name = (child.name || 'Part').replace(/[^a-zA-Z]/g, '')
|
|
59
|
+
name = name.charAt(0).toUpperCase() + name.slice(1)
|
|
60
|
+
duplicates.geometries[key] = {
|
|
61
|
+
count: 1,
|
|
62
|
+
name: uniqueName(name),
|
|
63
|
+
node: 'nodes' + sanitizeName(child.name)
|
|
59
64
|
}
|
|
65
|
+
} else {
|
|
66
|
+
duplicates.geometries[key].count++
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
})
|
|
@@ -64,7 +71,9 @@ function parse(fileName, gltf, options = {}) {
|
|
|
64
71
|
// Prune duplicate geometries
|
|
65
72
|
for (let key of Object.keys(duplicates.geometries)) {
|
|
66
73
|
const duplicate = duplicates.geometries[key]
|
|
67
|
-
if (duplicate.count === 1)
|
|
74
|
+
if (duplicate.count === 1) {
|
|
75
|
+
delete duplicates.geometries[key]
|
|
76
|
+
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
function sanitizeName(name) {
|
|
@@ -72,17 +81,17 @@ function parse(fileName, gltf, options = {}) {
|
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
const rNbr = (number) => {
|
|
75
|
-
return parseFloat(number.toFixed(Math.round(options.precision || 2)))
|
|
84
|
+
return Number.parseFloat(number.toFixed(Math.round(options.precision || 2)))
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
const rDeg = (number) => {
|
|
79
|
-
const abs = Math.abs(Math.round(parseFloat(number) * 100000))
|
|
88
|
+
const abs = Math.abs(Math.round(Number.parseFloat(number) * 100000))
|
|
80
89
|
for (let i = 1; i <= 10; i++) {
|
|
81
|
-
if (abs === Math.round(parseFloat(Math.PI / i) * 100000))
|
|
90
|
+
if (abs === Math.round(Number.parseFloat(Math.PI / i) * 100000))
|
|
82
91
|
return `${number < 0 ? '-' : ''}Math.PI${i > 1 ? ' / ' + i : ''}`
|
|
83
92
|
}
|
|
84
93
|
for (let i = 1; i <= 10; i++) {
|
|
85
|
-
if (abs === Math.round(parseFloat(Math.PI * i) * 100000))
|
|
94
|
+
if (abs === Math.round(Number.parseFloat(Math.PI * i) * 100000))
|
|
86
95
|
return `${number < 0 ? '-' : ''}Math.PI${i > 1 ? ' * ' + i : ''}`
|
|
87
96
|
}
|
|
88
97
|
return rNbr(number)
|
|
@@ -125,10 +134,20 @@ function parse(fileName, gltf, options = {}) {
|
|
|
125
134
|
|
|
126
135
|
function getType(obj) {
|
|
127
136
|
let type = obj.type.charAt(0).toLowerCase() + obj.type.slice(1)
|
|
137
|
+
|
|
128
138
|
// Turn object3d's into groups, it should be faster according to the threejs docs
|
|
129
|
-
if (type === 'object3D')
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
if (type === 'object3D') {
|
|
140
|
+
return 'group'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (type === 'perspectiveCamera') {
|
|
144
|
+
return 'PerspectiveCamera'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (type === 'orthographicCamera') {
|
|
148
|
+
return 'OrthographicCamera'
|
|
149
|
+
}
|
|
150
|
+
|
|
132
151
|
return type
|
|
133
152
|
}
|
|
134
153
|
|
|
@@ -261,7 +280,6 @@ function parse(fileName, gltf, options = {}) {
|
|
|
261
280
|
const firstProps = handleThrelteProps(first)
|
|
262
281
|
const regex = /([a-z-A-Z]*)={([a-zA-Z0-9\.\[\]\-\,\ \/]*)}/g
|
|
263
282
|
const keys1 = [...result.matchAll(regex)].map(([, match]) => match)
|
|
264
|
-
const values1 = [...result.matchAll(regex)].map(([, , match]) => match)
|
|
265
283
|
const keys2 = [...firstProps.matchAll(regex)].map(([, match]) => match)
|
|
266
284
|
|
|
267
285
|
/** Double negative rotations
|
|
@@ -467,7 +485,7 @@ function parse(fileName, gltf, options = {}) {
|
|
|
467
485
|
const scene = printThrelte(gltf.scene)
|
|
468
486
|
|
|
469
487
|
const useGltfOptions = {
|
|
470
|
-
draco: options.transform
|
|
488
|
+
draco: options.transform || options.draco
|
|
471
489
|
}
|
|
472
490
|
|
|
473
491
|
const imports = `
|
|
@@ -489,7 +507,7 @@ function parse(fileName, gltf, options = {}) {
|
|
|
489
507
|
|
|
490
508
|
const useGltf = `${options.suspense ? 'suspend(' : ''}useGltf${
|
|
491
509
|
options.types ? '<GLTFResult>' : ''
|
|
492
|
-
}('${url}'${useGltfOptions.draco ? `, { dracoLoader: useDraco(${typeof
|
|
510
|
+
}('${url}'${useGltfOptions.draco ? `, { dracoLoader: useDraco(${typeof options.draco === 'string' ? `'${useGltfOptions.draco}'` : ''}) }` : ''})${
|
|
493
511
|
options.suspense ? ')' : ''
|
|
494
512
|
}`
|
|
495
513
|
|
|
@@ -575,5 +593,3 @@ ${
|
|
|
575
593
|
</${hasAnimations ? 'T' : 'T.Group'}>
|
|
576
594
|
`
|
|
577
595
|
}
|
|
578
|
-
|
|
579
|
-
export default parse
|
package/src/utils/transform.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import { NodeIO } from '@gltf-transform/core'
|
|
1
|
+
import { Logger, NodeIO } from '@gltf-transform/core'
|
|
2
2
|
import {
|
|
3
|
+
reorder,
|
|
4
|
+
palette,
|
|
3
5
|
simplify,
|
|
6
|
+
join,
|
|
4
7
|
weld,
|
|
5
8
|
dedup,
|
|
6
9
|
resample,
|
|
7
10
|
prune,
|
|
8
11
|
textureCompress,
|
|
9
|
-
draco
|
|
12
|
+
draco,
|
|
13
|
+
sparse,
|
|
14
|
+
flatten
|
|
10
15
|
} from '@gltf-transform/functions'
|
|
11
16
|
import { ALL_EXTENSIONS } from '@gltf-transform/extensions'
|
|
12
17
|
import { MeshoptDecoder, MeshoptEncoder, MeshoptSimplifier } from 'meshoptimizer'
|
|
18
|
+
import { ready as resampleReady, resample as resampleWASM } from 'keyframe-resample'
|
|
13
19
|
import draco3d from 'draco3dgltf'
|
|
14
20
|
import sharp from 'sharp'
|
|
15
21
|
|
|
16
|
-
async function transform(file, output, config = {}) {
|
|
22
|
+
export async function transform(file, output, config = {}) {
|
|
17
23
|
await MeshoptDecoder.ready
|
|
18
24
|
await MeshoptEncoder.ready
|
|
19
25
|
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({
|
|
@@ -22,27 +28,37 @@ async function transform(file, output, config = {}) {
|
|
|
22
28
|
'meshopt.decoder': MeshoptDecoder,
|
|
23
29
|
'meshopt.encoder': MeshoptEncoder
|
|
24
30
|
})
|
|
31
|
+
if (config.console) {
|
|
32
|
+
io.setLogger(new Logger(Logger.Verbosity.ERROR))
|
|
33
|
+
} else {
|
|
34
|
+
io.setLogger(new Logger(Logger.Verbosity.WARN))
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
const document = await io.read(file)
|
|
27
38
|
const resolution = config.resolution ?? 1024
|
|
39
|
+
const normalResolution = Math.max(resolution, 2048)
|
|
28
40
|
|
|
29
|
-
const functions = [
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
const functions = []
|
|
42
|
+
|
|
43
|
+
if (!config.keepmaterials) {
|
|
44
|
+
functions.push(palette({ min: 5 }))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
functions.push(
|
|
48
|
+
reorder({ encoder: MeshoptEncoder }),
|
|
32
49
|
// Remove duplicate vertex or texture data, if any.
|
|
33
50
|
dedup(),
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
flatten()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (!config.keepmeshes) {
|
|
55
|
+
functions.push(join())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
functions.push(weld())
|
|
41
59
|
|
|
42
60
|
if (config.simplify) {
|
|
43
61
|
functions.push(
|
|
44
|
-
// Weld vertices
|
|
45
|
-
weld({ tolerance: config.weld ?? 0.0001 }),
|
|
46
62
|
// Simplify meshes
|
|
47
63
|
simplify({
|
|
48
64
|
simplifier: MeshoptSimplifier,
|
|
@@ -52,9 +68,54 @@ async function transform(file, output, config = {}) {
|
|
|
52
68
|
)
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
functions.push(
|
|
72
|
+
// Losslessly resample animation frames.
|
|
73
|
+
resample({ ready: resampleReady, resample: resampleWASM }),
|
|
74
|
+
// Remove unused nodes, textures, or other data.
|
|
75
|
+
prune({ keepAttributes: false, keepLeaves: false }),
|
|
76
|
+
sparse()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (config.degrade) {
|
|
80
|
+
// Custom per-file resolution
|
|
81
|
+
functions.push(
|
|
82
|
+
textureCompress({
|
|
83
|
+
encoder: sharp,
|
|
84
|
+
pattern: new RegExp(`^(?=${config.degrade}).*$`),
|
|
85
|
+
targetFormat: config.format,
|
|
86
|
+
resize: [degradeResolution, degradeResolution]
|
|
87
|
+
}),
|
|
88
|
+
textureCompress({
|
|
89
|
+
encoder: sharp,
|
|
90
|
+
pattern: new RegExp(`^(?!${config.degrade}).*$`),
|
|
91
|
+
targetFormat: config.format,
|
|
92
|
+
resize: [resolution, resolution]
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
} else {
|
|
96
|
+
// Keep normal maps near lossless
|
|
97
|
+
functions.push(
|
|
98
|
+
textureCompress({
|
|
99
|
+
slots: /^(?!normalTexture).*$/, // exclude normal maps
|
|
100
|
+
encoder: sharp,
|
|
101
|
+
targetFormat: config.format,
|
|
102
|
+
resize: [resolution, resolution]
|
|
103
|
+
}),
|
|
104
|
+
textureCompress({
|
|
105
|
+
slots: /^(?=normalTexture).*$/, // include normal maps
|
|
106
|
+
encoder: sharp,
|
|
107
|
+
targetFormat: 'jpeg',
|
|
108
|
+
resize: [normalResolution, normalResolution]
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
functions.push(
|
|
114
|
+
// Add Draco compression.
|
|
115
|
+
draco()
|
|
116
|
+
)
|
|
117
|
+
|
|
55
118
|
await document.transform(...functions)
|
|
56
119
|
|
|
57
120
|
await io.write(output, document)
|
|
58
121
|
}
|
|
59
|
-
|
|
60
|
-
export default transform
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// ESM out, modern syntax in source
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
|
|
8
|
+
// Stricter, modern TS defaults
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noImplicitOverride": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"useDefineForClassFields": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"exactOptionalPropertyTypes": true,
|
|
15
|
+
|
|
16
|
+
// Type ergonomics
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
// Declarations: keep off in the main tsconfig; use a build tsconfig to emit types
|
|
22
|
+
"declarationMap": false,
|
|
23
|
+
"allowJs": true,
|
|
24
|
+
|
|
25
|
+
// Svelte types
|
|
26
|
+
"types": ["svelte"]
|
|
27
|
+
},
|
|
28
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.js"],
|
|
29
|
+
"exclude": ["dist", "node_modules"]
|
|
30
|
+
}
|
package/src/utils/glftLoader.js
DELETED