Duet3D Logo

    Duet3D

    • Register
    • Login
    • Search
    • Categories
    • Tags
    • Documentation
    • Order
    1. Home
    2. theKM
    • Profile
    • Following 0
    • Followers 0
    • Topics 9
    • Posts 109
    • Best 36
    • Controversial 0
    • Groups 0

    theKM

    @theKM

    62
    Reputation
    17
    Profile views
    109
    Posts
    0
    Followers
    0
    Following
    Joined Last Online

    theKM Unfollow Follow

    Best posts made by theKM

    • Configurable Main Menu ( /w source code )

      Note edit: If anyone wants to try it out, I put the DWC zip files build here...
      https://github.com/abates-dentsu/DuetWebControl/tree/az_generatedUI_build/dist-packaged
      ...this is based on the latest beta version. I'd only recommend people try it if they've already successfully moved up to the latest core beta of DWC.

      --

      Other thread was going on about configurability, I latched onto the thought that given there's some handy plugins that can do what we want in the main panel, that the main menu is the part that would be nice to be able to configure in the settings json.

      So for whatever it's worth, I wrote a thing, and have a PR for anyone running their own builds and wants to play with it:
      https://github.com/Duet3D/DuetWebControl/pull/388

      It's the main menu, so it can't be plugin'ified.

      I tried to make sure it didn't get in the way of the DWC maintainers, while still getting the flexibility to people desiring to wrangle with the menu.

      Maintainers can muse over it, people who have a build setup can merge and play with it in their own branch, whatever anyone wants. There's dev info in the PR, but I need to put in some more user docco somewhere.

      .
      69b64df9-4290-465b-a9e4-4fadedc08385-image.png

      In short, in dwc-settings.json add a mainMenuConfig: [] array/list config in the main block, which represents the list of things in the main menu, including groups of items or buttons or whatever. The internal/system page components now have keys that you can use to define where they go. For the stock menu, the configuration would look like...

      "mainMenuConfig": [
        {
          "name": "Control",
          "icon": "mdi-tune",
          "caption": "menu.control.caption",
          "pages": [
            "control-dashboard",
            "control-console"
          ]
        },
        {
          "name": "Job",
          "icon": "mdi-printer",
          "caption": "menu.job.caption",
          "pages": [
            "job-status",
            "job-webcam"
          ]
        },
        {
          "name": "Files",
          "icon": "mdi-sd",
          "caption": "menu.files.caption",
          "pages": [
            "files-jobs",
            "files-macros",
            "files-filaments",
            "files-system"
          ]
        },
        {
          "name": "Plugins",
          "icon": "mdi-puzzle",
          "caption": "menu.plugins.caption",
          "pages": []
        },
        {
          "name": "Settings",
          "icon": "mdi-wrench",
          "caption": "menu.settings.caption",
          "pages": [
            "settings-general",
            "settings-machine"
          ]
        }
      ]
      

      ...this sets up the groups to work as they did, allowing plugins to register themselves as they always have.

      But you can see how the string key placeholders for the stock components are working, and how the main components will set themselves into the menu. Here's an example of flattening out the menu with some select components to have just the list of items without the groups.

      "mainMenuConfig": [
            "control-dashboard",
            "control-console",
            "/MoveItMoveIt",
            "job-status",
            "files-jobs",
            "files-macros",
            "files-system",
            "settings-general",
            "settings-machine"
      ]
      

      ...the "/MoveItMoveIt" in the list is my own plugin's path, showing how to add a plugin to the list that you may have installed. So you can set what you want, in the order you want, etc etc.

      Maybe I wanted to rename the dashboard item, you could define all the things about it including the icon....

      "mainMenuConfig": [
            { "caption": "Extra Dashboard!", "icon": "mdi-wrench", "path": "/", "button": true },
            "control-console",
            "/MoveItMoveIt",
            "job-status",
            "files-jobs",
            "files-macros",
            "files-system",
            "settings-general",
            "settings-machine"
      ]
      

      ...how about a submenu that actually works like a super handy macro panel (buttons can run gcode as well as route to pages)...

      "mainMenuConfig": [
            { "caption": "Extra Dashboard!", "icon": "mdi-wrench", "path": "/", "button": true },
            "control-console",
            "/MoveItMoveIt",
            "job-status",
            "files-jobs",
            "files-macros",
            "files-system",
            {
                "icon": "mdi-wrench",
                "caption": "Super Handy Macros",
                "pages": [
                     { "caption": "Home IT!!", "icon": "mdi-cog", "gcode": "G28", "button": true },
                     { "caption": "Home Sweet Home", "icon": "mdi-wrench", "path": "G28", "button": true }
                ]
              },
            "settings-general",
            "settings-machine"
      ]
      

      .
      Anyways, have a play with it if ya have the urge. I did accidentally fork with the account I use for work and couldn't be bothered moving it, so please no naughty review comment language 🙂

      .

      abates-dentsu created this issue in Duet3D/DuetWebControl

      open Configurable main menu #388

      posted in Duet Web Control
      theKM
      theKM
    • Handy way to browse GCode docco directly from the gcode

      edit: added cross-link gcode references in the docco itself.
      edit 2: gave the pasted gcode half the screen and re-packed/stripped rest of page style so it can fit and work better.

      _
      Browsing other people's gcode is super handy, particularly for custom setups and going your own way. However, it's a genuine hassle to both read someone else's GCode while trying to bounce around the gcode dictionary docco (The docco site its fine, but there's a lot of info, so it's naturally just a hassle to navigate around it). So I wrote me a "greasemonkey" script for the browser, for the docco site.

      • open the gcode dictionary site: https://duet3d.dozuki.com/Wiki/Gcode
      • show the javascript console ("web developer tools" > "console")
        "ctrl + shift + J" in chrome, "ctrl + shift + i" in FireFox.
      • copy and paste in the following code and hit return
        (someone else can feel free to code review to ensure there's no evil)...
      // ==UserScript==
      // @name     GCode Wiki script
      // @version  1
      // @include     https://duet3d.dozuki.com/Wiki/Gcode*
      // ==/UserScript==
      
      /** for processing injected menu clicks, gets changed
       * to proper implementation when needed
       */
      let processMegaPage = () => {};
      let crossLinkDoc = () => {};
      let setupGcodeBrowser = () => {};
      
      let codeMap = {};
      let crossLinked = false;
      
      let runnerCount = 2;
      
      // for identifying gcode references
      let codeRx = /[GM]+[0-9]+(.[0-9])?/g;
      
      /** Helper function with finding things, so I don't need a framework
       */
      let docE = (f, inp) => {
          if (Array.isArray(inp)) {
              return (inp.map(i => docE(f, i))).flat();
          }
          let seek = document[f](inp);
          if (!seek || seek instanceof HTMLElement) return seek;
          let tmp = [];
          for (let t of seek) tmp.push(t);
          return tmp;
      };
      
      let content = document.getElementById('content');
      let isInContent = function (e) {
          if (!e) return false;
          if (!e.parentElement || e.parentElement === document.body) return false;
          if (e.parentElement === content) return true;
          return isInContent(e.parentElement);
      }
      
      /** Handler to take input and process links in gcode browser
       */
      let ref = null;
      let paster = () => {
          if (ref) clearTimeout(ref);
          ref = setTimeout(() => {
              let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
              let v = docE('getElementById', 'gcode-paste-input').value;
              v = v.replace(codeRx, (s) => {
                  if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '">' + s + '</a>';
                  return s;
              });
      
              docE('getElementById', 'gcode-paste').innerHTML = '<pre style="text-align:left">' + v + '</pre>';
          }, 500);
      };
      
      
      // check to see if this page is the one we want, otherwise do nothing
      let loc = null;
      let pos = null;
      let hash = null;
      let qstring = null;
      
      const evalLocation = () => {
          loc = document.location + '';
          pos = loc.indexOf('#');
          hash = null;
          if (pos > -1) {
              hash = loc.substring(pos);
              loc = loc.substring(0, pos);
          }
      
          qstring = '';
          pos = loc.indexOf('?');
          if (pos > -1) {
              qstring = loc.substring(pos + 1);
              loc = loc.substring(0, pos);
          }
      };
      
      const moveToHash = () => {
          if (hash) {
              document.location = hash;
          }
      };
      
      // current location details...
      evalLocation();
      
      if (loc.endsWith('/Wiki/Gcode')) {
          // current page is the gcode wiki...
      
          // find the menu
          let topTitle = docE('getElementsByClassName', 'toc-title')[0];
      
          // add the link to run the mega-page import...
          let squirt = document.createElement('div');
          squirt.innerHTML = '<a id="create-mega-page-link" href="javascript:;">Create Mega-Page</a>';
      
          topTitle.parentElement.insertBefore(squirt, topTitle.nextSibling);
      
          setTimeout(() => {
              // take a beat, wire the event clicker (greasemonkey complication)...
              let tmp = null;
              (tmp = document.getElementById('create-mega-page-link')) ? tmp.addEventListener('click', () => processMegaPage()) : null;
          }, 50);
      
          // function handler for the click...
          processMegaPage = () => {
              squirt.innerHTML = '';
      
              // find all the specific links
              let glinks = docE('getElementsByTagName', 'a').filter(a => {
                  let t = a.innerText;
                  let h = '' + a.href;
                  h = h.substring(h.indexOf('/', 10));
                  return (t.match(codeRx) && !h.startsWith('/Wiki/Gcode') && (h.startsWith('/Wiki/M') || h.startsWith('/Wiki/G')));
              });
      
              // a function that represents the work for a page download
              let processor = a => {
      
                  return new Promise((resolve, reject) => {
      
                      // download...
                      fetch(a.href).then(async response => {
      
                          // the document dext...
                          let t = await response.text();
      
                          // pull out the good stuff...
                          let from = t.indexOf('<div id="Wiki_Details">');
                          let to = t.lastIndexOf('<div class="clearer">', t.lastIndexOf('<div itemprop="author"'));
                          t = t.substring(from, to);
      
                          // dont need this link in there now...
                          t = t.replace('<p>Back to the <a href="/Wiki/Gcode">Gcode Dictionary</a></p>', '');
      
                          // make a div for it
                          const d = document.createElement('div');
                          d.innerHTML = t;
      
                          // replace the link tag's parent <p>
                          let p = a.parentElement;
                          p.parentNode.replaceChild(d, p);
      
                          // end of task
                          resolve();
      
                      }).catch(reject);
                  });
              };
      
              let count = 0;
      
              // a function that represents a processing thread so we can start N of them
              let runner = async () => {
                  while (glinks.length > 0) {
                      count++;
                      if (count % 25 === 0) console.log(count);
                      let x = glinks.shift();
                      try {
                          await processor(x);
                      } catch (e) {
                          console.log(e);
                      }
                  }
              };
      
              // start the runners...
              (async () => {
                  let time = new Date().getTime();
      
                  console.log('links to process: ' + glinks.length);
      
                  let runners = [];
                  for (let i = 0; i < runnerCount; i++) runners.push(runner());
      
                  await Promise.all(runners);
      
                  console.log('DONE!!! (' + ((new Date().getTime() - time) / 1000) + ' seconds)');
      
                  moveToHash();
      
                  squirt.innerHTML = '<a id="cross-linker-link" href="javascript:;">Cross-link gcodes</a><br>'
                      + '<a id="setup-gcode-browser-link" href="javascript:;">Setup GCode Browser</a><br><br>';
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('cross-linker-link')) ? tmp.addEventListener('click', () => crossLinkDoc()) : null;
                      (tmp = document.getElementById('setup-gcode-browser-link')) ? tmp.addEventListener('click', () => setupGcodeBrowser()) : null;
                  }, 50);
              })();
      
              crossLinkDoc = () => {
                  if (crossLinked) return;
                  crossLinked = true;
      
                  let link = document.getElementById('cross-linker-link');
                  link.parentElement.removeChild(link);
      
                  /** Parse the references out of the document
                   */
                  docE('getElementsByClassName', 'header').forEach(sect => {
                      let s = sect.id + '';
                      s.replace(codeRx, sx => {
                          sx = sx.replace('_', '.');
                          if (!codeMap[sx]) {
                              codeMap[sx] = s;
                          }
                      });
                  });
      
                  /** Cross-link gcode references through the document
                   */
                  docE('getElementsByTagName', ['p', 'li', 'pre', 'strong']).forEach(tag => {
                      if (!tag) return;
                      if (!isInContent(tag)) return;
                      let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
                      let h = ('' + tag.innerHTML).trim();
                    	if (h.startsWith('<div/')) return;
                      tag.innerHTML = h.replace(/(?<!_)([GM]+[0-9]+(.[0-9])?)/g, (s) => {
                          if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '" class="crosslink">' + s + '</a>';
                          return s;
                      });
                  });
      
                  evalLocation();
                  moveToHash();
              };
      
              setupGcodeBrowser = () => {
                  crossLinkDoc();
      
                  if (squirt) {
                      squirt.parentElement.removeChild(squirt);
                      squirt = null;
                  }
      
                  /** Strip the main wrapper element of styling
                   */
                  let wrap = docE('getElementById', 'page');
                  wrap.className = '';
                  wrap.id = 'gnavPage';
      
                  /** Remove styling of sidebar nested element
                   */
                  docE('getElementById', 'sidebar-wiki-toc').className = '';
                  docE('getElementById', 'sidebar-wiki-toc').id = 'gnavSidebar';
      
                  /** Strip the sidebar
                   */
                  let sb = null;
                  sb = docE('getElementById', 'page-sidebar');
                  sb.innerHTML = '';
                  sb.id = 'gnavSidebar';
      
                  /** Strip the main area
                   */
                  let main = docE('getElementById', 'main');
                  main.id = 'gnavMain';
                  docE('getElementsByClassName', 'articleContainer')[0].className = '';
      
                  /** Clear our other elements not needed
                   */
                  let mb = docE('getElementById', 'mainBody');
                  for (let kid of mb.children) {
                      if (kid.id !== 'contentFloat') mb.removeChild(kid);
                  }
      
                  let bg = docE('getElementById', 'background');
                  for (let kid of bg.children) {
                      if (kid.id === 'gnavPage') break;
                      else bg.removeChild(kid);
                  }
      
                  for (let kid of document.body.children) {
                      if (kid.id !== 'background') document.body.removeChild(kid);
                  }
      
                  docE('getElementById', 'content').id = 'offContent';
      
                  /** Apply new styles to the wrapper, sidebar and main areas
                   */
                  wrap.setAttribute('style', 'display:flex;gap:1em;');
                  sb.setAttribute('style', 'flex: 1 1 45%; height: 100vh;');
                  main.setAttribute('style', 'flex: 1 1 50%; height: 100vh');
      
                  /** Input field to paste the gcode
                   */
                  sb.innerHTML = `
      <div style="width: 45vw; height: 100vh; position: fixed;overflow: scroll;">
          <textarea id="gcode-paste-input"></textArea>
        <hr />
        <pre id="gcode-paste" style="text-align: left"><center>( paste gcode above )</center></pre>
      </div>`;
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('gcode-paste-input')) ? tmp.addEventListener('keyup', () => paster()) : null;
                  }, 50);
              };
          };
      }
      
      // auto-trigger...
      //if (qstring === 'inflateContent') {
          processMegaPage();
      //}
      

      ...the script will hide the current menu, strip the page down so it's two side-by-side panels, and provide a place to paste gcode. Paste in any chunck of Gcode (like entire contents of 'config.g' ) and click "Run". It will then put the Gcode in place of the menu with all the codes changed to links in the document.

      Much easier to browse what's happening without losing concentration with bouncing around.

      I would love for the docco site to pilfer this idea.
      (I haven't put that much effort into styling or whatever, I just wanted it to work, so the styling is "rough").

      _
      CROSS LINKING: The docco frequently references other codes in the document, but it's a hassle to browse to them and back to where you were. This cross linker runs through the doc to find the gcode references, adds the links... so now you can click on the link, see what it was referencing and click back in the browser as the browser remembers scroll positions when clicking.

      I can understand why maintainers haven't put the links in, as it would be a hassle to look after the links with all the edits, if any link changes, blah blah. This script just looks after that as a post-processor style :)!

      Screenshot for what it's worth...

      67183642-03cd-47de-b49a-8dbc03f3a64a-image.png

      posted in General Discussion
      theKM
      theKM
    • Movement buttons/features managed by configuration (UI mod)

      I'm doing some hacking around in the UI, curious if others would be interested...

      Being able to change the button values in the movement panel was always an "almost" handy feature to me, particularly on larger CNC machine. Setting up a job uses coarser movements, so I set the buttons to larger numbers, but then getting in close up on something fiddly, I end up setting them to smaller measurements... which means the buttons are no longer handy for initial setting up of the job. a cycle that repeats. Not to mention that often used macros would be nice to have closer and super convenient (like a button to navigate to a fixture exactly, or over a fixture, etc).

      Synopsis: multiple movement panels set to different scenarios in easy reach, totally configurable by json file.

      At work, one of my favorite things to build are user interfaces that are assembled by configuration, so I wanted to get me some of that in the DWC!

      Screenshot walkthrough...

      Removed the title (it's pretty obvious that it's the movement panel 😉 ), and added buttons for N amount of alternate panels (it's just whatever you have configured; default, one extra, two, whatever) along with a reload button (the config is something I'm likely to tweak regularly as I please and don't want to restart the machine. If I have an idea, I can jump into the config file, add it, reload it, voila).

      Here I've configured two extra panels, one for finer moves and a dedicated macro pad, as well as adding an extra row of macro buttons to the default panel (a re-configured default panel shown)...

      84f691ab-dedb-4523-b201-1cfaff035cc7-image.png

      Here is a new panel that's fixed to fine movements (button color also configurable), macro row above (macros can go anywhere, any amount of rows)...

      83f9c024-491d-424c-8340-fe552adb3d53-image.png

      Even a full panel of macros...

      286e14a1-d66b-462a-b0c2-a35864bced84-image.png

      Without any config file it just works like normal...

      9c094630-e9d5-4fa8-83cb-34615efe4e7d-image.png

      ...I originally wrote it just to make more handy buttons to get to fixtures and parking the machine before turning it off... this satisfies that and a bunch of other nice-to-haves that I was after.

      Code isn't crazy sexy at the moment, but I'll tidy it up and push to a repo somewhere if people want to play with it.

      _
      "I want a horn here, here and here. You can never find a horn when you're mad" ~ Homer Simpson

      posted in Duet Web Control
      theKM
      theKM
    • RE: Repository for CNC Config's and Macro's

      Edit (2021-10-08) : I re-wrote the script because the structure of the GCode wiki pages changed, and now includes downloading the content to re-create the mega-page.

      _
      Browsing other people's gcode is super handy, particularly for custom setups and going your own way. However, it's a genuine hassle to both read someone else's GCode while trying to bounce around the gcode dictionary docco... and the new wiki structure is great, and necessary for the maintainers, but a challenge to bounce around efficiently.

      So here is a script that re-creates the mega-gcode docco page, and sets up a gcode browser in place of the menu.

      • open the gcode dictionary site: https://duet3d.dozuki.com/Wiki/Gcode
      • show the javascript console ("web developer tools" > "console")
        ctrl + shift + J in chrome, ctrl + shift + i in FireFox.
      • copy and paste in the following code and hit return
        (someone else can feel free to code review to ensure there's no evil)...
      // ==UserScript==
      // @name     GCode Wiki script
      // @version  1
      // @include     https://duet3d.dozuki.com/Wiki/Gcode*
      // ==/UserScript==
      
      /** for processing injected menu clicks, gets changed
       * to proper implementation when needed
       */
      let processMegaPage = () => {};
      let crossLinkDoc = () => {};
      let setupGcodeBrowser = () => {};
      
      let codeMap = {};
      let crossLinked = false;
      
      // for identifying gcode references
      let codeRx = /[GM]+[0-9]+(.[0-9])?/g;
      
      /** Helper function with finding things, so I don't need a framework
       */
      let docE = (f, inp) => {
          if (Array.isArray(inp)) {
              return (inp.map(i => docE(f, i))).flat();
          }
          let seek = document[f](inp);
          if (!seek || seek instanceof HTMLElement) return seek;
          let tmp = [];
          for (let t of seek) tmp.push(t);
          return tmp;
      };
      
      let content = document.getElementById('content');
      let isInContent = function (e) {
          if (!e) return false;
          if (!e.parentElement || e.parentElement === document.body) return false;
          if (e.parentElement === content) return true;
          return isInContent(e.parentElement);
      }
      
      /** Handler to take input and process links in gcode browser
       */
      let ref = null;
      let paster = () => {
          if (ref) clearTimeout(ref);
          ref = setTimeout(() => {
              let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
              let v = docE('getElementById', 'gcode-paste-input').value;
              v = v.replace(codeRx, (s) => {
                  if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '">' + s + '</a>';
                  return s;
              });
      
              docE('getElementById', 'gcode-paste').innerHTML = '<pre style="text-align:left">' + v + '</pre>';
          }, 500);
      };
      
      
      // check to see if this page is the one we want, otherwise do nothing
      let loc = null;
      let pos = null;
      let hash = null;
      let qstring = null;
      
      const evalLocation = () => {
          loc = document.location + '';
          pos = loc.indexOf('#');
          hash = null;
          if (pos > -1) {
              hash = loc.substring(pos);
              loc = loc.substring(0, pos);
          }
      
          qstring = '';
          pos = loc.indexOf('?');
          if (pos > -1) {
              qstring = loc.substring(pos + 1);
              loc = loc.substring(0, pos);
          }
      };
      
      const moveToHash = () => {
          if (hash) {
              document.location = hash;
          }
      };
      
      // current location details...
      evalLocation();
      
      if (loc.endsWith('/Wiki/Gcode')) {
          // current page is the gcode wiki...
      
          // find the menu
          let topTitle = docE('getElementsByClassName', 'toc-title')[0];
      
          // add the link to run the mega-page import...
          let squirt = document.createElement('div');
          squirt.innerHTML = '<a id="create-mega-page-link" href="javascript:;">Create Mega-Page</a>';
      
          topTitle.parentElement.insertBefore(squirt, topTitle.nextSibling);
      
          setTimeout(() => {
              // take a beat, wire the event clicker (greasemonkey complication)...
              let tmp = null;
              (tmp = document.getElementById('create-mega-page-link')) ? tmp.addEventListener('click', () => processMegaPage()) : null;
          }, 50);
      
          // function handler for the click...
          processMegaPage = () => {
              squirt.innerHTML = '';
      
              // find all the specific links
              let glinks = docE('getElementsByTagName', 'a').filter(a => {
                  let t = a.innerText;
                  let h = '' + a.href;
                  h = h.substring(h.indexOf('/', 10));
                  return (t.match(codeRx) && !h.startsWith('/Wiki/Gcode') && (h.startsWith('/Wiki/M') || h.startsWith('/Wiki/G')));
              });
      
              // a function that represents the work for a page download
              let processor = a => {
      
                  return new Promise((resolve, reject) => {
      
                      // download...
                      fetch(a.href).then(async response => {
      
                          // the document dext...
                          let t = await response.text();
      
                          // pull out the good stuff...
                          let from = t.indexOf('<div id="Wiki_Details">');
                          let to = t.lastIndexOf('<div class="clearer">', t.lastIndexOf('<div itemprop="author"'));
                          t = t.substring(from, to);
      
                          // dont need this link in there now...
                          t = t.replace('<p>Back to the <a href="/Wiki/Gcode">Gcode Dictionary</a></p>', '');
      
                          // make a div for it
                          const d = document.createElement('div');
                          d.innerHTML = t;
      
                          // replace the link tag's parent <p>
                          let p = a.parentElement;
                          p.parentNode.replaceChild(d, p);
      
                          // end of task
                          resolve();
      
                      }).catch(reject);
                  });
              };
      
              let count = 0;
      
              // a function that represents a processing thread so we can start N of them
              let runner = async () => {
                  while (glinks.length > 0) {
                      count++;
                      if (count % 25 === 0) console.log(count);
                      let x = glinks.shift();
                      try {
                          await processor(x);
                      } catch (e) {
                          console.log(e);
                      }
                  }
              };
      
              // start the runners...
              let runnerCount = 1;
              (async () => {
                  let time = new Date().getTime();
      
                  console.log('links to process: ' + glinks.length);
      
                  let runners = [];
                  for (let i = 0; i < runnerCount; i++) runners.push(runner());
      
                  await Promise.all(runners);
      
                  console.log('DONE!!! (' + ((new Date().getTime() - time) / 1000) + ' seconds)');
      
                  moveToHash();
      
                  squirt.innerHTML = '<a id="cross-linker-link" href="javascript:;">Cross-link gcodes</a><br>'
                      + '<a id="setup-gcode-browser-link" href="javascript:;">Setup GCode Browser</a><br><br>';
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('cross-linker-link')) ? tmp.addEventListener('click', () => crossLinkDoc()) : null;
                      (tmp = document.getElementById('setup-gcode-browser-link')) ? tmp.addEventListener('click', () => setupGcodeBrowser()) : null;
                  }, 50);
              })();
      
              crossLinkDoc = () => {
                  if (crossLinked) return;
                  crossLinked = true;
      
                  let link = document.getElementById('cross-linker-link');
                  link.parentElement.removeChild(link);
      
                  /** Parse the references out of the document
                   */
                  docE('getElementsByClassName', 'header').forEach(sect => {
                      let s = sect.id + '';
                      s.replace(codeRx, sx => {
                          sx = sx.replace('_', '.');
                          if (!codeMap[sx]) {
                              codeMap[sx] = s;
                          }
                      });
                  });
      
                  /** Cross-link gcode references through the document
                   */
                  docE('getElementsByTagName', ['p', 'li', 'pre', 'strong']).forEach(tag => {
                      if (!tag) return;
                      if (!isInContent(tag)) return;
                      let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
                      let h = ('' + tag.innerHTML).trim();
                    	if (h.startsWith('<div/')) return;
                      tag.innerHTML = h.replace(/(?<!_)([GM]+[0-9]+(.[0-9])?)/g, (s) => {
                          if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '" class="crosslink">' + s + '</a>';
                          return s;
                      });
                  });
      
                  evalLocation();
                  moveToHash();
              };
      
              setupGcodeBrowser = () => {
                  crossLinkDoc();
      
                  if (squirt) {
                      squirt.parentElement.removeChild(squirt);
                      squirt = null;
                  }
      
                  /** Strip the main wrapper element of styling
                   */
                  let wrap = docE('getElementById', 'page');
                  wrap.className = '';
                  wrap.id = 'gnavPage';
      
                  /** Remove styling of sidebar nested element
                   */
                  docE('getElementById', 'sidebar-wiki-toc').className = '';
                  docE('getElementById', 'sidebar-wiki-toc').id = 'gnavSidebar';
      
                  /** Strip the sidebar
                   */
                  let sb = null;
                  sb = docE('getElementById', 'page-sidebar');
                  sb.innerHTML = '';
                  sb.id = 'gnavSidebar';
      
                  /** Strip the main area
                   */
                  let main = docE('getElementById', 'main');
                  main.id = 'gnavMain';
                  docE('getElementsByClassName', 'articleContainer')[0].className = '';
      
                  /** Clear our other elements not needed
                   */
                  let mb = docE('getElementById', 'mainBody');
                  for (let kid of mb.children) {
                      if (kid.id !== 'contentFloat') mb.removeChild(kid);
                  }
      
                  let bg = docE('getElementById', 'background');
                  for (let kid of bg.children) {
                      if (kid.id === 'gnavPage') break;
                      else bg.removeChild(kid);
                  }
      
                  for (let kid of document.body.children) {
                      if (kid.id !== 'background') document.body.removeChild(kid);
                  }
      
                  docE('getElementById', 'content').id = 'offContent';
      
                  /** Apply new styles to the wrapper, sidebar and main areas
                   */
                  wrap.setAttribute('style', 'display:flex;gap:1em;');
                  sb.setAttribute('style', 'flex: 1 1 45%; height: 100vh;');
                  main.setAttribute('style', 'flex: 1 1 50%; height: 100vh');
      
                  /** Input field to paste the gcode
                   */
                  sb.innerHTML = `
      <div style="width: 45vw; height: 100vh; position: fixed;overflow: scroll;">
          <textarea id="gcode-paste-input"></textArea>
        <hr />
        <pre id="gcode-paste" style="text-align: left"><center>( paste gcode above )</center></pre>
      </div>`;
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('gcode-paste-input')) ? tmp.addEventListener('keyup', () => paster()) : null;
                  }, 50);
              };
          };
      }
      
      // auto-trigger...
      if (qstring === 'inflateContent') {
          processMegaPage();
      }
      

      ...with the script run, you will get a link on the top of the left hand menu to create the mega-page...

      • click it

      ...this will process all the links and download all the content and inject it into the page. When completed the download and setting up the content, this link will change to "Setup GCode Browser"...

      • click it

      ...the script will hide the current menu, strip the page down so it's two side-by-side panels, and provide a place to paste gcode. Paste in any chunck of Gcode (like entire contents of 'config.g' ) and click "Run". It will then put the Gcode in place of the menu with all the codes changed to links in the document.

      Much easier to browse what's happening without losing concentration with bouncing around.

      _
      CROSS LINKING: The docco frequently references other codes in the document, but it's a hassle to browse to them and back to where you were. This cross linker runs through the doc to find the gcode references, adds the links... so now you can click on the link, see what it was referencing and click back in the browser as the browser remembers scroll positions when clicking, just like it used to.

      _
      Greasemonkey/Tampermonkey
      If you want it to simply be around without having to paste in, blah blah... install Greasemonkey browser plugin (Chrome's is called Tampermonkey), and install the script above. The browser will execute it when you visit the gcode page automagically. And if you hit it with "?inflateContent" on the end of the URL ( https://duet3d.dozuki.com/Wiki/Gcode?inflateContent ), it will automatically create the mega-page without needing to click anything.

      posted in CNC
      theKM
      theKM
    • Mega GCode docco page + gcode file navigator

      For fans of the old mega-gcode-page...

      I was starting to miss the mega page, but understand why they had to do it.

      So I messed with my greaseMonkey script. Below is a script that re-creates the original mega-page by plucking content from the little gcode pages and squirting them into the main page. With three processing "threads" it rips the individual pages down and assembles the mega page in around 10-12 seconds on my connection (six threads is amusing, but it trips the server's rate limit, because it starts to look like a DDOS attack, so, 3 is ideal, IMO 🙂 ).

      To run it, browse to the gcode page ( https://duet3d.dozuki.com/Wiki/Gcode ) open JS console (ctrl + shift + J in chrome, ctrl + shift + i in firefox), copy and paste this in... hit enter to run it.

      When it runs, it should put a "Create Mega-Page" link at the top of the left hand menu... click that...

      You should see the scroll bar on the right go tiny as it downloads and injects the content from all the separate gcode pages into this page. What results is a page exactly like what used to be there, menu on the left still works, you can `ctrl+F' to find content, etc.

      _
      Script to copy and paste:

      // ==UserScript==
      // @name     GCode Wiki script
      // @version  1
      // @include     https://duet3d.dozuki.com/Wiki/Gcode*
      // ==/UserScript==
      
      /** for processing injected menu clicks, gets changed
       * to proper implementation when needed
       */
      let processMegaPage = () => {};
      let crossLinkDoc = () => {};
      let setupGcodeBrowser = () => {};
      
      let codeMap = {};
      let crossLinked = false;
      
      // for identifying gcode references
      let codeRx = /[GM]+[0-9]+(.[0-9])?/g;
      
      /** Helper function with finding things, so I don't need a framework
       */
      let docE = (f, inp) => {
          if (Array.isArray(inp)) {
              return (inp.map(i => docE(f, i))).flat();
          }
          let seek = document[f](inp);
          if (!seek || seek instanceof HTMLElement) return seek;
          let tmp = [];
          for (let t of seek) tmp.push(t);
          return tmp;
      };
      
      let content = document.getElementById('content');
      let isInContent = function (e) {
          if (!e) return false;
          if (!e.parentElement || e.parentElement === document.body) return false;
          if (e.parentElement === content) return true;
          return isInContent(e.parentElement);
      }
      
      /** Handler to take input and process links in gcode browser
       */
      let ref = null;
      let paster = () => {
          if (ref) clearTimeout(ref);
          ref = setTimeout(() => {
              let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
              let v = docE('getElementById', 'gcode-paste-input').value;
              v = v.replace(codeRx, (s) => {
                  if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '">' + s + '</a>';
                  return s;
              });
      
              docE('getElementById', 'gcode-paste').innerHTML = '<pre style="text-align:left">' + v + '</pre>';
          }, 500);
      };
      
      
      // check to see if this page is the one we want, otherwise do nothing
      let loc = null;
      let pos = null;
      let hash = null;
      let qstring = null;
      
      const evalLocation = () => {
          loc = document.location + '';
          pos = loc.indexOf('#');
          hash = null;
          if (pos > -1) {
              hash = loc.substring(pos);
              loc = loc.substring(0, pos);
          }
      
          qstring = '';
          pos = loc.indexOf('?');
          if (pos > -1) {
              qstring = loc.substring(pos + 1);
              loc = loc.substring(0, pos);
          }
      };
      
      const moveToHash = () => {
          if (hash) {
              document.location = hash;
          }
      };
      
      // current location details...
      evalLocation();
      
      if (loc.endsWith('/Wiki/Gcode')) {
          // current page is the gcode wiki...
      
          // find the menu
          let topTitle = docE('getElementsByClassName', 'toc-title')[0];
      
          // add the link to run the mega-page import...
          let squirt = document.createElement('div');
          squirt.innerHTML = '<a id="create-mega-page-link" href="javascript:;">Create Mega-Page</a>';
      
          topTitle.parentElement.insertBefore(squirt, topTitle.nextSibling);
      
          setTimeout(() => {
              // take a beat, wire the event clicker (greasemonkey complication)...
              let tmp = null;
              (tmp = document.getElementById('create-mega-page-link')) ? tmp.addEventListener('click', () => processMegaPage()) : null;
          }, 50);
      
          // function handler for the click...
          processMegaPage = () => {
              squirt.innerHTML = '';
      
              // find all the specific links
              let glinks = docE('getElementsByTagName', 'a').filter(a => {
                  let t = a.innerText;
                  let h = '' + a.href;
                  h = h.substring(h.indexOf('/', 10));
                  return (t.match(codeRx) && !h.startsWith('/Wiki/Gcode') && (h.startsWith('/Wiki/M') || h.startsWith('/Wiki/G')));
              });
      
              // a function that represents the work for a page download
              let processor = a => {
      
                  return new Promise((resolve, reject) => {
      
                      // download...
                      fetch(a.href).then(async response => {
      
                          // the document dext...
                          let t = await response.text();
      
                          // pull out the good stuff...
                          let from = t.indexOf('<div id="Wiki_Details">');
                          let to = t.lastIndexOf('<div class="clearer">', t.lastIndexOf('<div itemprop="author"'));
                          t = t.substring(from, to);
      
                          // dont need this link in there now...
                          t = t.replace('<p>Back to the <a href="/Wiki/Gcode">Gcode Dictionary</a></p>', '');
      
                          // make a div for it
                          const d = document.createElement('div');
                          d.innerHTML = t;
      
                          // replace the link tag's parent <p>
                          let p = a.parentElement;
                          p.parentNode.replaceChild(d, p);
      
                          // end of task
                          resolve();
      
                      }).catch(reject);
                  });
              };
      
              let count = 0;
      
              // a function that represents a processing thread so we can start N of them
              let runner = async () => {
                  while (glinks.length > 0) {
                      count++;
                      if (count % 25 === 0) console.log(count);
                      let x = glinks.shift();
                      try {
                          await processor(x);
                      } catch (e) {
                          console.log(e);
                      }
                  }
              };
      
              // start the runners...
              let runnerCount = 1;
              (async () => {
                  let time = new Date().getTime();
      
                  console.log('links to process: ' + glinks.length);
      
                  let runners = [];
                  for (let i = 0; i < runnerCount; i++) runners.push(runner());
      
                  await Promise.all(runners);
      
                  console.log('DONE!!! (' + ((new Date().getTime() - time) / 1000) + ' seconds)');
      
                  moveToHash();
      
                  squirt.innerHTML = '<a id="cross-linker-link" href="javascript:;">Cross-link gcodes</a><br>'
                      + '<a id="setup-gcode-browser-link" href="javascript:;">Setup GCode Browser</a><br><br>';
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('cross-linker-link')) ? tmp.addEventListener('click', () => crossLinkDoc()) : null;
                      (tmp = document.getElementById('setup-gcode-browser-link')) ? tmp.addEventListener('click', () => setupGcodeBrowser()) : null;
                  }, 50);
              })();
      
              crossLinkDoc = () => {
                  if (crossLinked) return;
                  crossLinked = true;
      
                  let link = document.getElementById('cross-linker-link');
                  link.parentElement.removeChild(link);
      
                  /** Parse the references out of the document
                   */
                  docE('getElementsByClassName', 'header').forEach(sect => {
                      let s = sect.id + '';
                      s.replace(codeRx, sx => {
                          sx = sx.replace('_', '.');
                          if (!codeMap[sx]) {
                              codeMap[sx] = s;
                          }
                      });
                  });
      
                  /** Cross-link gcode references through the document
                   */
                  docE('getElementsByTagName', ['p', 'li', 'pre', 'strong']).forEach(tag => {
                      if (!tag) return;
                      if (!isInContent(tag)) return;
                      let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
                      let h = ('' + tag.innerHTML).trim();
                    	if (h.startsWith('<div/')) return;
                      tag.innerHTML = h.replace(/(?<!_)([GM]+[0-9]+(.[0-9])?)/g, (s) => {
                          if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '" class="crosslink">' + s + '</a>';
                          return s;
                      });
                  });
      
                  evalLocation();
                  moveToHash();
              };
      
              setupGcodeBrowser = () => {
                  crossLinkDoc();
      
                  if (squirt) {
                      squirt.parentElement.removeChild(squirt);
                      squirt = null;
                  }
      
                  /** Strip the main wrapper element of styling
                   */
                  let wrap = docE('getElementById', 'page');
                  wrap.className = '';
                  wrap.id = 'gnavPage';
      
                  /** Remove styling of sidebar nested element
                   */
                  docE('getElementById', 'sidebar-wiki-toc').className = '';
                  docE('getElementById', 'sidebar-wiki-toc').id = 'gnavSidebar';
      
                  /** Strip the sidebar
                   */
                  let sb = null;
                  sb = docE('getElementById', 'page-sidebar');
                  sb.innerHTML = '';
                  sb.id = 'gnavSidebar';
      
                  /** Strip the main area
                   */
                  let main = docE('getElementById', 'main');
                  main.id = 'gnavMain';
                  docE('getElementsByClassName', 'articleContainer')[0].className = '';
      
                  /** Clear our other elements not needed
                   */
                  let mb = docE('getElementById', 'mainBody');
                  for (let kid of mb.children) {
                      if (kid.id !== 'contentFloat') mb.removeChild(kid);
                  }
      
                  let bg = docE('getElementById', 'background');
                  for (let kid of bg.children) {
                      if (kid.id === 'gnavPage') break;
                      else bg.removeChild(kid);
                  }
      
                  for (let kid of document.body.children) {
                      if (kid.id !== 'background') document.body.removeChild(kid);
                  }
      
                  docE('getElementById', 'content').id = 'offContent';
      
                  /** Apply new styles to the wrapper, sidebar and main areas
                   */
                  wrap.setAttribute('style', 'display:flex;gap:1em;');
                  sb.setAttribute('style', 'flex: 1 1 45%; height: 100vh;');
                  main.setAttribute('style', 'flex: 1 1 50%; height: 100vh');
      
                  /** Input field to paste the gcode
                   */
                  sb.innerHTML = `
      <div style="width: 45vw; height: 100vh; position: fixed;overflow: scroll;">
          <textarea id="gcode-paste-input"></textArea>
        <hr />
        <pre id="gcode-paste" style="text-align: left"><center>( paste gcode above )</center></pre>
      </div>`;
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('gcode-paste-input')) ? tmp.addEventListener('keyup', () => paster()) : null;
                  }, 50);
              };
          };
      }
      
      // auto-trigger...
      if (qstring === 'inflateContent') {
          processMegaPage();
      }
      

      _
      CROSS LINKING
      After all the gcode content is downloaded and set up on the page, there will be two links at the top of the menu... click on "Cross-link gcodes", and the script will then link all the GCode mentions on the page with other parts of the document, so you can bounce around from wherever you see a gcode mentioned.

      _
      GCODE BROWSER

      Click on "Setup Gcode Browser"... it will cross-link the gcodes as mentioned, and the page gets wrangled to set up the left hand size as a gcode panel with the documentation on the right.

      Paste the contents of a gcode file (like config.g ) into the text area, and it will update to show the gcode, with all the codes linked to the documentation on the right.

      This makes it handy to browse what's happening in a gcode file, the browser know where you're at (for example, rather than assume you know what a code is doing, click on it, confirm in the docco that it does what you assume, then just click back in the browser to be right back where you're at).

      This has greatly helped my understanding of what's going on in these setup files so I can set up my CNC machine.

      _
      Greasemonkey

      If you like this script, and wish it was just part of the site, then you can use a plugin called GreaseMonkey in FireFox (chrome has one called TamperMonkey that runs the same thing)... which just tells the browser to run scripts against sites you configure. The above script takes greasemonkey into account and should work as is.

      The script is also looking for "inflateContent" in the URL, so if you install it in greaseMoneky, and then just browse to https://duet3d.dozuki.com/Wiki/Gcode?inflateContent , the page will automatically inflate to the mega-page without needing to click anything.
      _

      ...if the wiki maintainers wish, this script could be included in the top html document. It only works when it sees that it's on the correct page, and then injects the link. So the site stays as-is, until the user clicks the link to create the voltron mega-page.

      I'm still fiddling with it to do all the cross-linking that my other script did previously, but thought people might like this to recreate what was, while still allowing wiki maintainers to do the separate page thing.

      posted in General Discussion
      theKM
      theKM
    • RE: Configurable Main Menu ( /w source code )

      If anyone wants to try it out, I put the DWC zip files build here...

      https://github.com/abates-dentsu/DuetWebControl/tree/az_generatedUI_build/dist-packaged

      ...note that this is based on the latest beta version. I'd only recommend people try it if they've already successfully moved up to the latest core beta of DWC.

      posted in Duet Web Control
      theKM
      theKM
    • G1 option to apply max deceleration to stop

      New option on G1 gcode ("H5"?) that applies the current max deceleration value from M201 to bring the axis to a stop rather than the hard termination.

      This would allow bigger/heavier machines to jog at speed toward the endstop, pass an extra switch/sensor that will indicate slowing down for the approaching endstop.

      If the move is not terminated by the switch, it currently decelerates as usual... so the info for the deceleration is already somewhere in the move's calculation, just need to truncate/skip between the switch trigger and the part where it would otherwise be slowing down and just let that play out.

      posted in Firmware wishlist
      theKM
      theKM
    • Slow down before endstop?

      Is there any way to connect a sensor that's near the endstops so that it can tell the machine to slow way down before touching the endstop?...

      Issue is, machine is large and heavy. Telling it to stop on a dime when hitting a switch 3d-printer-style makes a god-awful crunch. And it's of a size, that telling it to go super slow would take forever to traverse the machine.

      So electronically it would be super nice to have a "you're close, slow down" switch/sensor, but I understand that that would require a different kind of interrupt in the controller that would allow it to continue doing what it's doing but just decelerate for the impending endstop switch.

      I've done a bunch of searching, but only end up with usual endstop switch setup.

      It's not super critical (I can keep using it with the endstops just as emergency measures), but it would make the machine more productive if it can sort out its total workspace when turning it on, regardless of where it was stopped last.

      posted in CNC
      theKM
      theKM
    • RE: DWC Custom Configuration

      @jens55 ...absolutely. Anyone who's not a dev, who has a feature suggestion, has to try and convince the maintainers to care enough to work on it. And that's fine. If a maintainer doesn't care about it, then it doesn't get done, and I support that choice... but when others put up road blocks, insisting it's not needed, that's what weirds me out.

      I think literally everyone who's a fan of the Duet is crazy thankful for the controller and the web interface. There's no other controller this configurable in general.

      And as crazy good as the web interface is, the fact it can be changed with a web page tech means it's actually super adaptable. The Ooznest CNC interface being the perfect example, but now those features are rolled into the main branch, which is epic sauce and congrats to everyone!

      I'd like to see the UI be more configurable... my day job is VueJS development, so I plan to look into what would be pragmatic. If I ever come up with something, I'll push it somewhere, make a PR, and then the maintainer(s) can decide to merge it or not. And I would be fine with it not being merged... but, it would also mean that it's in a public repo, and people could download my fork if they wanted.

      ...this is how open source goes 🙂

      posted in Duet Web Control
      theKM
      theKM
    • RE: Slow down before endstop?

      @fcwilt ... created a thing in the wishlist

      https://forum.duet3d.com/topic/24900/g1-option-to-apply-max-deceleration-to-stop

      posted in CNC
      theKM
      theKM

    Latest posts made by theKM

    • RE: Manually provide the number of steps to be executed by a drive?

      @lee7670 ...or just do the math of steps per unit measure to how many units it needs to move?... if you're at 100 steps per centimeter, the move there is obviously four centimeters?

      posted in Firmware developers
      theKM
      theKM
    • RE: Handy way to browse GCode docco directly from the gcode

      update: I've since updated the script to have it wrangle all the newly managed pages into the single gcode page that my original script relied on. The script above was updated just so people don't try something that wont work, but I did start another thread on it (mods can maybe close this thread? not sure how things are done around here for old content 🙂 ).

      https://forum.duet3d.com/topic/25459/mega-gcode-docco-page-gcode-file-navigator

      posted in General Discussion
      theKM
      theKM
    • RE: Mega GCode docco page + gcode file navigator

      updated the script for greasemonkey/tampermonkey... apparently they sandbox a few things so link handling attachments had to be more complicated than a usual first-class-citizen script. The *monkey plugins seem to work pretty nice now, certainly a more handy way to run it than to paste it in all the time 🙂

      posted in General Discussion
      theKM
      theKM
    • RE: Gcode documentation change

      @zapta said in Gcode documentation change:

      Can we have the official documentation page running this script automatically when opened? Loading time was very fast.

      I updated and tested the script in greasemonkey (firefox) / tampermonkey (chrome) plugins... so if you want it to work automagically, that's one way to do it without the wiki admins applying the script.

      With the plugin, this link will automatically make the large page without needing to click:
      https://duet3d.dozuki.com/Wiki/Gcode?inflateContent

      And if the 'inflateContent' link is too much of a hassle, you can always have it build the page by changing the last part in the script from ...

      // auto-trigger...
      
      if (qstring === 'inflateContent') {
          processMegaPage();
      }
      

      ...to...

      processMegaPage();
      
      posted in General Discussion
      theKM
      theKM
    • RE: Gcode documentation change

      @zapta said in Gcode documentation change:

      If the target section link is represented in the URL, the loader could potentially parse it after loading the parts and jump there, or something like that, but it's also above my pay grade. 😉

      See, then you went and made me feel bad 🙂

      ...script above is updated to reevaluate the hash link to re-bump into the correct location after the content injection.

      It is also looking for "inflate content" in the URL query string, if it's there it will automatically inflate the content without needing to click.

      posted in General Discussion
      theKM
      theKM
    • RE: Gcode documentation change

      @zapta said in Gcode documentation change:

      Works for me! (Using chrome/mac so had to open the console in a different way).

      woot

      Can we have the official documentation page running this script automatically when opened? Loading time was very fast.

      way above my pay grade to include it, but it is written in a way that has it do nothing if the user's not on the right page.

      also, is it possible to have links to this overall page? E.g. a link for G1 that will take you to the G1 in that page?

      The page is inflated after loading, the browser doesn't know how to cope with sub-linking into a page that's altered after the main document as loaded, so some things are simply off the table.

      That said, try the script I put here... https://forum.duet3d.com/topic/25459/mega-gcode-docco-page-gcode-file-navigator

      ...specifically the "cross linking" mentioned, and see if that's what you're after.

      posted in General Discussion
      theKM
      theKM
    • RE: Gcode documentation change

      For what it's worth...

      A script that puts a link at the top of the left hand menu, that if clicked, will re-assemble the single mega-page from all the separate pages.

      It is written in a way that if included as a script tag in the wiki's root html template, it will only wake up on the correct page, and simply add a link to the menu. User can click it to assemble the mega page, or ignore it all together to leave it as-is. I'd recommend putting it in a file somewhere and referring to it so the browser can cache it so there's no download impact each page load.

      For anyone that wants to run it themselves, just go to the gcode page, open JS console ( ctrl + shift + J for chrome, ctrl + shift + i for firefox ) , paste it in, press enter...

      // ==UserScript==
      // @name     GCode Wiki script
      // @version  1
      // @include     https://duet3d.dozuki.com/Wiki/Gcode*
      // ==/UserScript==
      
      /** for processing injected menu clicks, gets changed
       * to proper implementation when needed
       */
      let processMegaPage = () => {};
      let crossLinkDoc = () => {};
      let setupGcodeBrowser = () => {};
      
      let codeMap = {};
      let crossLinked = false;
      
      // for identifying gcode references
      let codeRx = /[GM]+[0-9]+(.[0-9])?/g;
      
      /** Helper function with finding things, so I don't need a framework
       */
      let docE = (f, inp) => {
          if (Array.isArray(inp)) {
              return (inp.map(i => docE(f, i))).flat();
          }
          let seek = document[f](inp);
          if (!seek || seek instanceof HTMLElement) return seek;
          let tmp = [];
          for (let t of seek) tmp.push(t);
          return tmp;
      };
      
      let content = document.getElementById('content');
      let isInContent = function (e) {
          if (!e) return false;
          if (!e.parentElement || e.parentElement === document.body) return false;
          if (e.parentElement === content) return true;
          return isInContent(e.parentElement);
      }
      
      /** Handler to take input and process links in gcode browser
       */
      let ref = null;
      let paster = () => {
          if (ref) clearTimeout(ref);
          ref = setTimeout(() => {
              let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
              let v = docE('getElementById', 'gcode-paste-input').value;
              v = v.replace(codeRx, (s) => {
                  if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '">' + s + '</a>';
                  return s;
              });
      
              docE('getElementById', 'gcode-paste').innerHTML = '<pre style="text-align:left">' + v + '</pre>';
          }, 500);
      };
      
      
      // check to see if this page is the one we want, otherwise do nothing
      let loc = null;
      let pos = null;
      let hash = null;
      let qstring = null;
      
      const evalLocation = () => {
          loc = document.location + '';
          pos = loc.indexOf('#');
          hash = null;
          if (pos > -1) {
              hash = loc.substring(pos);
              loc = loc.substring(0, pos);
          }
      
          qstring = '';
          pos = loc.indexOf('?');
          if (pos > -1) {
              qstring = loc.substring(pos + 1);
              loc = loc.substring(0, pos);
          }
      };
      
      const moveToHash = () => {
          if (hash) {
              document.location = hash;
          }
      };
      
      // current location details...
      evalLocation();
      
      if (loc.endsWith('/Wiki/Gcode')) {
          // current page is the gcode wiki...
      
          // find the menu
          let topTitle = docE('getElementsByClassName', 'toc-title')[0];
      
          // add the link to run the mega-page import...
          let squirt = document.createElement('div');
          squirt.innerHTML = '<a id="create-mega-page-link" href="javascript:;">Create Mega-Page</a>';
      
          topTitle.parentElement.insertBefore(squirt, topTitle.nextSibling);
      
          setTimeout(() => {
              // take a beat, wire the event clicker (greasemonkey complication)...
              let tmp = null;
              (tmp = document.getElementById('create-mega-page-link')) ? tmp.addEventListener('click', () => processMegaPage()) : null;
          }, 50);
      
          // function handler for the click...
          processMegaPage = () => {
              squirt.innerHTML = '';
      
              // find all the specific links
              let glinks = docE('getElementsByTagName', 'a').filter(a => {
                  let t = a.innerText;
                  let h = '' + a.href;
                  h = h.substring(h.indexOf('/', 10));
                  return (t.match(codeRx) && !h.startsWith('/Wiki/Gcode') && (h.startsWith('/Wiki/M') || h.startsWith('/Wiki/G')));
              });
      
              // a function that represents the work for a page download
              let processor = a => {
      
                  return new Promise((resolve, reject) => {
      
                      // download...
                      fetch(a.href).then(async response => {
      
                          // the document dext...
                          let t = await response.text();
      
                          // pull out the good stuff...
                          let from = t.indexOf('<div id="Wiki_Details">');
                          let to = t.lastIndexOf('<div class="clearer">', t.lastIndexOf('<div itemprop="author"'));
                          t = t.substring(from, to);
      
                          // dont need this link in there now...
                          t = t.replace('<p>Back to the <a href="/Wiki/Gcode">Gcode Dictionary</a></p>', '');
      
                          // make a div for it
                          const d = document.createElement('div');
                          d.innerHTML = t;
      
                          // replace the link tag's parent <p>
                          let p = a.parentElement;
                          p.parentNode.replaceChild(d, p);
      
                          // end of task
                          resolve();
      
                      }).catch(reject);
                  });
              };
      
              let count = 0;
      
              // a function that represents a processing thread so we can start N of them
              let runner = async () => {
                  while (glinks.length > 0) {
                      count++;
                      if (count % 25 === 0) console.log(count);
                      let x = glinks.shift();
                      try {
                          await processor(x);
                      } catch (e) {
                          console.log(e);
                      }
                  }
              };
      
              // start the runners...
              let runnerCount = 1;
              (async () => {
                  let time = new Date().getTime();
      
                  console.log('links to process: ' + glinks.length);
      
                  let runners = [];
                  for (let i = 0; i < runnerCount; i++) runners.push(runner());
      
                  await Promise.all(runners);
      
                  console.log('DONE!!! (' + ((new Date().getTime() - time) / 1000) + ' seconds)');
      
                  moveToHash();
      
                  squirt.innerHTML = '<a id="cross-linker-link" href="javascript:;">Cross-link gcodes</a><br>'
                      + '<a id="setup-gcode-browser-link" href="javascript:;">Setup GCode Browser</a><br><br>';
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('cross-linker-link')) ? tmp.addEventListener('click', () => crossLinkDoc()) : null;
                      (tmp = document.getElementById('setup-gcode-browser-link')) ? tmp.addEventListener('click', () => setupGcodeBrowser()) : null;
                  }, 50);
              })();
      
              crossLinkDoc = () => {
                  if (crossLinked) return;
                  crossLinked = true;
      
                  let link = document.getElementById('cross-linker-link');
                  link.parentElement.removeChild(link);
      
                  /** Parse the references out of the document
                   */
                  docE('getElementsByClassName', 'header').forEach(sect => {
                      let s = sect.id + '';
                      s.replace(codeRx, sx => {
                          sx = sx.replace('_', '.');
                          if (!codeMap[sx]) {
                              codeMap[sx] = s;
                          }
                      });
                  });
      
                  /** Cross-link gcode references through the document
                   */
                  docE('getElementsByTagName', ['p', 'li', 'pre', 'strong']).forEach(tag => {
                      if (!tag) return;
                      if (!isInContent(tag)) return;
                      let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
                      let h = ('' + tag.innerHTML).trim();
                    	if (h.startsWith('<div/')) return;
                      tag.innerHTML = h.replace(/(?<!_)([GM]+[0-9]+(.[0-9])?)/g, (s) => {
                          if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '" class="crosslink">' + s + '</a>';
                          return s;
                      });
                  });
      
                  evalLocation();
                  moveToHash();
              };
      
              setupGcodeBrowser = () => {
                  crossLinkDoc();
      
                  if (squirt) {
                      squirt.parentElement.removeChild(squirt);
                      squirt = null;
                  }
      
                  /** Strip the main wrapper element of styling
                   */
                  let wrap = docE('getElementById', 'page');
                  wrap.className = '';
                  wrap.id = 'gnavPage';
      
                  /** Remove styling of sidebar nested element
                   */
                  docE('getElementById', 'sidebar-wiki-toc').className = '';
                  docE('getElementById', 'sidebar-wiki-toc').id = 'gnavSidebar';
      
                  /** Strip the sidebar
                   */
                  let sb = null;
                  sb = docE('getElementById', 'page-sidebar');
                  sb.innerHTML = '';
                  sb.id = 'gnavSidebar';
      
                  /** Strip the main area
                   */
                  let main = docE('getElementById', 'main');
                  main.id = 'gnavMain';
                  docE('getElementsByClassName', 'articleContainer')[0].className = '';
      
                  /** Clear our other elements not needed
                   */
                  let mb = docE('getElementById', 'mainBody');
                  for (let kid of mb.children) {
                      if (kid.id !== 'contentFloat') mb.removeChild(kid);
                  }
      
                  let bg = docE('getElementById', 'background');
                  for (let kid of bg.children) {
                      if (kid.id === 'gnavPage') break;
                      else bg.removeChild(kid);
                  }
      
                  for (let kid of document.body.children) {
                      if (kid.id !== 'background') document.body.removeChild(kid);
                  }
      
                  docE('getElementById', 'content').id = 'offContent';
      
                  /** Apply new styles to the wrapper, sidebar and main areas
                   */
                  wrap.setAttribute('style', 'display:flex;gap:1em;');
                  sb.setAttribute('style', 'flex: 1 1 45%; height: 100vh;');
                  main.setAttribute('style', 'flex: 1 1 50%; height: 100vh');
      
                  /** Input field to paste the gcode
                   */
                  sb.innerHTML = `
      <div style="width: 45vw; height: 100vh; position: fixed;overflow: scroll;">
          <textarea id="gcode-paste-input"></textArea>
        <hr />
        <pre id="gcode-paste" style="text-align: left"><center>( paste gcode above )</center></pre>
      </div>`;
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('gcode-paste-input')) ? tmp.addEventListener('keyup', () => paster()) : null;
                  }, 50);
              };
          };
      }
      
      // auto-trigger...
      if (qstring === 'inflateContent') {
          processMegaPage();
      }
      
      posted in General Discussion
      theKM
      theKM
    • RE: Repository for CNC Config's and Macro's

      @o_lampe ...I originally replied that the script no longer works, but then I re-wrote it to include a content downloader that re-creates the previous structure of the single mega-page before setting up the gcode browser.

      The updated script and details are above if you want to give it a go (I edited the post to reflect the new changes).

      posted in CNC
      theKM
      theKM
    • Mega GCode docco page + gcode file navigator

      For fans of the old mega-gcode-page...

      I was starting to miss the mega page, but understand why they had to do it.

      So I messed with my greaseMonkey script. Below is a script that re-creates the original mega-page by plucking content from the little gcode pages and squirting them into the main page. With three processing "threads" it rips the individual pages down and assembles the mega page in around 10-12 seconds on my connection (six threads is amusing, but it trips the server's rate limit, because it starts to look like a DDOS attack, so, 3 is ideal, IMO 🙂 ).

      To run it, browse to the gcode page ( https://duet3d.dozuki.com/Wiki/Gcode ) open JS console (ctrl + shift + J in chrome, ctrl + shift + i in firefox), copy and paste this in... hit enter to run it.

      When it runs, it should put a "Create Mega-Page" link at the top of the left hand menu... click that...

      You should see the scroll bar on the right go tiny as it downloads and injects the content from all the separate gcode pages into this page. What results is a page exactly like what used to be there, menu on the left still works, you can `ctrl+F' to find content, etc.

      _
      Script to copy and paste:

      // ==UserScript==
      // @name     GCode Wiki script
      // @version  1
      // @include     https://duet3d.dozuki.com/Wiki/Gcode*
      // ==/UserScript==
      
      /** for processing injected menu clicks, gets changed
       * to proper implementation when needed
       */
      let processMegaPage = () => {};
      let crossLinkDoc = () => {};
      let setupGcodeBrowser = () => {};
      
      let codeMap = {};
      let crossLinked = false;
      
      // for identifying gcode references
      let codeRx = /[GM]+[0-9]+(.[0-9])?/g;
      
      /** Helper function with finding things, so I don't need a framework
       */
      let docE = (f, inp) => {
          if (Array.isArray(inp)) {
              return (inp.map(i => docE(f, i))).flat();
          }
          let seek = document[f](inp);
          if (!seek || seek instanceof HTMLElement) return seek;
          let tmp = [];
          for (let t of seek) tmp.push(t);
          return tmp;
      };
      
      let content = document.getElementById('content');
      let isInContent = function (e) {
          if (!e) return false;
          if (!e.parentElement || e.parentElement === document.body) return false;
          if (e.parentElement === content) return true;
          return isInContent(e.parentElement);
      }
      
      /** Handler to take input and process links in gcode browser
       */
      let ref = null;
      let paster = () => {
          if (ref) clearTimeout(ref);
          ref = setTimeout(() => {
              let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
              let v = docE('getElementById', 'gcode-paste-input').value;
              v = v.replace(codeRx, (s) => {
                  if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '">' + s + '</a>';
                  return s;
              });
      
              docE('getElementById', 'gcode-paste').innerHTML = '<pre style="text-align:left">' + v + '</pre>';
          }, 500);
      };
      
      
      // check to see if this page is the one we want, otherwise do nothing
      let loc = null;
      let pos = null;
      let hash = null;
      let qstring = null;
      
      const evalLocation = () => {
          loc = document.location + '';
          pos = loc.indexOf('#');
          hash = null;
          if (pos > -1) {
              hash = loc.substring(pos);
              loc = loc.substring(0, pos);
          }
      
          qstring = '';
          pos = loc.indexOf('?');
          if (pos > -1) {
              qstring = loc.substring(pos + 1);
              loc = loc.substring(0, pos);
          }
      };
      
      const moveToHash = () => {
          if (hash) {
              document.location = hash;
          }
      };
      
      // current location details...
      evalLocation();
      
      if (loc.endsWith('/Wiki/Gcode')) {
          // current page is the gcode wiki...
      
          // find the menu
          let topTitle = docE('getElementsByClassName', 'toc-title')[0];
      
          // add the link to run the mega-page import...
          let squirt = document.createElement('div');
          squirt.innerHTML = '<a id="create-mega-page-link" href="javascript:;">Create Mega-Page</a>';
      
          topTitle.parentElement.insertBefore(squirt, topTitle.nextSibling);
      
          setTimeout(() => {
              // take a beat, wire the event clicker (greasemonkey complication)...
              let tmp = null;
              (tmp = document.getElementById('create-mega-page-link')) ? tmp.addEventListener('click', () => processMegaPage()) : null;
          }, 50);
      
          // function handler for the click...
          processMegaPage = () => {
              squirt.innerHTML = '';
      
              // find all the specific links
              let glinks = docE('getElementsByTagName', 'a').filter(a => {
                  let t = a.innerText;
                  let h = '' + a.href;
                  h = h.substring(h.indexOf('/', 10));
                  return (t.match(codeRx) && !h.startsWith('/Wiki/Gcode') && (h.startsWith('/Wiki/M') || h.startsWith('/Wiki/G')));
              });
      
              // a function that represents the work for a page download
              let processor = a => {
      
                  return new Promise((resolve, reject) => {
      
                      // download...
                      fetch(a.href).then(async response => {
      
                          // the document dext...
                          let t = await response.text();
      
                          // pull out the good stuff...
                          let from = t.indexOf('<div id="Wiki_Details">');
                          let to = t.lastIndexOf('<div class="clearer">', t.lastIndexOf('<div itemprop="author"'));
                          t = t.substring(from, to);
      
                          // dont need this link in there now...
                          t = t.replace('<p>Back to the <a href="/Wiki/Gcode">Gcode Dictionary</a></p>', '');
      
                          // make a div for it
                          const d = document.createElement('div');
                          d.innerHTML = t;
      
                          // replace the link tag's parent <p>
                          let p = a.parentElement;
                          p.parentNode.replaceChild(d, p);
      
                          // end of task
                          resolve();
      
                      }).catch(reject);
                  });
              };
      
              let count = 0;
      
              // a function that represents a processing thread so we can start N of them
              let runner = async () => {
                  while (glinks.length > 0) {
                      count++;
                      if (count % 25 === 0) console.log(count);
                      let x = glinks.shift();
                      try {
                          await processor(x);
                      } catch (e) {
                          console.log(e);
                      }
                  }
              };
      
              // start the runners...
              let runnerCount = 1;
              (async () => {
                  let time = new Date().getTime();
      
                  console.log('links to process: ' + glinks.length);
      
                  let runners = [];
                  for (let i = 0; i < runnerCount; i++) runners.push(runner());
      
                  await Promise.all(runners);
      
                  console.log('DONE!!! (' + ((new Date().getTime() - time) / 1000) + ' seconds)');
      
                  moveToHash();
      
                  squirt.innerHTML = '<a id="cross-linker-link" href="javascript:;">Cross-link gcodes</a><br>'
                      + '<a id="setup-gcode-browser-link" href="javascript:;">Setup GCode Browser</a><br><br>';
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('cross-linker-link')) ? tmp.addEventListener('click', () => crossLinkDoc()) : null;
                      (tmp = document.getElementById('setup-gcode-browser-link')) ? tmp.addEventListener('click', () => setupGcodeBrowser()) : null;
                  }, 50);
              })();
      
              crossLinkDoc = () => {
                  if (crossLinked) return;
                  crossLinked = true;
      
                  let link = document.getElementById('cross-linker-link');
                  link.parentElement.removeChild(link);
      
                  /** Parse the references out of the document
                   */
                  docE('getElementsByClassName', 'header').forEach(sect => {
                      let s = sect.id + '';
                      s.replace(codeRx, sx => {
                          sx = sx.replace('_', '.');
                          if (!codeMap[sx]) {
                              codeMap[sx] = s;
                          }
                      });
                  });
      
                  /** Cross-link gcode references through the document
                   */
                  docE('getElementsByTagName', ['p', 'li', 'pre', 'strong']).forEach(tag => {
                      if (!tag) return;
                      if (!isInContent(tag)) return;
                      let linkStyle = 'all:unset;cursor:pointer;text-decoration:underline;color:blue;';
                      let h = ('' + tag.innerHTML).trim();
                    	if (h.startsWith('<div/')) return;
                      tag.innerHTML = h.replace(/(?<!_)([GM]+[0-9]+(.[0-9])?)/g, (s) => {
                          if (codeMap[s]) return '<a href="#' + codeMap[s] + '" style="' + linkStyle + '" class="crosslink">' + s + '</a>';
                          return s;
                      });
                  });
      
                  evalLocation();
                  moveToHash();
              };
      
              setupGcodeBrowser = () => {
                  crossLinkDoc();
      
                  if (squirt) {
                      squirt.parentElement.removeChild(squirt);
                      squirt = null;
                  }
      
                  /** Strip the main wrapper element of styling
                   */
                  let wrap = docE('getElementById', 'page');
                  wrap.className = '';
                  wrap.id = 'gnavPage';
      
                  /** Remove styling of sidebar nested element
                   */
                  docE('getElementById', 'sidebar-wiki-toc').className = '';
                  docE('getElementById', 'sidebar-wiki-toc').id = 'gnavSidebar';
      
                  /** Strip the sidebar
                   */
                  let sb = null;
                  sb = docE('getElementById', 'page-sidebar');
                  sb.innerHTML = '';
                  sb.id = 'gnavSidebar';
      
                  /** Strip the main area
                   */
                  let main = docE('getElementById', 'main');
                  main.id = 'gnavMain';
                  docE('getElementsByClassName', 'articleContainer')[0].className = '';
      
                  /** Clear our other elements not needed
                   */
                  let mb = docE('getElementById', 'mainBody');
                  for (let kid of mb.children) {
                      if (kid.id !== 'contentFloat') mb.removeChild(kid);
                  }
      
                  let bg = docE('getElementById', 'background');
                  for (let kid of bg.children) {
                      if (kid.id === 'gnavPage') break;
                      else bg.removeChild(kid);
                  }
      
                  for (let kid of document.body.children) {
                      if (kid.id !== 'background') document.body.removeChild(kid);
                  }
      
                  docE('getElementById', 'content').id = 'offContent';
      
                  /** Apply new styles to the wrapper, sidebar and main areas
                   */
                  wrap.setAttribute('style', 'display:flex;gap:1em;');
                  sb.setAttribute('style', 'flex: 1 1 45%; height: 100vh;');
                  main.setAttribute('style', 'flex: 1 1 50%; height: 100vh');
      
                  /** Input field to paste the gcode
                   */
                  sb.innerHTML = `
      <div style="width: 45vw; height: 100vh; position: fixed;overflow: scroll;">
          <textarea id="gcode-paste-input"></textArea>
        <hr />
        <pre id="gcode-paste" style="text-align: left"><center>( paste gcode above )</center></pre>
      </div>`;
      
                  setTimeout(() => {
                      // take a beat, wire the event clicker (greasemonkey complication)...
                      let tmp = null;
                      (tmp = document.getElementById('gcode-paste-input')) ? tmp.addEventListener('keyup', () => paster()) : null;
                  }, 50);
              };
          };
      }
      
      // auto-trigger...
      if (qstring === 'inflateContent') {
          processMegaPage();
      }
      

      _
      CROSS LINKING
      After all the gcode content is downloaded and set up on the page, there will be two links at the top of the menu... click on "Cross-link gcodes", and the script will then link all the GCode mentions on the page with other parts of the document, so you can bounce around from wherever you see a gcode mentioned.

      _
      GCODE BROWSER

      Click on "Setup Gcode Browser"... it will cross-link the gcodes as mentioned, and the page gets wrangled to set up the left hand size as a gcode panel with the documentation on the right.

      Paste the contents of a gcode file (like config.g ) into the text area, and it will update to show the gcode, with all the codes linked to the documentation on the right.

      This makes it handy to browse what's happening in a gcode file, the browser know where you're at (for example, rather than assume you know what a code is doing, click on it, confirm in the docco that it does what you assume, then just click back in the browser to be right back where you're at).

      This has greatly helped my understanding of what's going on in these setup files so I can set up my CNC machine.

      _
      Greasemonkey

      If you like this script, and wish it was just part of the site, then you can use a plugin called GreaseMonkey in FireFox (chrome has one called TamperMonkey that runs the same thing)... which just tells the browser to run scripts against sites you configure. The above script takes greasemonkey into account and should work as is.

      The script is also looking for "inflateContent" in the URL, so if you install it in greaseMoneky, and then just browse to https://duet3d.dozuki.com/Wiki/Gcode?inflateContent , the page will automatically inflate to the mega-page without needing to click anything.
      _

      ...if the wiki maintainers wish, this script could be included in the top html document. It only works when it sees that it's on the correct page, and then injects the link. So the site stays as-is, until the user clicks the link to create the voltron mega-page.

      I'm still fiddling with it to do all the cross-linking that my other script did previously, but thought people might like this to recreate what was, while still allowing wiki maintainers to do the separate page thing.

      posted in General Discussion
      theKM
      theKM
    • RE: Configurable Main Menu ( /w source code )

      @t3p3tony , no worries! Not even a problem. I used to be a maintainer over on Apache/Jakarta stuff, totally get it.

      My other branch has the menu generating whole pages/routes to make control panels, and that's much less likely to be merged 🙂

      Forks are part of the coolness of open source. The Ooznest CNC version is what convinced me to take the leap on something not a printer, and now I don't think I'd like it any other way... and now it's part of the core, sweetness.

      posted in Duet Web Control
      theKM
      theKM