From e9ddd033384b7fb496700a89f22e7cda75ede461 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Sun, 4 Nov 2018 16:00:03 -0800 Subject: [PATCH] Switch to webview for gltf preview --- src/contextBase.ts | 16 +++ src/extension.ts | 37 ++--- ...umentContentProvider.ts => gltfPreview.ts} | 135 +++++++++--------- src/utilities.ts | 10 ++ 4 files changed, 104 insertions(+), 94 deletions(-) create mode 100644 src/contextBase.ts rename src/{gltfPreviewDocumentContentProvider.ts => gltfPreview.ts} (54%) diff --git a/src/contextBase.ts b/src/contextBase.ts new file mode 100644 index 0000000..2eb1a46 --- /dev/null +++ b/src/contextBase.ts @@ -0,0 +1,16 @@ +import * as vscode from "vscode"; +import { toResourceUrl } from "./utilities"; + +export abstract class ContextBase { + protected readonly _context: vscode.ExtensionContext; + private readonly _extensionRootPath: string; + + constructor(context: vscode.ExtensionContext) { + this._context = context; + this._extensionRootPath = `${toResourceUrl(this._context.extensionPath.replace(/\\$/, ''))}/`; + } + + protected get extensionRootPath(): string { + return this._extensionRootPath; + } +} diff --git a/src/extension.ts b/src/extension.ts index 60cedf5..cb1a479 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,15 +1,14 @@ import * as vscode from 'vscode'; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient'; import { DataUriTextDocumentContentProvider } from './dataUriTextDocumentContentProvider'; -import { GltfPreviewDocumentContentProvider } from './gltfPreviewDocumentContentProvider'; import { GltfOutlineTreeDataProvider } from './gltfOutlineTreeDataProvider'; +import { GltfPreview } from './gltfPreview'; import { ConvertGLBtoGltfLoadFirst, ConvertToGLB, getBuffer } from 'gltf-import-export'; import * as GltfValidate from './validationProvider'; -import * as jsonMap from 'json-source-map'; import * as path from 'path'; import * as Url from 'url'; import * as fs from 'fs'; -import { getFromJsonPointer, guessMimeType, btoa, guessFileExtension, getAccessorData, AccessorTypeToNumComponents } from './utilities'; +import { getFromJsonPointer, guessMimeType, btoa, guessFileExtension, getAccessorData, AccessorTypeToNumComponents, parseJsonMap } from './utilities'; import { GLTF2 } from './GLTF2'; function checkValidEditor(): boolean { @@ -35,7 +34,7 @@ function pointerContains(pointer: any, selection: vscode.Selection): boolean { function tryGetJsonMap() { try { - return jsonMap.parse(vscode.window.activeTextEditor.document.getText()) as { data: GLTF2.GLTF, pointers: Array }; + return parseJsonMap(vscode.window.activeTextEditor.document.getText()); } catch (ex) { vscode.window.showErrorMessage('Error parsing this document. Please make sure it is valid JSON.'); } @@ -354,32 +353,17 @@ export function activate(context: vscode.ExtensionContext) { } })); + const gltfPreview = new GltfPreview(context); + // - // Register a preview of the whole glTF file. + // Preview a glTF model. // - const gltfPreviewProvider = new GltfPreviewDocumentContentProvider(context); - const gltfPreviewRegistration = vscode.workspace.registerTextDocumentContentProvider('gltf-preview', gltfPreviewProvider); - context.subscriptions.push(gltfPreviewRegistration); - context.subscriptions.push(vscode.commands.registerCommand('gltf.previewModel', () => { if (!checkValidEditor()) { return; } - const fileName = vscode.window.activeTextEditor.document.fileName; - const baseName = path.basename(fileName); - const gltfPreviewUri = vscode.Uri.parse(gltfPreviewProvider.UriPrefix + encodeURIComponent(fileName)); - - vscode.commands.executeCommand('vscode.previewHtml', gltfPreviewUri, vscode.ViewColumn.Two, `glTF Preview [${baseName}]`) - .then((success) => { }, (reason) => { vscode.window.showErrorMessage(reason); }); - - // This can be used to debug the preview HTML. - //vscode.workspace.openTextDocument(gltfPreviewUri).then((doc: vscode.TextDocument) => { - // vscode.window.showTextDocument(doc, ViewColumn.Three, false).then(e => { - // }); - //}, (reason) => { vscode.window.showErrorMessage(reason); }); - - gltfPreviewProvider.update(gltfPreviewUri); + gltfPreview.showPanel(vscode.window.activeTextEditor.document); })); // @@ -704,11 +688,8 @@ export function activate(context: vscode.ExtensionContext) { // // Update all preview windows when the glTF file is saved. // - vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { - if (document === vscode.window.activeTextEditor.document) { - const gltfPreviewUri = vscode.Uri.parse(gltfPreviewProvider.UriPrefix + encodeURIComponent(document.fileName)); - gltfPreviewProvider.update(gltfPreviewUri); - } + vscode.workspace.onDidSaveTextDocument((document) => { + gltfPreview.updatePanel(document); }); } diff --git a/src/gltfPreviewDocumentContentProvider.ts b/src/gltfPreview.ts similarity index 54% rename from src/gltfPreviewDocumentContentProvider.ts rename to src/gltfPreview.ts index e0376e6..59ac934 100644 --- a/src/gltfPreviewDocumentContentProvider.ts +++ b/src/gltfPreview.ts @@ -1,37 +1,26 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; +import { ContextBase } from './contextBase'; +import { toResourceUrl, parseJsonMap } from './utilities'; -export class GltfPreviewDocumentContentProvider implements vscode.TextDocumentContentProvider { - private _onDidChange = new vscode.EventEmitter(); - private _context: vscode.ExtensionContext; - private _mainHtml: string; - private _babylonHtml: string; - private _cesiumHtml: string; - private _threeHtml: string; +export class GltfPreview extends ContextBase { + private readonly _mainHtml: string; + private readonly _babylonHtml: string; + private readonly _cesiumHtml: string; + private readonly _threeHtml: string; - public UriPrefix = 'gltf-preview://'; + private _panels: { [fileName: string]: vscode.WebviewPanel } = {}; constructor(context: vscode.ExtensionContext) { - this._context = context; - this._mainHtml = fs.readFileSync(this._context.asAbsolutePath('pages/previewModel.html'), 'UTF-8') + super(context); + + this._mainHtml = fs.readFileSync(this._context.asAbsolutePath('pages/previewModel.html'), 'UTF-8'); this._babylonHtml = encodeURI(fs.readFileSync(this._context.asAbsolutePath('pages/babylonView.html'), 'UTF-8')); this._cesiumHtml = encodeURI(fs.readFileSync(this._context.asAbsolutePath('pages/cesiumView.html'), 'UTF-8')); this._threeHtml = encodeURI(fs.readFileSync(this._context.asAbsolutePath('pages/threeView.html'), 'UTF-8')); } - private addFilePrefix(file: string): string { - return ((file[0] === '/') ? 'file://' : 'file:///') + file; - } - - private getFilePath(file: string): string { - return this.addFilePrefix(this._context.asAbsolutePath(file)); - } - - private toUrl(file: string): string { - return this.addFilePrefix(file.replace(/\\/g, '/')); - } - // Instructions to open Chrome DevTools on the HTML preview window: // // 1. With the HTML preview window open, click Help->Toggle Developer Tools. @@ -47,53 +36,76 @@ export class GltfPreviewDocumentContentProvider implements vscode.TextDocumentCo // click the pull-down and change "top" to "active-frame (webview.html)". // Now you can debug the HTML preview in the sandboxed iframe. - public provideTextDocumentContent(uri: vscode.Uri): string { - let filePath = decodeURIComponent(uri.authority); - const document = vscode.workspace.textDocuments.find(e => e.fileName.toLowerCase() === filePath.toLowerCase()); - if (!document) { - return 'ERROR: Can no longer find document in editor: ' + filePath; + public showPanel(gltfDocument: vscode.TextDocument): void { + const gltfFilePath = gltfDocument.fileName; + + let panel = this._panels[gltfFilePath]; + if (!panel) { + panel = vscode.window.createWebviewPanel('gltf.preview', 'glTF Preview', vscode.ViewColumn.Two, { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [ + vscode.Uri.file(this._context.extensionPath), + vscode.Uri.file(path.dirname(gltfFilePath)), + ], + }); + + panel.onDidDispose(() => { + delete this._panels[gltfFilePath]; + }); + + this._panels[gltfFilePath] = panel; } - // Update case of `fileName` to match actual document filename. - filePath = document.fileName; - - const gltfContent = document.getText(); - const gltfFileName = path.basename(filePath); - let gltfRootPath: string = this.toUrl(path.dirname(filePath)); - if (!gltfRootPath.endsWith("/")) { - gltfRootPath += "/"; + + const gltfContent = gltfDocument.getText(); + this.updatePanelInternal(panel, gltfFilePath, gltfContent); + panel.reveal(vscode.ViewColumn.Two); + } + + public updatePanel(gltfDocument: vscode.TextDocument): void { + const gltfFileName = gltfDocument.fileName; + let panel = this._panels[gltfFileName]; + if (panel) { + const gltfContent = gltfDocument.getText(); + this.updatePanelInternal(panel, gltfFileName, gltfContent); } + } + + private updatePanelInternal(panel: vscode.WebviewPanel, gltfFilePath: string, gltfContent: string): void { + const map = parseJsonMap(gltfContent); + + const gltfRootPath = toResourceUrl(`${path.dirname(gltfFilePath)}/`); + const gltfFileName = path.basename(gltfFilePath); - var gltfMajorVersion = 1; - try { - const gltf = JSON.parse(gltfContent); - if (gltf && gltf.asset && gltf.asset.version && gltf.asset.version[0] === '2') { - gltfMajorVersion = 2; - } - } catch (ex) { } - - let extensionRootPath: string = this._context.asAbsolutePath('').replace(/\\/g, '/'); - if (!extensionRootPath.endsWith("/")) { - extensionRootPath += "/"; + const gltf = map.data; + let gltfMajorVersion = 1; + if (gltf && gltf.asset && gltf.asset.version && gltf.asset.version[0] === '2') { + gltfMajorVersion = 2; } + panel.title = `glTF Preview [${gltfFileName}]`; + panel.webview.html = this.formatHtml(gltfMajorVersion, gltfContent, gltfRootPath, gltfFileName); + } + + private formatHtml(gltfMajorVersion: number, gltfContent: string, gltfRootPath: string, gltfFileName: string): string { const defaultEngine = vscode.workspace.getConfiguration('glTF').get('defaultV' + gltfMajorVersion + 'Engine'); const defaultBabylonReflection = String(vscode.workspace.getConfiguration('glTF.Babylon') - .get('environment')).replace('{extensionRootPath}', extensionRootPath.replace(/\/$/, '')); + .get('environment')).replace('{extensionRootPath}', this.extensionRootPath); const defaultThreeReflection = String(vscode.workspace.getConfiguration('glTF.Three') - .get('environment')).replace('{extensionRootPath}', extensionRootPath.replace(/\/$/, '')); - const dracoLoaderPath = extensionRootPath + 'engines/Draco/draco_decoder.js'; + .get('environment')).replace('{extensionRootPath}', this.extensionRootPath); + const dracoLoaderPath = this.extensionRootPath + 'engines/Draco/draco_decoder.js'; // These strings are available in JavaScript by looking up the ID. They provide the extension's root // path (needed for locating additional assets), various settings, and the glTF name and contents. // Some engines can display "live" glTF contents, others must load from the glTF path and filename. // The path name is needed for glTF files that include external resources. const strings = [ - { id: 'extensionRootPath', text: this.toUrl(extensionRootPath) }, + { id: 'extensionRootPath', text: this.extensionRootPath }, { id: 'defaultEngine', text: defaultEngine }, - { id: 'defaultBabylonReflection', text: this.toUrl(defaultBabylonReflection) }, - { id: 'defaultThreeReflection', text: this.toUrl(defaultThreeReflection) }, - { id: 'dracoLoaderPath', text: this.toUrl(dracoLoaderPath) }, + { id: 'defaultBabylonReflection', text: defaultBabylonReflection }, + { id: 'defaultThreeReflection', text: defaultThreeReflection }, + { id: 'dracoLoaderPath', text: dracoLoaderPath }, { id: 'babylonHtml', text: this._babylonHtml }, { id: 'cesiumHtml', text: this._cesiumHtml }, { id: 'threeHtml', text: this._threeHtml }, @@ -112,6 +124,7 @@ export class GltfPreviewDocumentContentProvider implements vscode.TextDocumentCo const scripts = [ 'engines/Cesium/Cesium.js', 'node_modules/babylonjs/babylon.max.js', + 'node_modules/babylonjs/babylon.inspector.min.js', 'node_modules/babylonjs-loaders/babylonjs.loaders.js', 'engines/Three/three.min.js', 'engines/Three/DDSLoader.js', @@ -125,19 +138,9 @@ export class GltfPreviewDocumentContentProvider implements vscode.TextDocumentCo ]; // Note that with the file: protocol, we must manually specify the UTF-8 charset. - const content = this._mainHtml.replace('{assets}', - styles.map(s => `\n`).join('') + + return this._mainHtml.replace('{assets}', + styles.map(s => `\n`).join('') + strings.map(s => `\n`).join('') + - scripts.map(s => `\n`).join('')); - - return content; - } - - get onDidChange(): vscode.Event { - return this._onDidChange.event; - } - - public update(uri: vscode.Uri) { - this._onDidChange.fire(uri); + scripts.map(s => `\n`).join('')); } } diff --git a/src/utilities.ts b/src/utilities.ts index df8d6ff..e629689 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -1,4 +1,5 @@ import { GLTF2 } from "./glTF2"; +import * as jsonMap from 'json-source-map'; export const ComponentTypeToBytesPerElement = { 5120: Int8Array.BYTES_PER_ELEMENT, @@ -124,3 +125,12 @@ export function guessMimeType(filename: string): string { } return 'application/octet-stream'; } + +export function toResourceUrl(path: string): string { + return `vscode-resource:${path.replace(/\\/g, '/')}`; +} + +export function parseJsonMap(content: string) { + return jsonMap.parse(content) as { data: GLTF2.GLTF, pointers: Array }; +} +