3dRipper-assistant

A userscript to assist with using 3dRipper

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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();
    }
  })();

})();