| "rimraf": "^5.0.1", | "rimraf": "^5.0.1", | ||||
| "rollup-plugin-glsl": "^1.3.0", | "rollup-plugin-glsl": "^1.3.0", | ||||
| "rollup-plugin-license": "^3.0.1", | "rollup-plugin-license": "^3.0.1", | ||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1004/package.tgz", | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1007/package.tgz", | |||||
| "tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
| "typedoc": "^0.27.5", | "typedoc": "^0.27.5", | ||||
| "typescript": "5.7.2", | "typescript": "5.7.2", | ||||
| "vitepress-plugin-nprogress": "^0.0.4" | "vitepress-plugin-nprogress": "^0.0.4" | ||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1004/package.tgz", | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1005/package.tgz", | |||||
| "@types/webxr": "^0.5.1", | "@types/webxr": "^0.5.1", | ||||
| "@types/wicg-file-system-access": "^2020.9.5", | "@types/wicg-file-system-access": "^2020.9.5", | ||||
| "stats.js": "^0.17.0", | "stats.js": "^0.17.0", | ||||
| "popmotion": "^11.0.5" | "popmotion": "^11.0.5" | ||||
| }, | }, | ||||
| "peerDependencies": { | "peerDependencies": { | ||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1004/package.tgz" | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1007/package.tgz" | |||||
| }, | }, | ||||
| "peerDependenciesMeta": { | "peerDependenciesMeta": { | ||||
| "three": { | "three": { | ||||
| "dependencies": { | "dependencies": { | ||||
| "uiconfig.js": "^0.1.3", | "uiconfig.js": "^0.1.3", | ||||
| "ts-browser-helpers": "^0.16.2", | "ts-browser-helpers": "^0.16.2", | ||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1004/package.tgz", | |||||
| "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.157.1004.tar.gz", | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1004/package.tgz", | |||||
| "@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.157.1004.tar.gz", | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1007/package.tgz", | |||||
| "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.157.1007.tar.gz", | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1005/package.tgz", | |||||
| "@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.157.1005.tar.gz", | |||||
| "@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three" | "@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three" | ||||
| }, | }, | ||||
| "local_dependencies": { | "local_dependencies": { |
| private _fileDatabase: Map<string, IFile> = new Map<string, IFile>() | private _fileDatabase: Map<string, IFile> = new Map<string, IFile>() | ||||
| private _cachedAssets: IAsset[] = [] | private _cachedAssets: IAsset[] = [] | ||||
| static WHITE_IMAGE_DATA = new ImageData(new Uint8ClampedArray([255, 255, 255, 255]), 1, 1) | |||||
| readonly importers: IImporter[] = [ | readonly importers: IImporter[] = [ | ||||
| // new Importer(VideoTextureLoader, ['mp4', 'ogg', 'mov', 'data:video'], false), | // new Importer(VideoTextureLoader, ['mp4', 'ogg', 'mov', 'data:video'], false), | ||||
| new Importer(SimpleJSONLoader, ['json', 'vjson'], ['application/json'], false), | new Importer(SimpleJSONLoader, ['json', 'vjson'], ['application/json'], false), |
| } from '../core' | } from '../core' | ||||
| import {Importer} from './Importer' | import {Importer} from './Importer' | ||||
| import {MaterialManager} from './MaterialManager' | import {MaterialManager} from './MaterialManager' | ||||
| import {DRACOLoader2, GLTFLoader2, JSONMaterialLoader, MTLLoader2, OBJLoader2, ZipLoader} from './import' | |||||
| import { | |||||
| DRACOLoader2, | |||||
| FBXLoader2, | |||||
| GLTFLoader2, | |||||
| JSONMaterialLoader, | |||||
| MTLLoader2, | |||||
| OBJLoader2, | |||||
| SVGTextureLoader, | |||||
| ZipLoader, | |||||
| } from './import' | |||||
| import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader.js' | import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader.js' | ||||
| import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader.js' | |||||
| import {EXRLoader} from 'three/examples/jsm/loaders/EXRLoader.js' | import {EXRLoader} from 'three/examples/jsm/loaders/EXRLoader.js' | ||||
| import {Class, ValOrArr} from 'ts-browser-helpers' | import {Class, ValOrArr} from 'ts-browser-helpers' | ||||
| import {ILoader} from './IImporter' | import {ILoader} from './IImporter' | ||||
| const viewer = this.viewer | const viewer = this.viewer | ||||
| if (!viewer) return | if (!viewer) return | ||||
| // todo fix - loading manager getHandler matches backwards? | |||||
| const importers: Importer[] = [ | const importers: Importer[] = [ | ||||
| new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'svg', 'ico', 'data:image', 'avif'], [ | |||||
| 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif', | |||||
| new Importer(SVGTextureLoader, ['svg', 'data:image/svg'], ['image/svg+xml'], false), // todo: use ImageBitmapLoader if supported (better performance) | |||||
| new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'ico', 'data:image', 'avif'], [ | |||||
| 'image/webp', 'image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif', | |||||
| ], false), // todo: use ImageBitmapLoader if supported (better performance) | ], false), // todo: use ImageBitmapLoader if supported (better performance) | ||||
| new Importer<JSONMaterialLoader>(JSONMaterialLoader, | new Importer<JSONMaterialLoader>(JSONMaterialLoader, | ||||
| } | } | ||||
| }, ['exr'], ['image/x-exr'], false), | }, ['exr'], ['image/x-exr'], false), | ||||
| new Importer(FBXLoader, ['fbx'], ['model/fbx'], true), | |||||
| new Importer(FBXLoader2, ['fbx'], ['model/fbx'], true), | |||||
| new Importer(ZipLoader, ['zip', 'glbz', 'gltfz'], ['application/zip', 'model/gltf+zip', 'model/zip'], true), // gltfz and glbz are invented zip files with gltf/glb inside along with resources | new Importer(ZipLoader, ['zip', 'glbz', 'gltfz'], ['application/zip', 'model/gltf+zip', 'model/zip'], true), // gltfz and glbz are invented zip files with gltf/glb inside along with resources | ||||
| new Importer(OBJLoader2 as any as Class<ILoader>, ['obj'], ['model/obj'], true), | new Importer(OBJLoader2 as any as Class<ILoader>, ['obj'], ['model/obj'], true), |
| maxTextureSize: options.maxTextureSize ?? Infinity, | maxTextureSize: options.maxTextureSize ?? Infinity, | ||||
| animations: options.animations ?? [], | animations: options.animations ?? [], | ||||
| includeCustomExtensions: options.includeCustomExtensions ?? true, | includeCustomExtensions: options.includeCustomExtensions ?? true, | ||||
| forceIndices: options.forceIndices ?? false, | |||||
| forceIndices: options.forceIndices ?? false, // todo implement | |||||
| exporterOptions: options, | exporterOptions: options, | ||||
| ignoreInvalidMorphTargetTracks: options.ignoreInvalidMorphTargetTracks, | |||||
| ignoreEmptyTextures: options.ignoreEmptyTextures, | |||||
| } | } | ||||
| if (options.exportExt === 'glb') { | if (options.exportExt === 'glb') { | ||||
| gltfOptions.binary = true | gltfOptions.binary = true |
| } | } | ||||
| if (textureDef.source < 0) { | if (textureDef.source < 0) { | ||||
| console.error('textureDef.source cannot be saved', textureDef, map) | console.error('textureDef.source cannot be saved', textureDef, map) | ||||
| delete textureDef.source // gltf spec allows undefined, not -1 | |||||
| } | } | ||||
| return processed | return processed |
| const extensionDef: any = {} | const extensionDef: any = {} | ||||
| if (material.alphaMap) { | |||||
| if (material.alphaMap && writer.checkEmptyMap(material.alphaMap)) { | |||||
| const alphaMapDef = {index: writer.processTexture(material.alphaMap)} | const alphaMapDef = {index: writer.processTexture(material.alphaMap)} | ||||
| writer.applyTextureTransform(alphaMapDef, material.alphaMap) | writer.applyTextureTransform(alphaMapDef, material.alphaMap) |
| extensionDef.bumpScale = material.bumpScale | extensionDef.bumpScale = material.bumpScale | ||||
| if (material.bumpMap) { | |||||
| if (material.bumpMap && writer.checkEmptyMap(material.bumpMap)) { | |||||
| const bumpMapDef = {index: writer.processTexture(material.bumpMap)} | const bumpMapDef = {index: writer.processTexture(material.bumpMap)} | ||||
| writer.applyTextureTransform(bumpMapDef, material.bumpMap) | writer.applyTextureTransform(bumpMapDef, material.bumpMap) |
| extensionDef.displacementScale = material.displacementScale | extensionDef.displacementScale = material.displacementScale | ||||
| extensionDef.displacementBias = material.displacementBias | extensionDef.displacementBias = material.displacementBias | ||||
| if (material.displacementMap) { | |||||
| if (material.displacementMap && writer.checkEmptyMap(material.displacementMap)) { | |||||
| const displacementMapDef = {index: writer.processTexture(material.displacementMap)} | const displacementMapDef = {index: writer.processTexture(material.displacementMap)} | ||||
| writer.applyTextureTransform(displacementMapDef, material.displacementMap) | writer.applyTextureTransform(displacementMapDef, material.displacementMap) |
| extensionDef.lightMapIntensity = material.lightMapIntensity | extensionDef.lightMapIntensity = material.lightMapIntensity | ||||
| if (material.lightMap) { | |||||
| if (material.lightMap && writer.checkEmptyMap(material.lightMap)) { | |||||
| const lightMapDef = {index: writer.processTexture(material.lightMap)} | const lightMapDef = {index: writer.processTexture(material.lightMap)} | ||||
| writer.applyTextureTransform(lightMapDef, material.lightMap) | writer.applyTextureTransform(lightMapDef, material.lightMap) |
| import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader.js' | |||||
| import {Group, Texture} from 'three' | |||||
| import {AssetImporter} from '../AssetImporter' | |||||
| /** | |||||
| * Extended FBXLoader that sets the default image from AssetImporter (for invalid/missing textures) | |||||
| */ | |||||
| export class FBXLoader2 extends FBXLoader { | |||||
| async loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<Group> { | |||||
| const val = Texture.DEFAULT_IMAGE | |||||
| // this will be used when doing new Texture(). Which is done for not found images or when some error happens in loading. See FBXLoader. | |||||
| // todo save the path of invalid textures, check if they can be found in the loaded libs, and ask the user in UI to remap it to something else manually | |||||
| if (!Texture.DEFAULT_IMAGE) Texture.DEFAULT_IMAGE = AssetImporter.WHITE_IMAGE_DATA | |||||
| const res = await super.loadAsync(url, onProgress) | |||||
| Texture.DEFAULT_IMAGE = val | |||||
| return res | |||||
| } | |||||
| } |
| import type {GLTF, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js' | import type {GLTF, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js' | ||||
| import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js' | import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js' | ||||
| import {LoadingManager, Object3D, OrthographicCamera} from 'three' | |||||
| import {LoadingManager, Object3D, OrthographicCamera, Texture} from 'three' | |||||
| import {AnyOptions, safeSetProperty} from 'ts-browser-helpers' | import {AnyOptions, safeSetProperty} from 'ts-browser-helpers' | ||||
| import {ThreeViewer} from '../../viewer' | import {ThreeViewer} from '../../viewer' | ||||
| import {generateUUID} from '../../three' | import {generateUUID} from '../../three' | ||||
| UnlitLineMaterial, | UnlitLineMaterial, | ||||
| UnlitMaterial, | UnlitMaterial, | ||||
| } from '../../core' | } from '../../core' | ||||
| import {AssetImporter} from '../AssetImporter' | |||||
| // todo move somewhere | |||||
| const supportedEmbeddedFiles = ['hdr', 'exr', 'webp', 'avif', 'ktx', 'hdrpng', 'svg', 'cube'] // ktx2, drc handled separately | |||||
| export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|undefined> { | export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|undefined> { | ||||
| isGLTFLoader2 = true | isGLTFLoader2 = true | ||||
| parse(data: ArrayBuffer | string, path: string, onLoad: (gltf: GLTF) => void, onError?: (event: ErrorEvent) => void, url?: string) { | parse(data: ArrayBuffer | string, path: string, onLoad: (gltf: GLTF) => void, onError?: (event: ErrorEvent) => void, url?: string) { | ||||
| this.preparse.call(this, data, url || path) | this.preparse.call(this, data, url || path) | ||||
| .then((res: ArrayBuffer | string) => res ? super.parse(res, path, onLoad, onError) : onError && onError(new ErrorEvent('no data'))) | |||||
| .then((res: ArrayBuffer|string) => { | |||||
| const val = Texture.DEFAULT_IMAGE | |||||
| // this will be used when doing new Texture(). Which is done for not found images or when some error happens in loading. See FBXLoader. | |||||
| // todo save the path of invalid textures, check if they can be found in the loaded libs, and ask the user in UI to remap it to something else manually | |||||
| if (!Texture.DEFAULT_IMAGE) Texture.DEFAULT_IMAGE = AssetImporter.WHITE_IMAGE_DATA | |||||
| return res ? super.parse(res, path, (ret)=>{ | |||||
| Texture.DEFAULT_IMAGE = val | |||||
| onLoad && onLoad(ret) | |||||
| }, onError) : onError && onError(new ErrorEvent('no data')) | |||||
| }) | |||||
| .catch((e: any) => { | .catch((e: any) => { | ||||
| console.error(e) | console.error(e) | ||||
| if (onError) onError(e ?? new ErrorEvent('unknown error')) | if (onError) onError(e ?? new ErrorEvent('unknown error')) | ||||
| console.error('Add GLTFMeshOptPlugin(and initialize it) to viewer to enable EXT_meshopt_compression decode') | console.error('Add GLTFMeshOptPlugin(and initialize it) to viewer to enable EXT_meshopt_compression decode') | ||||
| } | } | ||||
| } | } | ||||
| const needsBasisU = parser.json?.extensionsUsed?.includes?.('KHR_texture_basisu') | |||||
| if (needsBasisU) { | |||||
| const ktx2 = viewer.assetManager.importer.registerFile(tempPathKtx2) | |||||
| if (ktx2) { | |||||
| this.setKTX2Loader(ktx2 as any) // todo: check class? | |||||
| parser.options.ktx2Loader = ktx2 as any | |||||
| } | |||||
| // create ktx2 loader so it can be used with getHandler, we need to do this even when extension is not used since we dont know | |||||
| const ktx2 = viewer.assetManager.importer.registerFile(tempPathKtx2) | |||||
| // const needsBasisU = parser.json?.extensionsUsed?.includes?.('KHR_texture_basisu') | |||||
| // if (needsBasisU) { | |||||
| // const ktx2 = viewer.assetManager.importer.registerFile(tempPathKtx2) | |||||
| if (ktx2) { | |||||
| this.setKTX2Loader(ktx2 as any) // todo: check class? | |||||
| parser.options.ktx2Loader = ktx2 as any | |||||
| } | } | ||||
| // } | |||||
| // registering temp file creates and makes a loader available to the loading manager of that type | |||||
| const tempFiles = supportedEmbeddedFiles.map(f=>generateUUID() + '.' + f) | |||||
| tempFiles.forEach(f=>viewer.assetManager.importer.registerFile(f)) | |||||
| return {name: 'GLTF2_HELPER_PLUGIN', afterRoot: async(result: GLTF) => { | return {name: 'GLTF2_HELPER_PLUGIN', afterRoot: async(result: GLTF) => { | ||||
| if (needsDrc) viewer.assetManager.importer.unregisterFile(tempPathDrc) | if (needsDrc) viewer.assetManager.importer.unregisterFile(tempPathDrc) | ||||
| if (needsBasisU) viewer.assetManager.importer.unregisterFile(tempPathKtx2) | |||||
| if (ktx2) viewer.assetManager.importer.unregisterFile(tempPathKtx2) | |||||
| tempFiles.forEach(f=>viewer.assetManager.importer.unregisterFile(f)) | |||||
| await GLTFViewerConfigExtension.ImportViewerConfig(parser, viewer, result.scenes || [result.scene]) | await GLTFViewerConfigExtension.ImportViewerConfig(parser, viewer, result.scenes || [result.scene]) | ||||
| }} | }} | ||||
| } | } |
| }) | }) | ||||
| return super._createMaterial(material) | return super._createMaterial(material) | ||||
| } | } | ||||
| private _compareMaterials!: (material: Material) => Material | |||||
| private declare _compareMaterials: (material: Material) => Material | |||||
| async loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<Object3D> { | async loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<Object3D> { | ||||
| const ret = await super.loadAsync(url, onProgress) | const ret = await super.loadAsync(url, onProgress) |
| import {CanvasTexture, ImageLoader, Loader, LoadingManager, Texture} from 'three' | |||||
| import {getUrlQueryParam} from 'ts-browser-helpers' | |||||
| /** | |||||
| * Same as TextureLoader but loads SVG images, fixes issues with windows not loading svg files without a defined size. | |||||
| * See - https://github.com/mrdoob/three.js/issues/30899 | |||||
| * | |||||
| * todo - create example for test, see sample code in gh issue. | |||||
| */ | |||||
| class SVGTextureLoader extends Loader { | |||||
| constructor(manager: LoadingManager) { | |||||
| super(manager) | |||||
| } | |||||
| static USE_CANVAS_TEXTURE = getUrlQueryParam('svg-load-disable-canvas') !== 'true' | |||||
| load(url: string, onLoad: (texture: Texture) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): Texture { | |||||
| const canvas = SVGTextureLoader.USE_CANVAS_TEXTURE ? document.createElement('canvas') : undefined | |||||
| const texture = SVGTextureLoader.USE_CANVAS_TEXTURE ? new CanvasTexture(canvas!) : new Texture() | |||||
| const loader = new ImageLoader(this.manager) | |||||
| loader.setCrossOrigin(this.crossOrigin) | |||||
| loader.setPath(this.path) | |||||
| loader.load(url, function(image) { | |||||
| if (canvas) { | |||||
| SVGTextureLoader.CopyImageToCanvas(canvas, image) | |||||
| } else { | |||||
| texture.image = image | |||||
| } | |||||
| texture.needsUpdate = true | |||||
| if (onLoad !== undefined) { | |||||
| onLoad(texture) | |||||
| } | |||||
| }, onProgress, onError) | |||||
| return texture | |||||
| } | |||||
| static CopyImageToCanvas(canvas: HTMLCanvasElement, image: HTMLImageElement) { | |||||
| // size can be scaled here, this is based on the viewBox aspect ratio and minimum size of 150hx300w | |||||
| canvas.width = image.naturalWidth || image.width || 512 | |||||
| canvas.height = image.naturalHeight || image.height || 512 | |||||
| const ctx = canvas.getContext('2d') | |||||
| if (ctx) { | |||||
| ctx.clearRect(0, 0, canvas.width, canvas.height) | |||||
| ctx.drawImage(image, 0, 0, canvas.width, canvas.height) | |||||
| } else { | |||||
| console.error('SVGTextureLoader: Failed to get canvas context.') | |||||
| } | |||||
| } | |||||
| } | |||||
| export {SVGTextureLoader} |
| embedUrlImages: false, | embedUrlImages: false, | ||||
| encrypt: false, | encrypt: false, | ||||
| encryptKey: '', | encryptKey: '', | ||||
| ignoreInvalidMorphTargetTracks: true, | |||||
| ignoreEmptyTextures: true, | |||||
| } | } | ||||
| async exportScene(options?: ExportAssetOptions): Promise<BlobExt | undefined> { | async exportScene(options?: ExportAssetOptions): Promise<BlobExt | undefined> { | ||||
| // label: 'Convert to indexed', | // label: 'Convert to indexed', | ||||
| // property: [this.exportOptions, 'convertMeshToIndexed'], | // property: [this.exportOptions, 'convertMeshToIndexed'], | ||||
| // }, | // }, | ||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Ignore invalid animations', | |||||
| property: [this.exportOptions, 'ignoreInvalidMorphTargetTracks'], | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Ignore invalid textures', | |||||
| property: [this.exportOptions, 'ignoreInvalidTextures'], | |||||
| }, | |||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Export GLB', | label: 'Export GLB', |
| extensionDef.customBumpScale = material.userData._customBumpScale || 1.0 | extensionDef.customBumpScale = material.userData._customBumpScale || 1.0 | ||||
| if (material.userData._customBumpMap) { | |||||
| if (w.checkEmptyMap(material.userData._customBumpMap)) { | |||||
| const customBumpMapDef = {index: w.processTexture(material.userData._customBumpMap)} | const customBumpMapDef = {index: w.processTexture(material.userData._customBumpMap)} | ||||
| w.applyTextureTransform(customBumpMapDef, material.userData._customBumpMap) | w.applyTextureTransform(customBumpMapDef, material.userData._customBumpMap) |