From f0c22e4bc120f5ae80f221cba59a5697875cbde3 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 10 Jan 2025 11:57:08 +0100 Subject: [PATCH] Added .psd, .gltf, .glb, .obj support --- main.ts | 68 ++++++++++++++++- manifest.json | 2 +- package-lock.json | 65 +++++++++++++++- package.json | 5 +- src/abstractions/threejsComponent.ts | 110 +++++++++++++++++++++++++++ src/extensions/glb.ts | 30 ++++++++ src/extensions/kra.ts | 1 + src/extensions/obj.ts | 31 ++++++++ src/extensions/psd.ts | 48 ++++++++++++ src/extensionsRegistry.ts | 8 +- src/settings.ts | 13 ++++ styles.css | 4 + tsconfig.json | 2 +- 13 files changed, 379 insertions(+), 8 deletions(-) create mode 100644 src/abstractions/threejsComponent.ts create mode 100644 src/extensions/glb.ts create mode 100644 src/extensions/obj.ts create mode 100644 src/extensions/psd.ts diff --git a/main.ts b/main.ts index dae69e9..ea85475 100644 --- a/main.ts +++ b/main.ts @@ -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); + })); } } diff --git a/manifest.json b/manifest.json index 6ae2bca..c8b2995 100644 --- a/manifest.json +++ b/manifest.json @@ -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 diff --git a/package-lock.json b/package-lock.json index 45b5913..2546086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1df8253..fc88a2a 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/abstractions/threejsComponent.ts b/src/abstractions/threejsComponent.ts new file mode 100644 index 0000000..ca9ad0a --- /dev/null +++ b/src/abstractions/threejsComponent.ts @@ -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 { + 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 extends ExtensionView { + getIcon(): string { + return "box"; + } +} diff --git a/src/extensions/glb.ts b/src/extensions/glb.ts new file mode 100644 index 0000000..b6b1b80 --- /dev/null +++ b/src/extensions/glb.ts @@ -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 { + getComponent(): new (contentEl: HTMLElement, plugin: ExtendedFileSupport, file: TFile, linkText?: string | undefined) => GLBComponent { + return GLBComponent; + } + + getViewType(): string { + return VIEW_TYPE_GLB; + } +} diff --git a/src/extensions/kra.ts b/src/extensions/kra.ts index c57dafa..b04419f 100644 --- a/src/extensions/kra.ts +++ b/src/extensions/kra.ts @@ -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); diff --git a/src/extensions/obj.ts b/src/extensions/obj.ts new file mode 100644 index 0000000..cacc3fb --- /dev/null +++ b/src/extensions/obj.ts @@ -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 { + getComponent(): new (contentEl: HTMLElement, plugin: ExtendedFileSupport, file: TFile, linkText?: string | undefined) => OBJComponent { + return OBJComponent; + } + + getViewType(): string { + return VIEW_TYPE_OBJ; + } +} diff --git a/src/extensions/psd.ts b/src/extensions/psd.ts new file mode 100644 index 0000000..f19e028 --- /dev/null +++ b/src/extensions/psd.ts @@ -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 { + 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 { + getIcon(): string { + return "image"; + } + + getComponent(): new (...args: ConstructorParameters) => PSDComponent { + return PSDComponent; + } + + getViewType(): string { + return VIEW_TYPE_PSD; + } +} diff --git a/src/extensionsRegistry.ts b/src/extensionsRegistry.ts index 9c478ac..d0b1863 100644 --- a/src/extensionsRegistry.ts +++ b/src/extensionsRegistry.ts @@ -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 } ] diff --git a/src/settings.ts b/src/settings.ts index 3c996ef..01ee758 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -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, } diff --git a/styles.css b/styles.css index ec3f0c4..e7196c4 100644 --- a/styles.css +++ b/styles.css @@ -4,3 +4,7 @@ padding: 10px; text-align: center; } + +.full-width { + width: 100%; +} diff --git a/tsconfig.json b/tsconfig.json index c44b729..903a9f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "target": "ES6", "allowJs": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "importHelpers": true, "isolatedModules": true, "strictNullChecks": true,