diff --git a/package.json b/package.json index 5b4f4fb..7ba683b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "onCommand:gltf.previewModel", "onCommand:gltf.validateFile", "onCommand:gltf.exportGlbFile", - "onCommand:gltf.importGlbFile" + "onCommand:gltf.importGlbFile", + "onCommand:gltf.openGlbFile" ], "main": "./out/src/extension", "contributes": { @@ -177,6 +178,10 @@ "command": "gltf.importGlbFile", "title": "glTF: Import from GLB" }, + { + "command": "gltf.openGlbFile", + "title": "glTF: Open GLB" + }, { "command": "gltf.importAnimation", "title": "glTF: Import animation" @@ -232,6 +237,10 @@ "command": "gltf.importGlbFile", "group": "glTF" }, + { + "command": "gltf.openGlbFile", + "group": "glTF" + }, { "command": "gltf.validateFile", "group": "glTF" diff --git a/src/dataUriTextDocumentContentProvider.ts b/src/dataUriTextDocumentContentProvider.ts index 0746a0b..b66e3a1 100644 --- a/src/dataUriTextDocumentContentProvider.ts +++ b/src/dataUriTextDocumentContentProvider.ts @@ -10,7 +10,7 @@ import { getFromJsonPointer, btoa, atob, getAccessorData, AccessorTypeToNumCompo import { GLTF2 } from './GLTF2'; let decoderModule; -draco3dgltf.createDecoderModule({}).then(function(module) { +draco3dgltf.createDecoderModule({}).then(function (module) { decoderModule = module; }); @@ -57,12 +57,54 @@ export class DataUriTextDocumentContentProvider implements vscode.TextDocumentCo return !!jsonPointer.match(/^\/meshes\/\d+\/primitives\//); } + getDocument(fileName: string): vscode.TextDocument { + let document = vscode.workspace.textDocuments.find(e => e.uri.scheme === 'file' && e.fileName === fileName); + if (document) { + return document; + } + + // get json document from glbProvider + return vscode.workspace.textDocuments.find(e => e.uri.scheme === 'glb' && e.fileName === fileName); + } + + getTitleAndData(data: any, gltf: GLTF2.GLTF, jsonPointer: string, fileName: string): [string, string] { + if (data.hasOwnProperty('uri')) { + // from external uri + let dataUri: string = data.uri; + let previewTitle = dataUri; + if (!dataUri.startsWith('data:')) { + // Not a DataURI: Look up external reference. + const name = decodeURI(Url.resolve(fileName, dataUri)); + const contents = fs.readFileSync(name); + dataUri = 'data:image;base64,' + btoa(contents); + previewTitle = decodeURI(previewTitle); + } else { + previewTitle = jsonPointer; + } + + return [previewTitle, dataUri]; + } + + if (data.hasOwnProperty('bufferView')) { + // from internal bufferView + const bufferView = gltf.bufferViews[data.bufferView]; + const buffer = gltf.buffers[bufferView.buffer]; + const name = decodeURI(Url.resolve(fileName, buffer.uri)); + const contents = fs.readFileSync(name).slice(bufferView.byteOffset, bufferView.byteOffset + bufferView.byteLength); + const dataUri = 'data:image;base64,' + btoa(contents); + const previewTitle = `${buffer.uri}:${jsonPointer}`; + return [previewTitle, dataUri]; + } + + return [null, null] + } + public async provideTextDocumentContent(uri: vscode.Uri): Promise { const fileName = decodeURIComponent(uri.fragment); const query = querystring.parse(uri.query); query.viewColumn = query.viewColumn || vscode.ViewColumn.Active.toString(); let glTFContent: string; - const document = vscode.workspace.textDocuments.find(e => e.uri.scheme === 'file' && e.fileName === fileName); + const document = this.getDocument(fileName); if (document) { glTFContent = document.getText(); } else { @@ -76,18 +118,8 @@ export class DataUriTextDocumentContentProvider implements vscode.TextDocumentCo const data = getFromJsonPointer(glTF, jsonPointer); if (data && (typeof data === 'object')) { - if (data.hasOwnProperty('uri')) { - let dataUri: string = data.uri; - let previewTitle = dataUri; - if (!dataUri.startsWith('data:')) { - // Not a DataURI: Look up external reference. - const name = decodeURI(Url.resolve(fileName, dataUri)); - const contents = fs.readFileSync(name); - dataUri = 'data:image;base64,' + btoa(contents); - previewTitle = decodeURI(previewTitle); - } else { - previewTitle = jsonPointer; - } + let [previewTitle, dataUri] = this.getTitleAndData(data, glTF, jsonPointer, fileName); + if (previewTitle && dataUri) { if (this.isImage(jsonPointer)) { // Peek Definition requests have a document that matches the current document diff --git a/src/extension.ts b/src/extension.ts index 52cc528..3a69c9d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,6 +9,7 @@ import * as fs from 'fs'; import { getFromJsonPointer, guessMimeType, btoa, guessFileExtension, getAccessorData, AccessorTypeToNumComponents, parseJsonMap, truncateJsonPointer } from './utilities'; import { GLTF2 } from './GLTF2'; import { GltfWindow } from './gltfWindow'; +import { GlbTextDocumentContentProvider } from './glbTextDocumentContentProvider'; function checkValidEditor(): boolean { if (vscode.window.activeTextEditor === undefined) { @@ -457,6 +458,24 @@ export function activate(context: vscode.ExtensionContext): void { } })); + // + // Virtual TextDocumentContentProvider for glb json chunk. + // + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('glb', new GlbTextDocumentContentProvider())); + + // + // Open GLB with 'glb:' schema, and send GlbProvider. + // + context.subscriptions.push(vscode.commands.registerCommand('gltf.openGlbFile', async (fileUri) => { + + const virtualTextPath = `glb:${fileUri.fsPath}.json`; + const uri = vscode.Uri.parse(virtualTextPath); + const doc = await vscode.workspace.openTextDocument(uri); // calls back into the provider + vscode.languages.setTextDocumentLanguage(doc, "json"); + await vscode.window.showTextDocument(doc, { preview: false }); + + })); + // // Run the validator on an external file. // diff --git a/src/glbTextDocumentContentProvider.ts b/src/glbTextDocumentContentProvider.ts new file mode 100644 index 0000000..03b8252 --- /dev/null +++ b/src/glbTextDocumentContentProvider.ts @@ -0,0 +1,82 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +// +// GlbTextDocumentContentProvider provide json chunk of glb. +// +export class GlbTextDocumentContentProvider implements vscode.TextDocumentContentProvider { + + // emitter and its event + onDidChangeEmitter = new vscode.EventEmitter(); + onDidChange = this.onDidChangeEmitter.event; + + provideTextDocumentContent(uri: vscode.Uri): string { + + if (!uri.fsPath.endsWith(".json")) { + return "fsPath must endsWith .json"; + } + let fsPath = uri.fsPath.substr(0, uri.fsPath.length - 5); + + let data = fs.readFileSync(fsPath); + let offset = 0; + if (data.readInt32LE(offset) != 0x46546C67) { + return `not glb`; + } + offset += 4; + + let version = data.readInt32LE(offset) + if (version != 2) { + return `gltf version ${version} not support`; + } + offset += 4; + + let length = data.readInt32LE(offset); + offset += 4; + + let json = null; + let binOffset = null; + while (offset < length) { + + let chunkLength = data.readInt32LE(offset); + offset += 4; + + let chunkType = data.readInt32LE(offset); + offset += 4; + + let chunkData = data.toString('utf8', offset, offset + chunkLength); + + if (chunkType == 0x4E4F534A) { + + json = JSON.parse(chunkData); + + } + else if (chunkType == 0x004E4942) { + + binOffset = offset; + + } + else { + // unknown + } + + offset += chunkLength; + } + + if (binOffset && json) { + + // buffer access hack + json.buffers[0]['uri'] = path.basename(fsPath); + for (let bufferView of json.bufferViews) { + bufferView.byteOffset += binOffset; + } + + return JSON.stringify(json, null, ' '); + } + else { + + return `invalid glb`; + + } + } +}; diff --git a/src/gltfWindow.ts b/src/gltfWindow.ts index 9a94f3c..1d3a0b4 100644 --- a/src/gltfWindow.ts +++ b/src/gltfWindow.ts @@ -4,7 +4,15 @@ import { GltfInspectData } from './gltfInspectData'; import { GltfOutline } from './gltfOutline'; function isGltfFile(editor: vscode.TextEditor | undefined): boolean { - return editor && editor.document.fileName.toLowerCase().endsWith('.gltf'); + if (editor) { + if (editor.document.fileName.toLowerCase().endsWith('.gltf')) { + return true; + } + if (editor.document.uri.scheme == 'glb') { + return true; + } + } + return false; } export class GltfWindow {