3dRipper-assistant

A userscript to assist with using 3dRipper

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         3dRipper-assistant
// @namespace    3dRipper
// @version      0.0.178-BETA
// @author       3dRipper
// @description  A userscript to assist with using 3dRipper
// @license      MIT
// @icon         https://3dripper.dev/assets/favicons/favicon-32x32.png
// @match        https://3dripper.dev/*
// @match        https://sketchfab.com/3d-models/*
// @match        https://sketchfab.com/models/*
// @match        https://www.artstation.com/embed/*
// @match        https://www.artstation.com/artwork/*
// @match        https://www.cgtrader.com/*
// @match        https://www.fab.com/dope/*
// @match        https://www.fab.com/listings/*
// @grant        GM_info
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  (function() {
    const isUserScript = typeof GM_info !== "undefined";
    const URL_HOST = "https://3dripper.dev";
    const Marmoset_Embed_Url = `https://www.artstation.com/embed/`;
    const Artstation_View_Url = `https://www.artstation.com/artwork/`;
    const CGTrader_View_Url = `https://www.cgtrader.com/`;
    const SF_View_URL = `https://sketchfab.com/3d-models/`;
    const SF_Viewer_URL = `https://sketchfab.com/models/`;
    const Dope_Viewer_URL = `https://www.fab.com/dope/`;
    const Dope_List_URL = `https://www.fab.com/listings/`;
    const tag = "shuziwenwu";
    const inject_tag = "_3DRIPPER_";
    const { href } = window.location;
    if (href.includes(URL_HOST)) {
      localStorage.setItem("_3DRIPPER_", "_3DRIPPER_");
      return;
    }
    if (typeof window.assistant_tag !== "undefined") {
      return;
    } else {
      window.assistant_tag = inject_tag;
    }
    function addASIframeButtons() {
      const iframes = document.querySelectorAll("iframe");
      let count = 0, container = document.querySelector(".modal-layout");
      for (let i = 0; i < iframes.length; i++) {
        let iframe = iframes[i];
        let { contentWindow, src } = iframe;
        if (!(src.includes("artstation") || src.includes("marmoset")) || src.includes("cgtrader")) {
          continue;
        }
        let { currentViewer } = contentWindow;
        const button = document.createElement("button");
        button.textContent = "download model";
        if (count == 0 && container) {
          scrollToElementInContainer(container, iframe);
        }
        count++;
        button.addEventListener("click", async () => {
          let author = document.querySelector("div.project-author-name > h3 > a:nth-child(1)").innerHTML;
          let { sceneURL: mview, ui } = currentViewer;
          let { title } = contentWindow.document;
          let { loadingImageURL: thumb } = ui;
          let data = JSON.stringify({ thumb, mview, title, author });
          let compressed = await compress(data, "gzip");
          compressed = arrayBufferToBase64(compressed);
          window.open(`${URL_HOST}/MMRipper#${compressed}?auto=1`);
        });
        iframe.parentNode.insertBefore(button, iframe);
      }
      if (count == 0) {
        alert("This page does not seem to have any models available for online preview!");
      } else {
        alert("Discover online models that can be downloaded");
      }
    }
    function addASEmbedButtons() {
      const button = document.createElement("button");
      button.textContent = "download model";
      button.style.position = "fixed";
      button.style.top = "0";
      button.style.left = "0";
      button.addEventListener("click", async () => {
        var _a;
        let { currentViewer, document: document2 } = window;
        let { sceneURL: mview, ui } = currentViewer;
        let { title } = document2;
        let { loadingImageURL: thumb } = ui;
        const regex = new RegExp("(?<=medium\\/)([^-]+-[^-]+)(?=-mview-image)");
        const match = thumb.match(regex);
        let author = ((_a = match[0]) == null ? void 0 : _a.replace(/\-/, " ")) || "unknown";
        let data = JSON.stringify({ thumb, mview, title, author });
        let compressed = await compress(data, "gzip");
        compressed = arrayBufferToBase64(compressed);
        window.open(`${URL_HOST}/MMRipper#${compressed}?auto=1`);
      });
      document.body.appendChild(button);
      alert("Discover online models that can be downloaded");
    }
    function addDPMmbedButtons() {
      const button = document.createElement("button");
      button.textContent = "download model";
      button.style.position = "fixed";
      button.style.zIndex = "9999";
      button.style.bottom = "0";
      button.style.left = "0";
      button.style.color = "#fff";
      button.style.backgroundColor = "#409eff";
      button.addEventListener("click", async () => {
        let { href: href2 } = window.location;
        fetch(href2).then((res) => res.text()).then(async (html) => {
          var _a;
          let regex = /[a-zA-z]+:\/\/[^\s]*\.mview/g;
          let mview = (_a = html.match(regex)) == null ? void 0 : _a[0];
          if (!mview) {
            alert("This page does not seem to have any models available for online preview!");
            return;
          }
          let { document: document2 } = window;
          let { title } = document2;
          let author = "unknown";
          let data = JSON.stringify({ mview, title, author });
          let compressed = await compress(data, "gzip");
          compressed = arrayBufferToBase64(compressed);
          window.open(`${URL_HOST}/MMRipper#${compressed}?auto=1`);
        });
      });
      document.body.appendChild(button);
      alert("Discover online models that can be downloaded");
    }
    function addCGViewer() {
      const views = document.querySelectorAll('img[data-type="marmoset"]');
      let count = 0;
      for (let i = 0; i < views.length; i++) {
        let view = views[i];
        let mview = view.getAttribute("data-src");
        const button = document.createElement("button");
        button.textContent = i == 0 ? "download model" : `download model ${i + 1}`;
        if (count == 0 && view) {
          scrollToElementInContainer(document.body, view);
        }
        count++;
        button.addEventListener("click", async () => {
          let author = document.querySelector(".author-info .username").innerHTML;
          let title = document.querySelector(".product-header__title").innerHTML;
          let thumb;
          let data = JSON.stringify({ thumb, mview, title, author });
          let compressed = await compress(data, "gzip");
          compressed = arrayBufferToBase64(compressed);
          window.open(`${URL_HOST}/MMRipper#${compressed}?auto=1`);
        });
        let container = document.querySelector("#product-main");
        container.parentNode.insertBefore(button, container);
      }
      if (count == 0) {
        alert("This page does not seem to have any models available for online preview!");
      } else {
        alert("Discover online models that can be downloaded");
      }
    }
    function addSFIframeButtons() {
      const sfLinks = document.querySelectorAll('iframe[src*="sketchfab.com/models/"]');
      for (let i = 0; i < sfLinks.length; i++) {
        let iframe = sfLinks[i];
        let url = iframe.src;
        let id = url.split("/")[4];
        let button = document.createElement("button");
        button.textContent = "Download Via 3dRipper";
        button.addEventListener("click", () => {
          window.open(`${URL_HOST}/SFRipper#${id}?auto=1`);
        });
        button.style.position = "absolute";
        button.style.left = "0";
        button.style.top = "0";
        button.style.zIndex = "9999";
        iframe.parentNode.insertBefore(button, iframe);
      }
      if (sfLinks.length == 0) {
        alert("This page does not seem to have any models available for online preview!");
      } else {
        alert("Discover online models that can be downloaded");
      }
    }
    function addSFViewerButtons() {
      let url = location.href;
      let id = url.split("/")[4];
      let button = document.createElement("button");
      button.style.position = "fixed";
      button.style.top = "0";
      button.style.left = "0";
      button.textContent = "Download Via 3dRipper";
      button.addEventListener("click", () => {
        window.open(`${URL_HOST}/SFRipper#${id}?auto=1`);
      });
      button.style.position = "absolute";
      button.style.left = "0";
      button.style.top = "0";
      button.style.zIndex = "9999";
      document.body.appendChild(button);
      alert("Discover online models that can be downloaded");
    }
    function arrayBufferToBase64(buffer) {
      let binary = "";
      const bytes = new Uint8Array(buffer);
      const len = bytes.byteLength;
      for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
      }
      return window.btoa(binary);
    }
    function compress(string, encoding) {
      const byteArray = new TextEncoder().encode(string);
      const cs = new CompressionStream(encoding);
      const writer = cs.writable.getWriter();
      writer.write(byteArray);
      writer.close();
      return new Response(cs.readable).arrayBuffer();
    }
    function scrollToElementInContainer(container, target, scrollDuration = 300) {
      const containerRect = container.getBoundingClientRect();
      const targetRect = target.getBoundingClientRect();
      const targetPosition = {
        top: targetRect.top - containerRect.top,
        left: targetRect.left - containerRect.left
      };
      const startPosition = {
        top: container.scrollTop,
        left: container.scrollLeft
      };
      const distance = {
        top: targetPosition.top - startPosition.top,
        left: targetPosition.left - startPosition.left
      };
      let start = null;
      function scrollAnimation(timestamp) {
        if (!start) start = timestamp;
        const progress = timestamp - start;
        const ease = easeInOutQuad(progress, 0, 1, scrollDuration);
        container.scrollTop = startPosition.top + distance.top * ease;
        container.scrollLeft = startPosition.left + distance.left * ease;
        if (progress < scrollDuration) {
          requestAnimationFrame(scrollAnimation);
        }
      }
      function easeInOutQuad(t, b, c, d) {
        t /= d / 2;
        if (t < 1) return c / 2 * t * t + b;
        t--;
        return -1 / 2 * (t * (t - 2) - 1) + b;
      }
      requestAnimationFrame(scrollAnimation);
    }
    const excute = async () => {
      if (href.includes(Artstation_View_Url)) {
        addASIframeButtons();
      }
      if (href.includes(CGTrader_View_Url)) {
        addCGViewer();
      }
      if (href.includes(Marmoset_Embed_Url)) {
        addASEmbedButtons();
      }
      if (href.includes(SF_View_URL)) {
        addSFIframeButtons();
      }
      if (href.includes(SF_Viewer_URL)) {
        addSFViewerButtons();
      }
      if (href.includes(tag)) {
        addDPMmbedButtons();
      }
      if (href.includes(Dope_List_URL)) {
        await dopeListInject();
      }
      console.log("3dRipper assistant injected!");
    };
    const createFloatBtn = () => {
      if (href.includes(URL_HOST) && document.referrer.includes(URL_HOST) || href.includes(Marmoset_Embed_Url) && document.referrer.includes(Artstation_View_Url)) {
        return;
      }
      let btn = document.createElement("button");
      let img = document.createElement("img");
      img.crossOrigin = "anonymous";
      img.src = "https://3dripper.dev/assets/favicons/favicon-32x32.png";
      btn.style.cursor = "pointer";
      img.draggable = false;
      btn.appendChild(img);
      btn.style.width = "50px";
      btn.style.height = "50px";
      btn.style.position = "fixed";
      btn.style.bottom = "60%";
      btn.style.right = "0px";
      btn.style.zIndex = "9999";
      btn.style.padding = "10px";
      btn.style.borderRadius = "25px";
      btn.style.background = "#59caef";
      btn.style.color = "#fff";
      btn.style.border = "none";
      let offsetX, offsetY, isDragging = false;
      btn.addEventListener("mousedown", (e) => {
        isDragging = true;
        offsetX = e.clientX - btn.getBoundingClientRect().left;
        offsetY = e.clientY - btn.getBoundingClientRect().top;
      });
      document.addEventListener("mousemove", (e) => {
        if (isDragging) {
          btn.style.left = `${e.clientX - offsetX}px`;
          btn.style.top = `${e.clientY - offsetY}px`;
        }
      });
      document.addEventListener("mouseup", () => {
        isDragging = false;
      });
      btn.onclick = () => {
        if (isDragging) return;
        excute();
      };
      document.body.appendChild(btn);
    };
    const dopeInject = () => {
      let step = -1;
      let _log = (...msg) => {
      };
      const data = {
        buffers: [],
        textures: [],
        animations: {}
      };
      _log(data);
      let bufferDataCount = 0, transferrCommand = false;
      const uid = location.href.split("/").pop();
      let _bufferSubData = WebGL2RenderingContext.prototype.bufferSubData;
      WebGL2RenderingContext.prototype.bufferSubData = function(...args) {
        if (step == 1) {
          _log("bufferSubData:", args);
          _log(args);
          if (bufferDataCount > 0) {
            data.buffers.push(args[2]);
            bufferDataCount--;
          }
        }
        _bufferSubData.apply(this, args);
      };
      let _bufferData = WebGL2RenderingContext.prototype.bufferData;
      WebGL2RenderingContext.prototype.bufferData = function(...args) {
        if (step == 1) {
          _log("bufferData:", args);
          bufferDataCount++;
          _log(args);
        }
        _bufferData.apply(this, args);
      };
      let _getAllResponseHeaders = XMLHttpRequest.prototype.getAllResponseHeaders;
      XMLHttpRequest.prototype.getAllResponseHeaders = function() {
        const { responseURL, responseText } = this;
        _log(responseURL);
        if (responseURL.includes("options")) {
          let options = JSON.parse(responseText);
          data.options = options;
        } else if (responseURL.includes("/textures")) {
          let json = JSON.parse(responseText);
          data.textures = json.results;
          if (data.textures.length == 0) {
            if (!transferrCommand && document.referrer.includes(URL_HOST)) {
              transferrCommand = true;
              top.postMessage({ data, type: "data" }, URL_HOST);
            }
          }
        } else if (responseURL.split("/").pop() == uid) {
          let json = JSON.parse(responseText);
          data.model = json;
        }
        return _getAllResponseHeaders.call(this);
      };
      const hook = (__dope) => {
        const _release = __dope.releaseXHRDopeProgress;
        __dope.releaseXHRDopeProgress = (d) => {
          _log(`releaseXHRDopeProgress:${d}`);
          if (d.indexOf("/file.binz") !== -1) {
            step = 0;
            const model = [111, 114, 34, 58, 32, 34, 79, 112, 101, 110, 83, 99, 101, 110, 101, 71, 114, 97, 112, 104];
            let modelBuffer = searchAddr(model);
            data.modelBuffer = modelBuffer;
          } else if (d.indexOf("/model_file.binz") !== -1) {
            step = 1;
          } else if (d.indexOf("animations") !== -1) {
            step = 2;
          } else if (d.indexOf("textures") !== -1) {
            step = 3;
            if (!transferrCommand && document.referrer.includes(URL_HOST)) {
              transferrCommand = true;
              top.postMessage({ data, type: "data" }, URL_HOST);
            }
          }
          return _release.call(this, d);
        };
        const _dopeInterceptXHR = __dope.dopeInterceptXHR;
        __dope.dopeInterceptXHR = (url, response) => {
          if (url.indexOf("/model_file.binz") !== -1) {
            data.buffer = response;
          } else if (url.indexOf("/file.binz") !== -1) {
            data.file = response;
          }
          _dopeInterceptXHR && _dopeInterceptXHR.apply(this, [url, response]);
        };
        const searchAddr = (data2) => {
          const m = __dope["HEAPU8"];
          const match = (j) => {
            return data2.every((b, i) => m[i + j] === b);
          };
          let max = Math.max(0, m.byteLength || m.length);
          let addr = 0;
          for (addr = 0; addr < max; addr++) {
            if (match(addr)) {
              break;
            }
          }
          const len = max - addr;
          for (let i = addr; i < len; i++) {
            if (m[i] === 0) {
              max = i;
              break;
            }
          }
          return m.slice(addr, max);
        };
      };
      Object.defineProperty(window, "__dope", {
        get() {
          _log("获取 __dope");
          return this._dope;
        },
        set(value) {
          hook(value);
          this._dope = value;
        }
      });
    };
    const dopeListInject = async () => {
      const listId = location.href.split("/").pop().split("#").shift() || "";
      const content = await request(`https://www.fab.com/i/listings/${listId}`);
      const { medias } = content;
      let flag = true;
      if (medias.length == 1) {
        const item = medias[0];
        if (item.type == "model") {
          const container = document.querySelector("div.fabkit-Thumbnail-root");
          const { previewUid } = item;
          const a = document.createElement("a");
          a.href = `${URL_HOST}/FabRipper#${previewUid}?auto=1`;
          a.textContent = "download";
          a.style.position = "absolute";
          a.style.top = 0;
          a.style.left = 0;
          a.style.color = "#f00";
          a.target = "_blank";
          container.appendChild(a);
          flag = false;
        }
      } else {
        const container = document.querySelector("div.fabkit-Stack-root > ol");
        const thumbs = container.querySelectorAll(".fabkit-Thumbnail-root");
        for (let i = 0; i < medias.length; i++) {
          const item = medias[i];
          if (item.type !== "model") {
            continue;
          }
          const { mediaUrl, previewUid } = item;
          const li = thumbs[i];
          const a = document.createElement("a");
          a.href = `${URL_HOST}/FabRipper#${previewUid}?auto=1`;
          a.textContent = "download";
          a.style.position = "absolute";
          a.style.color = "#f00";
          a.target = "_blank";
          li.appendChild(a);
          flag = false;
        }
      }
      if (flag) {
        alert("This page does not seem to have any models available for online preview!");
      }
    };
    const request = (url) => {
      return new Promise((resolve) => {
        fetch(url).then((res) => res.json()).then((data) => resolve(data));
      });
    };
    const inject = () => {
      if (href.includes(Dope_Viewer_URL)) {
        if (!document.referrer.includes(Dope_List_URL)) {
          dopeInject();
        }
      } else if (href.includes(URL_HOST)) {
        console.log(inject_tag);
      } else {
        createFloatBtn();
      }
    };
    if (!isUserScript) {
      excute();
    } else {
      inject();
    }
  })();

})();