mirror of
https://github.com/janispritzkau/obsidian-model-viewer.git
synced 2026-07-22 05:44:57 +00:00
feat: embed model viewer
This commit is contained in:
parent
fc176357cb
commit
54228294d1
6 changed files with 344 additions and 12 deletions
10
README.md
10
README.md
|
|
@ -1,3 +1,13 @@
|
|||
# Obsidian Model Viewer
|
||||
|
||||
This plugin allows you to view and embed interactive 3D models directly in your [Obsidian](http://obsidian.md/) vault, powered by Google’s [\<model-viewer\>](https://modelviewer.dev/) component.
|
||||
|
||||

|
||||
|
||||
> \<model-viewer\> supports and plans to support only glTF/GLB 3D models. It is the Khronos standard known as the JPEG of 3D and the first format to standardize Physically-Based Rendering (PBR), making your models look realistic under any lighting, on any renderer. It is also compact, compressible, and loads rapidly into the GPU.
|
||||
|
||||
[\<model-viewer\> FAQ](https://modelviewer.dev/docs/faq.html)
|
||||
|
||||
## Features
|
||||
|
||||
==TODO==
|
||||
|
|
|
|||
204
main.ts
204
main.ts
|
|
@ -1,11 +1,33 @@
|
|||
import { App, Plugin, PluginSettingTab, Setting } from "obsidian";
|
||||
import type { ModelViewerElement } from "@google/model-viewer";
|
||||
import {
|
||||
App,
|
||||
Component,
|
||||
FileView,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
type TFile,
|
||||
type WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
|
||||
if (customElements.get("model-viewer") == null) {
|
||||
import("@google/model-viewer");
|
||||
}
|
||||
|
||||
interface ModelViewerSettings {
|
||||
mySetting: string;
|
||||
cameraControls: boolean;
|
||||
disablePan: boolean;
|
||||
disableZoom: boolean;
|
||||
autoRotate: boolean;
|
||||
interactionPrompt: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: ModelViewerSettings = {
|
||||
mySetting: "default",
|
||||
cameraControls: true,
|
||||
disablePan: false,
|
||||
disableZoom: false,
|
||||
autoRotate: true,
|
||||
interactionPrompt: true,
|
||||
};
|
||||
|
||||
export default class ModelViewerPlugin extends Plugin {
|
||||
|
|
@ -15,10 +37,31 @@ export default class ModelViewerPlugin extends Plugin {
|
|||
await this.loadSettings();
|
||||
|
||||
this.addSettingTab(new ModelViewerSettingTab(this.app, this));
|
||||
|
||||
this.registerView("model-viewer-file-view", (leaf) => {
|
||||
return new ModelViewerFileView(leaf, this);
|
||||
});
|
||||
|
||||
this.registerExtensions(["glb", "gltf"], "model-viewer-file-view");
|
||||
|
||||
// @ts-expect-error
|
||||
this.app.embedRegistry.registerExtensions(
|
||||
["gltf", "glb"],
|
||||
(
|
||||
context: {
|
||||
containerEl: HTMLElement;
|
||||
displayMode: boolean;
|
||||
linktext: string;
|
||||
showInline: boolean;
|
||||
},
|
||||
file: TFile
|
||||
) => new ModelViewerComponent(this, context.containerEl, file)
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
//
|
||||
// @ts-expect-error
|
||||
this.app.embedRegistry.unregisterExtensions(["gltf", "glb"]);
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
@ -30,6 +73,97 @@ export default class ModelViewerPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
class ModelViewerComponent extends Component {
|
||||
plugin: ModelViewerPlugin;
|
||||
containerEl: HTMLElement;
|
||||
file: TFile;
|
||||
viewer: ModelViewerElement;
|
||||
|
||||
constructor(plugin: ModelViewerPlugin, containerEl: HTMLElement, file: TFile) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
this.containerEl = containerEl;
|
||||
this.file = file;
|
||||
containerEl.addClass("model-viewer-embed");
|
||||
this.updateWidth();
|
||||
|
||||
this.observer = new MutationObserver(() => {
|
||||
this.updateWidth();
|
||||
});
|
||||
this.observer.observe(this.containerEl, {
|
||||
attributes: true,
|
||||
attributeFilter: ["width"],
|
||||
});
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.observer.disconnect();
|
||||
this.viewer?.remove();
|
||||
}
|
||||
|
||||
observer: MutationObserver;
|
||||
|
||||
unload(): void {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
|
||||
updateWidth() {
|
||||
const width = this.containerEl.getAttribute("width");
|
||||
this.containerEl.style.width = width ? `${width}px` : "100%";
|
||||
}
|
||||
|
||||
loadFile() {
|
||||
this.viewer = document.createElement("model-viewer");
|
||||
this.viewer.src = this.plugin.app.vault.getResourcePath(this.file);
|
||||
this.containerEl.appendChild(this.viewer);
|
||||
const settings = this.plugin.settings;
|
||||
if (settings.cameraControls) this.viewer.cameraControls = true;
|
||||
if (settings.disablePan) this.viewer.disablePan = true;
|
||||
if (settings.disableZoom) this.viewer.disableZoom = true;
|
||||
if (settings.autoRotate) this.viewer.autoRotate = true;
|
||||
if (!settings.interactionPrompt) this.viewer.interactionPrompt = "none";
|
||||
}
|
||||
}
|
||||
|
||||
class ModelViewerFileView extends FileView {
|
||||
private plugin: ModelViewerPlugin;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ModelViewerPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return "model-viewer-file-view";
|
||||
}
|
||||
|
||||
canAcceptExtension(extension: string): boolean {
|
||||
return ["glb", "gltf"].includes(extension);
|
||||
}
|
||||
|
||||
async onLoadFile(file: TFile): Promise<void> {
|
||||
this.displayInEl(file);
|
||||
}
|
||||
|
||||
displayInEl(file: TFile) {
|
||||
const viewer = document.createElement("model-viewer");
|
||||
viewer.style.position = "absolute";
|
||||
viewer.style.width = "100%";
|
||||
viewer.style.height = "100%";
|
||||
viewer.src = this.app.vault.getResourcePath(file);
|
||||
this.contentEl.appendChild(viewer);
|
||||
|
||||
const settings = this.plugin.settings;
|
||||
if (settings.cameraControls) viewer.cameraControls = true;
|
||||
if (settings.disablePan) viewer.disablePan = true;
|
||||
if (settings.disableZoom) viewer.disableZoom = true;
|
||||
if (settings.autoRotate) viewer.autoRotate = true;
|
||||
if (!settings.interactionPrompt) viewer.interactionPrompt = "none";
|
||||
}
|
||||
|
||||
async onUnloadFile(file: TFile): Promise<void> {}
|
||||
}
|
||||
|
||||
class ModelViewerSettingTab extends PluginSettingTab {
|
||||
plugin: ModelViewerPlugin;
|
||||
|
||||
|
|
@ -44,15 +178,61 @@ class ModelViewerSettingTab extends PluginSettingTab {
|
|||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Setting #1")
|
||||
.setDesc("It's a secret")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Enter your secret")
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
.setName("Camera Controls")
|
||||
.setDesc("Enable camera controls for the model")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.cameraControls).onChange(async (value) => {
|
||||
this.plugin.settings.cameraControls = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Disable Pan")
|
||||
.setDesc("Disable panning the model")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.disablePan).onChange(async (value) => {
|
||||
this.plugin.settings.disablePan = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Disable Zoom")
|
||||
.setDesc("Disable zooming the model")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.disableZoom).onChange(async (value) => {
|
||||
this.plugin.settings.disableZoom = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Auto Rotate")
|
||||
.setDesc("Enable auto rotation of the model")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.autoRotate).onChange(async (value) => {
|
||||
this.plugin.settings.autoRotate = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Interaction Prompt")
|
||||
.setDesc("Show prompt to interact with the model")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.interactionPrompt).onChange(async (value) => {
|
||||
this.plugin.settings.interactionPrompt = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Reset Settings")
|
||||
.setDesc("Reset settings to default")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Reset to default")
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
this.plugin.settings = DEFAULT_SETTINGS;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
121
package-lock.json
generated
121
package-lock.json
generated
|
|
@ -8,6 +8,9 @@
|
|||
"name": "obsidian-model-viewer",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@google/model-viewer": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
|
|
@ -484,6 +487,22 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/model-viewer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/model-viewer/-/model-viewer-4.0.0.tgz",
|
||||
"integrity": "sha512-hf4mixP178gGtvWQq97a0uULr7k98BwJGRU3lvt12XnurOdLmZA/RPdW4tPsINNNQxZvmvQsjud4lZWre6WGNw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@monogrid/gainmap-js": "^3.0.1",
|
||||
"lit": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": "^0.169.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
|
|
@ -525,6 +544,21 @@
|
|||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
|
||||
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
|
|
@ -533,6 +567,18 @@
|
|||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@monogrid/gainmap-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"promise-worker-transferable": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
|
@ -612,6 +658,12 @@
|
|||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz",
|
||||
|
|
@ -1610,6 +1662,12 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
|
|
@ -1704,6 +1762,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -1776,6 +1840,46 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
|
|
@ -2024,6 +2128,16 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-worker-transferable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-promise": "^2.1.0",
|
||||
"lie": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -2239,6 +2353,13 @@
|
|||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.169.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz",
|
||||
"integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -20,5 +20,8 @@
|
|||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/model-viewer": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
screenshot.avif
Normal file
BIN
screenshot.avif
Normal file
Binary file not shown.
18
styles.css
18
styles.css
|
|
@ -0,0 +1,18 @@
|
|||
:is(.markdown-preview-view, .markdown-source-view) .model-viewer-embed {
|
||||
display: inline-block;
|
||||
border-radius: var(--radius-m);
|
||||
background-color: var(--background-primary-alt);
|
||||
}
|
||||
|
||||
.model-viewer-embed model-viewer {
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 150px;
|
||||
max-height: 450px;
|
||||
}
|
||||
|
||||
.popover .model-viewer-embed model-viewer {
|
||||
width: 300px;
|
||||
}
|
||||
Loading…
Reference in a new issue