Added .psd, .gltf, .glb, .obj support

This commit is contained in:
Nick 2025-01-10 11:57:08 +01:00
parent 135fffa98b
commit f0c22e4bc1
13 changed files with 379 additions and 8 deletions

68
main.ts
View file

@ -20,7 +20,7 @@ export default class ExtendedFileSupport extends Plugin {
if (this.settings[extension_type]) {
this.registerExtensions([extension_type], extension.view_type);
embedRegistry.registerExtension(extension_type, (context, file, _) => {
embedRegistry.registerExtension(extension_type, (context, file, p) => {
return new extension.component(context.containerEl, this, file, context.linktext);
});
}
@ -34,7 +34,7 @@ export default class ExtendedFileSupport extends Plugin {
for (const extension of EXTENSION_REGISTRY) {
for (const extension_type of extension.types) {
// @ts-ignore
if (this.settings[extension.type]) {
if (this.settings[extension_type]) {
this.app.viewRegistry.unregisterExtensions([extension_type]);
embedRegistry.unregisterExtension(extension_type);
}
@ -82,9 +82,24 @@ class ExtendedFileSupportSettingTab extends PluginSettingTab {
containerEl.empty();
new Setting(containerEl)
.setName("Image formats")
.setHeading();
new Setting(containerEl)
.setName(".psd")
.setDesc("Photoshop documents. Generated by Adobe Photoshop or similar programs.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.psd)
.onChange(async (value) => {
this.plugin.settings.psd = value;
await this.plugin.saveSettings();
this.plugin.toggleExtension("psd", value);
}));
new Setting(containerEl)
.setName(".kra")
.setDesc("Files generated by Krita")
.setDesc("Files generated by Krita.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.kra)
.onChange(async (value) => {
@ -92,5 +107,52 @@ class ExtendedFileSupportSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
this.plugin.toggleExtension("kra", value);
}));
new Setting(containerEl)
.setName("3D formats")
.setHeading();
new Setting(containerEl)
.setName("Animate")
.setDesc("Animate 3d objects (Rotate the camera around them).")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.animate_3d_objects)
.onChange(async (value) => {
this.plugin.settings.animate_3d_objects = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(".obj")
.setDesc("Object file type for 3d models.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.obj)
.onChange(async (value) => {
this.plugin.settings.obj = value;
await this.plugin.saveSettings();
this.plugin.toggleExtension("obj", value);
}));
new Setting(containerEl)
.setName(".gltf")
.setDesc("glTF format. Only embedded glTF files are supported.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.gltf)
.onChange(async (value) => {
this.plugin.settings.gltf = value;
await this.plugin.saveSettings();
this.plugin.toggleExtension("gltf", value);
}));
new Setting(containerEl)
.setName(".glb")
.setDesc("Binary glTF format. Only self-contained glb files are supported.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.glb)
.onChange(async (value) => {
this.plugin.settings.glb = value;
await this.plugin.saveSettings();
this.plugin.toggleExtension("glb", value);
}));
}
}

View file

@ -3,7 +3,7 @@
"name": "Extended File Support",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Adds file support for various file types. Allows viewing and embedding these filetypes. Includes: .kra, .psd, .obj, .gltf",
"description": "Adds file support for various file types. Allows viewing and embedding these filetypes. Includes: .kra, .psd, .obj, .glb, .gltf, and more.",
"author": "Nick de Bruin",
"authorUrl": "https://nickdebruin.me/",
"isDesktopOnly": false

65
package-lock.json generated
View file

@ -9,10 +9,13 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@zip.js/zip.js": "^2.6.40"
"@webtoon/psd": "^0.4.0",
"@zip.js/zip.js": "^2.6.40",
"three": "^0.172.0"
},
"devDependencies": {
"@types/node": "^16.11.6",
"@types/three": "^0.172.0",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@ -633,6 +636,12 @@
"node": ">=10"
}
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@ -700,6 +709,12 @@
"@types/node": "*"
}
},
"node_modules/@types/stats.js": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
"integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==",
"dev": true
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@ -709,6 +724,20 @@
"@types/estree": "*"
}
},
"node_modules/@types/three": {
"version": "0.172.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.172.0.tgz",
"integrity": "sha512-LrUtP3FEG26Zg5WiF0nbg8VoXiKokBLTcqM2iLvM9vzcfEiYmmBAPGdBgV0OYx9fvWlY3R/3ERTZcD9X5sc0NA==",
"dev": true,
"dependencies": {
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": "*",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/turndown": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
@ -716,6 +745,12 @@
"dev": true,
"peer": true
},
"node_modules/@types/webxr": {
"version": "0.5.20",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.20.tgz",
"integrity": "sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==",
"dev": true
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@ -918,6 +953,17 @@
"dev": true,
"peer": true
},
"node_modules/@webgpu/types": {
"version": "0.1.52",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.52.tgz",
"integrity": "sha512-eI883Nlag2hGIkhXxAnq8s4APpqXWuPL3Gbn2ghiU12UjLvfCbVqHK4XfXl3eLRTatqcMmeK7jws7IwWsGfbzw==",
"dev": true
},
"node_modules/@webtoon/psd": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@webtoon/psd/-/psd-0.4.0.tgz",
"integrity": "sha512-ztriE8oFOamRrV9opBURDy+JMiyhur2//vOXsC5CgdnYCB0L1Lnaag4NzP8N+NFCj7uNz9JRYtPmAbQMSDLIsQ=="
},
"node_modules/@zip.js/zip.js": {
"version": "2.6.40",
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.6.40.tgz",
@ -1757,6 +1803,12 @@
"pend": "~1.2.0"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -2330,6 +2382,12 @@
"node": ">= 8"
}
},
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@ -2932,6 +2990,11 @@
"dev": true,
"peer": true
},
"node_modules/three": {
"version": "0.172.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz",
"integrity": "sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow=="
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View file

@ -13,6 +13,7 @@
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@types/three": "^0.172.0",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@ -23,6 +24,8 @@
"typescript": "4.7.4"
},
"dependencies": {
"@zip.js/zip.js": "^2.6.40"
"@webtoon/psd": "^0.4.0",
"@zip.js/zip.js": "^2.6.40",
"three": "^0.172.0"
}
}

View file

@ -0,0 +1,110 @@
import { ExtensionComponent } from "src/extensionComponent";
import { ExtensionView } from "src/extensionView";
import * as THREE from 'three';
export const VIEW_TYPE_OBJ = "extended-file-support-obj";
export abstract class ThreeJSComponent extends ExtensionComponent {
renderer?: THREE.WebGLRenderer;
scene?: THREE.Scene;
resizeObserver?: ResizeObserver;
abstract loadModel(resource: string): void;
async loadFile(): Promise<void> {
this.scene = new THREE.Scene();
// Subtract 1 to remove the scrollbar...
let width = this.contentEl.innerWidth - 1;
width = width > 0 ? width : 1;
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(width, width);
this.renderer.setClearColor(0x000000, 0);
const camera = new THREE.PerspectiveCamera( 75, 1, 0.1, 1000 );
camera.position.set(1, 0.5, 1);
camera.lookAt(0, 0, 0);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
directionalLight.position.set(0, 5, 0); // Above the object
directionalLight.castShadow = true;
this.scene.add(directionalLight);
const resource = this.plugin.app.vault.getResourcePath(this.file);
this.loadModel(resource);
this.contentEl.empty();
this.contentEl.removeClass("extended-file-loading");
this.contentEl.append(this.renderer.domElement);
let angle = 0;
const radius = 1;
this.renderer.setAnimationLoop(() => {
if (this.scene && this.renderer) {
if (this.plugin.settings.animate_3d_objects) {
angle += 0.01;
camera.position.x = radius * Math.cos(angle);
camera.position.z = radius * Math.sin(angle);
camera.lookAt(0, 0, 0);
}
this.renderer.render(this.scene, camera);
}
});
this.resizeObserver = new ResizeObserver(() => {
let width = this.contentEl.innerWidth - 1;
if (this.linkText && !isNaN(Number(this.linkText))) {
width = Number(this.linkText);
}
this.renderer?.setSize(width, width);
camera.updateProjectionMatrix();
});
this.resizeObserver.observe(this.contentEl);
}
onunload(): void {
this.renderer?.setAnimationLoop(null);
this.renderer?.dispose();
if (this.scene) while (this.scene.children.length > 0) {
this.scene.remove(this.scene.children[0]);
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = undefined;
}
}
protected scaleGroup(group: THREE.Group) {
const size_box = new THREE.Box3().setFromObject(group);
const size = size_box.getSize(new THREE.Vector3());
const scale_ratio = 1 / Math.max(size.x, size.y, size.z);
group.scale.set(scale_ratio, scale_ratio, scale_ratio);
}
protected centerGroup(group: THREE.Group) {
const center_box = new THREE.Box3().setFromObject(group);
const center = center_box.getCenter(new THREE.Vector3());
group.position.sub(center);
}
}
export abstract class ThreeJSView<T extends ThreeJSComponent> extends ExtensionView<T> {
getIcon(): string {
return "box";
}
}

30
src/extensions/glb.ts Normal file
View file

@ -0,0 +1,30 @@
import ExtendedFileSupport from "main";
import { TFile } from "obsidian";
import { ThreeJSComponent, ThreeJSView } from "src/abstractions/threejsComponent";
import { GLTFLoader } from "three/examples/jsm/Addons.js";
export const VIEW_TYPE_GLB = "extended-file-support-GLB";
export class GLBComponent extends ThreeJSComponent {
loadModel(resource: string): void {
const loader = new GLTFLoader();
loader.load(resource, (gltf) => {
const model = gltf.scene;
this.scaleGroup(model);
this.centerGroup(model);
this.scene?.add(model);
})
}
}
export class GLBView extends ThreeJSView<GLBComponent> {
getComponent(): new (contentEl: HTMLElement, plugin: ExtendedFileSupport, file: TFile, linkText?: string | undefined) => GLBComponent {
return GLBComponent;
}
getViewType(): string {
return VIEW_TYPE_GLB;
}
}

View file

@ -38,6 +38,7 @@ export class KRAComponent extends ExtensionComponent {
this.contentEl.createEl("i", { text: `Could not load ${this.file.path}` });
}
}
onunload(): void {
if (this.objectURL) {
URL.revokeObjectURL(this.objectURL);

31
src/extensions/obj.ts Normal file
View file

@ -0,0 +1,31 @@
import ExtendedFileSupport from "main";
import { TFile } from "obsidian";
import { ThreeJSComponent, ThreeJSView } from "src/abstractions/threejsComponent";
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
export const VIEW_TYPE_OBJ = "extended-file-support-obj";
export class OBJComponent extends ThreeJSComponent {
loadModel(resource: string): void {
const loader = new OBJLoader();
loader.load(resource, obj => {
this.scaleGroup(obj);
this.centerGroup(obj);
this.scene?.add(obj);
}, undefined, e => {
console.log("Error while loading", e);
});
}
}
export class OBJView extends ThreeJSView<OBJComponent> {
getComponent(): new (contentEl: HTMLElement, plugin: ExtendedFileSupport, file: TFile, linkText?: string | undefined) => OBJComponent {
return OBJComponent;
}
getViewType(): string {
return VIEW_TYPE_OBJ;
}
}

48
src/extensions/psd.ts Normal file
View file

@ -0,0 +1,48 @@
import { ExtensionComponent } from "src/extensionComponent";
import { ExtensionView } from "src/extensionView";
import Psd from "@webtoon/psd";
export const VIEW_TYPE_PSD = "extended-file-support-psd";
export class PSDComponent extends ExtensionComponent {
async loadFile(): Promise<void> {
const resource = this.plugin.app.vault.getResourcePath(this.file);
const res = await fetch(resource);
const arrayBuffer = await res.arrayBuffer();
const psd = Psd.parse(arrayBuffer);
const canvasEl = document.createElement("canvas");
const context = canvasEl.getContext("2d");
const compositeBuffer = await psd.composite();
const imageData = new ImageData(compositeBuffer, psd.width, psd.height);
canvasEl.width = psd.width;
canvasEl.height = psd.height;
canvasEl.addClass("full-width");
context?.putImageData(imageData, 0, 0);
this.contentEl.empty();
this.contentEl.removeClass("extended-file-loading");
this.contentEl.addClasses(["media-embed", "image-embed"]);
this.contentEl.append(canvasEl);
}
onunload(): void {}
}
export class PSDView extends ExtensionView<PSDComponent> {
getIcon(): string {
return "image";
}
getComponent(): new (...args: ConstructorParameters<typeof ExtensionComponent>) => PSDComponent {
return PSDComponent;
}
getViewType(): string {
return VIEW_TYPE_PSD;
}
}

View file

@ -1,6 +1,9 @@
import { KRAComponent, KRAView, VIEW_TYPE_KRA } from "./extensions/kra"
import { ExtensionView } from "./extensionView"
import { ExtensionComponent } from "./extensionComponent"
import { OBJComponent, OBJView, VIEW_TYPE_OBJ } from "./extensions/obj"
import { GLBComponent, GLBView, VIEW_TYPE_GLB } from "./extensions/glb"
import { PSDComponent, PSDView, VIEW_TYPE_PSD } from "./extensions/psd"
export type Extension = {
types: string[],
@ -11,5 +14,8 @@ export type Extension = {
// Type should match settings field
export const EXTENSION_REGISTRY: Extension[] = [
{ types: ["kra"], view_type: VIEW_TYPE_KRA, view: KRAView, component: KRAComponent }
{ types: ["kra"], view_type: VIEW_TYPE_KRA, view: KRAView, component: KRAComponent },
{ types: ["obj"], view_type: VIEW_TYPE_OBJ, view: OBJView, component: OBJComponent },
{ types: ["glb", "gltf"], view_type: VIEW_TYPE_GLB, view: GLBView, component: GLBComponent },
{ types: ["psd"], view_type: VIEW_TYPE_PSD, view: PSDView, component: PSDComponent }
]

View file

@ -1,7 +1,20 @@
export interface ExtendedFileSupportSettings {
kra: boolean;
psd: boolean;
// 3D objects
animate_3d_objects: boolean;
obj: boolean;
gltf: boolean;
glb: boolean;
}
export const DEFAULT_SETTINGS: ExtendedFileSupportSettings = {
kra: true,
psd: true,
animate_3d_objects: true,
obj: true,
gltf: true,
glb: true,
}

View file

@ -4,3 +4,7 @@
padding: 10px;
text-align: center;
}
.full-width {
width: 100%;
}

View file

@ -7,7 +7,7 @@
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,