├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── help_wanted.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── ci.yml ├── .gitignore ├── .npmrc ├── .vscode ├── .debug.script.mjs ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build ├── icon.icns ├── icon.png └── icons │ ├── icon.icns │ └── icon.png ├── electron-builder.json5 ├── electron-vite-vue.gif ├── electron ├── electron-env.d.ts ├── main │ └── index.ts └── preload │ └── index.ts ├── index.html ├── package.json ├── postcss.config.js ├── public ├── logo.svg └── nozzle.svg ├── scripts ├── Events │ ├── Job.Placement.Complete.js │ ├── Nozzle.AfterPick.js │ ├── Nozzle.AfterPlace.js │ ├── Nozzle.BeforePick.js │ └── Nozzle.BeforePlace.js └── httpBasic.js ├── src ├── App.vue ├── assets │ ├── airFlow.json │ ├── electron.svg │ ├── vite.svg │ └── vue.svg ├── components │ └── DashBoard.vue ├── demos │ ├── ipc.ts │ └── node.ts ├── main.ts ├── style.css └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.flat.txt └── vite.config.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🐞 Bug report 4 | about: Create a report to help us improve 5 | title: "[Bug] the title of bug report" 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | #### Describe the bug 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help_wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🥺 Help wanted 3 | about: Confuse about the use of electron-vue-vite 4 | title: "[Help] the title of help wanted report" 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the problem you confuse 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### What is the purpose of this pull request? 8 | 9 | - [ ] Bug fix 10 | - [ ] New Feature 11 | - [ ] Documentation update 12 | - [ ] Other 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - "**.md" 8 | - "**.spec.js" 9 | - ".idea" 10 | - ".vscode" 11 | - ".dockerignore" 12 | - "Dockerfile" 13 | - ".gitignore" 14 | - ".github/**" 15 | - "!.github/workflows/build.yml" 16 | 17 | jobs: 18 | build: 19 | runs-on: ${{ matrix.os }} 20 | 21 | strategy: 22 | matrix: 23 | os: [macos-latest, ubuntu-latest, windows-latest] 24 | arch: [x64, arm64] # Add ARM64 architecture here 25 | 26 | steps: 27 | - name: Checkout Code 28 | uses: actions/checkout@v3 29 | 30 | - name: Setup Node.js 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: 21 34 | 35 | - name: Install Dependencies 36 | run: npm install 37 | 38 | - name: Build Release Files 39 | run: npm run build -- --${{ matrix.arch }} 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Upload Installer 44 | uses: actions/upload-artifact@v3 45 | with: 46 | name: installer_${{ matrix.os }}_${{ matrix.arch }} 47 | path: | 48 | release/*/*.deb 49 | release/*/*.exe 50 | release/*/*.dmg 51 | retention-days: 5 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | job1: 13 | name: Check Not Allowed File Changes 14 | runs-on: ubuntu-latest 15 | outputs: 16 | markdown_change: ${{ steps.filter_markdown.outputs.change }} 17 | markdown_files: ${{ steps.filter_markdown.outputs.change_files }} 18 | steps: 19 | 20 | - name: Check Not Allowed File Changes 21 | uses: dorny/paths-filter@v2 22 | id: filter_not_allowed 23 | with: 24 | list-files: json 25 | filters: | 26 | change: 27 | - 'package-lock.json' 28 | - 'yarn.lock' 29 | - 'pnpm-lock.yaml' 30 | 31 | # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml 32 | - name: Comment About Changes We Can't Accept 33 | if: ${{ steps.filter_not_allowed.outputs.change == 'true' }} 34 | uses: actions/github-script@v6 35 | with: 36 | script: | 37 | let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions." 38 | try { 39 | const badFilesArr = [ 40 | 'package-lock.json', 41 | 'yarn.lock', 42 | 'pnpm-lock.yaml', 43 | ] 44 | const badFiles = badFilesArr.join('\n- ') 45 | const reviewMessage = `👋 Hey there spelunker. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main \`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/electron-vite/electron-vite-vue/issues/192` 46 | createdComment = await github.rest.issues.createComment({ 47 | owner: context.repo.owner, 48 | repo: context.repo.repo, 49 | issue_number: context.payload.number, 50 | body: reviewMessage, 51 | }) 52 | workflowFailMessage = `${workflowFailMessage} Please see ${createdComment.data.html_url} for details.` 53 | } catch(err) { 54 | console.log("Error creating comment.", err) 55 | } 56 | core.setFailed(workflowFailMessage) 57 | 58 | - name: Check Not Linted Markdown 59 | if: ${{ always() }} 60 | uses: dorny/paths-filter@v2 61 | id: filter_markdown 62 | with: 63 | list-files: shell 64 | filters: | 65 | change: 66 | - added|modified: '*.md' 67 | 68 | 69 | job2: 70 | name: Lint Markdown 71 | runs-on: ubuntu-latest 72 | needs: job1 73 | if: ${{ always() && needs.job1.outputs.markdown_change == 'true' }} 74 | steps: 75 | - name: Checkout Code 76 | uses: actions/checkout@v3 77 | with: 78 | ref: ${{ github.event.pull_request.head.sha }} 79 | 80 | - name: Lint markdown 81 | run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dist-electron 14 | release 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/.debug.env 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # lockfile 28 | package-lock.json 29 | pnpm-lock.yaml 30 | yarn.lock -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # For electron-builder 2 | # https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 3 | shamefully-hoist=true 4 | 5 | # For China 🇨🇳 developers 6 | # electron_mirror=https://npmmirror.com/mirrors/electron/ 7 | -------------------------------------------------------------------------------- /.vscode/.debug.script.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { createRequire } from 'node:module' 5 | import { spawn } from 'node:child_process' 6 | 7 | const pkg = createRequire(import.meta.url)('../package.json') 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 9 | 10 | // write .debug.env 11 | const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`) 12 | fs.writeFileSync(path.join(__dirname, '.debug.env'), envContent.join('\n')) 13 | 14 | // bootstrap 15 | spawn( 16 | // TODO: terminate `npm run dev` when Debug exits. 17 | process.platform === 'win32' ? 'npm.cmd' : 'npm', 18 | ['run', 'dev'], 19 | { 20 | stdio: 'inherit', 21 | env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "Vue.vscode-typescript-vue-plugin" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "compounds": [ 7 | { 8 | "name": "Debug App", 9 | "preLaunchTask": "Before Debug", 10 | "configurations": [ 11 | "Debug Main Process", 12 | "Debug Renderer Process" 13 | ], 14 | "presentation": { 15 | "hidden": false, 16 | "group": "", 17 | "order": 1 18 | }, 19 | "stopAll": true 20 | } 21 | ], 22 | "configurations": [ 23 | { 24 | "name": "Debug Main Process", 25 | "type": "node", 26 | "request": "launch", 27 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 28 | "windows": { 29 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 30 | }, 31 | "runtimeArgs": [ 32 | "--remote-debugging-port=9229", 33 | "." 34 | ], 35 | "envFile": "${workspaceFolder}/.vscode/.debug.env", 36 | "console": "integratedTerminal" 37 | }, 38 | { 39 | "name": "Debug Renderer Process", 40 | "port": 9229, 41 | "request": "attach", 42 | "type": "chrome", 43 | "timeout": 60000, 44 | "skipFiles": [ 45 | "/**", 46 | "${workspaceRoot}/node_modules/**", 47 | "${workspaceRoot}/dist-electron/**", 48 | // Skip files in host(VITE_DEV_SERVER_URL) 49 | "http://127.0.0.1:3344/**" 50 | ] 51 | }, 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.tsc.autoDetect": "off", 4 | "json.schemas": [ 5 | { 6 | "fileMatch": [ 7 | "/*electron-builder.json5", 8 | "/*electron-builder.json" 9 | ], 10 | "url": "https://json.schemastore.org/electron-builder" 11 | } 12 | ], 13 | "i18n-ally.localesPaths": [ 14 | "release/28.1.0/win-unpacked/locales" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Before Debug", 8 | "type": "shell", 9 | "command": "node .vscode/.debug.script.mjs", 10 | "isBackground": true, 11 | "problemMatcher": { 12 | "owner": "typescript", 13 | "fileLocation": "relative", 14 | "pattern": { 15 | // TODO: correct "regexp" 16 | "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", 17 | "file": 1, 18 | "line": 3, 19 | "column": 4, 20 | "code": 5, 21 | "message": 6 22 | }, 23 | "background": { 24 | "activeOnStart": true, 25 | "beginsPattern": "^.*VITE v.* ready in \\d* ms.*$", 26 | "endsPattern": "^.*\\[startup\\] Electron App.*$" 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2022-10-03 2 | 3 | [v2.1.0](https://github.com/electron-vite/electron-vite-vue/pull/267) 4 | 5 | - `vite-electron-plugin` is Fast, and WYSIWYG. 🌱 6 | - last-commit: db2e830 v2.1.0: use `vite-electron-plugin` instead `vite-plugin-electron` 7 | 8 | ## 2022-06-04 9 | 10 | [v2.0.0](https://github.com/electron-vite/electron-vite-vue/pull/156) 11 | 12 | - 🖖 Based on the `vue-ts` template created by `npm create vite`, integrate `vite-plugin-electron` 13 | - ⚡️ More simplify, is in line with Vite project structure 14 | - last-commit: a15028a (HEAD -> main) feat: hoist `process.env` 15 | 16 | ## 2022-01-30 17 | 18 | [v1.0.0](https://github.com/electron-vite/electron-vite-vue/releases/tag/v1.0.0) 19 | 20 | - ⚡️ Main、Renderer、preload, all built with vite 21 | 22 | ## 2022-01-27 23 | - Refactor the scripts part. 24 | - Remove `configs` directory. 25 | 26 | ## 2021-11-11 27 | - Refactor the project. Use vite.config.ts build `Main-process`, `Preload-script` and `Renderer-process` alternative rollup. 28 | - Scenic `Vue>=3.2.13`, `@vue/compiler-sfc` is no longer necessary. 29 | - If you prefer Rollup, Use rollup branch. 30 | 31 | ```bash 32 | Error: @vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc to be present in the dependency tree. 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 草鞋没号 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenPnP-Dashboard 2 | 3 | 🥳 A Dashboard showing progress bar, nozzle status for LumenPnP. Should complactiable with any OpenPnP machine, since it is based on OpenPnP's scripts events. 4 | 5 | # Download 6 | Download the latest builds on Actions, it supports Windows and Linux and macOS on X64 and ARM64. 7 | 8 | # Scripts 9 | Put the scripts files in OpenPnP's Scripts Events Folder. 10 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealCorebb/openpnp-dashboard/2a4515be29a3adf4a12491823c737a519e29cafc/build/icon.icns -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealCorebb/openpnp-dashboard/2a4515be29a3adf4a12491823c737a519e29cafc/build/icon.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealCorebb/openpnp-dashboard/2a4515be29a3adf4a12491823c737a519e29cafc/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealCorebb/openpnp-dashboard/2a4515be29a3adf4a12491823c737a519e29cafc/build/icons/icon.png -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", 3 | appId: "com.corebb.openpnpdashbaord", 4 | asar: true, 5 | productName: "OpenPnP-Dashboard", 6 | directories: { 7 | output: "release/${version}", 8 | }, 9 | files: ["dist", "dist-electron"], 10 | mac: { 11 | target: ["dmg"], 12 | artifactName: "${productName}-Mac-${version}-Installer.${ext}", 13 | }, 14 | win: { 15 | target: [ 16 | { 17 | target: "nsis", 18 | }, 19 | ], 20 | icon: "icons/icon.png", 21 | artifactName: "${productName}-Windows-${version}-Setup.${ext}", 22 | }, 23 | nsis: { 24 | oneClick: false, 25 | perMachine: false, 26 | allowToChangeInstallationDirectory: true, 27 | deleteAppDataOnUninstall: false, 28 | }, 29 | linux: { 30 | target: [ 31 | { 32 | target: "deb", 33 | }, 34 | ], 35 | icon: "icons/icon.icns", 36 | artifactName: "${productName}-Linux-${version}.${ext}", 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /electron-vite-vue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RealCorebb/openpnp-dashboard/2a4515be29a3adf4a12491823c737a519e29cafc/electron-vite-vue.gif -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: 'true' 6 | /** 7 | * The built directory structure 8 | * 9 | * ```tree 10 | * ├─┬ dist-electron 11 | * │ ├─┬ main 12 | * │ │ └── index.js > Electron-Main 13 | * │ └─┬ preload 14 | * │ └── index.mjs > Preload-Scripts 15 | * ├─┬ dist 16 | * │ └── index.html > Electron-Renderer 17 | * ``` 18 | */ 19 | APP_ROOT: string 20 | /** /dist/ or /public/ */ 21 | VITE_PUBLIC: string 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /electron/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, screen, shell } from 'electron' 2 | import express from 'express' 3 | import bodyParser from 'body-parser' 4 | import path from 'node:path' 5 | import { fileURLToPath } from 'node:url' 6 | import os from 'node:os' 7 | import { createRequire } from 'node:module' 8 | 9 | const require = createRequire(import.meta.url) 10 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 11 | 12 | // The built directory structure 13 | // 14 | // ├─┬ dist-electron 15 | // │ ├─┬ main 16 | // │ │ └── index.js > Electron-Main 17 | // │ └─┬ preload 18 | // │ └── index.mjs > Preload-Scripts 19 | // ├─┬ dist 20 | // │ └── index.html > Electron-Renderer 21 | // 22 | process.env.APP_ROOT = path.join(__dirname, '../..') 23 | 24 | export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') 25 | export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') 26 | export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL 27 | 28 | process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL 29 | ? path.join(process.env.APP_ROOT, 'public') 30 | : RENDERER_DIST 31 | 32 | // Disable GPU Acceleration for Windows 7 33 | if (os.release().startsWith('6.1')) app.disableHardwareAcceleration() 34 | 35 | // Set application name for Windows 10+ notifications 36 | if (process.platform === 'win32') app.setAppUserModelId(app.getName()) 37 | 38 | if (!app.requestSingleInstanceLock()) { 39 | app.quit() 40 | process.exit(0) 41 | } 42 | 43 | let win: BrowserWindow | null = null 44 | let statusServer: express.Express | null = null 45 | const preload = path.join(__dirname, '../preload/index.mjs') 46 | const indexHtml = path.join(RENDERER_DIST, 'index.html') 47 | 48 | // Status Server Function 49 | function startStatusServer() { 50 | const serverApp = express() 51 | const PORT = 10064 52 | 53 | // Middleware 54 | serverApp.use(bodyParser.json()) 55 | 56 | // Single machine status object with optional fields 57 | let machineStatus = { 58 | done: 0, 59 | total: 0, 60 | nozzles: [{ id: "N1" }, { id: "N2" }], 61 | state: '' 62 | } 63 | 64 | // Flexible update endpoint 65 | serverApp.post('/update-status', (req, res) => { 66 | const { 67 | done, 68 | total, 69 | nozzles: updatedNozzles, 70 | state 71 | } = req.body; 72 | 73 | // Update the nozzles while keeping existing nozzles intact 74 | if (updatedNozzles && Array.isArray(updatedNozzles)) { 75 | // Create a map for quick lookup of updated nozzle data by ID 76 | const updatedNozzlesMap = new Map(updatedNozzles.map(nozzle => [nozzle.id, nozzle])); 77 | 78 | // Update only the nozzles specified in the request body 79 | machineStatus.nozzles = machineStatus.nozzles.map(existingNozzle => { 80 | const updatedNozzle = updatedNozzlesMap.get(existingNozzle.id); 81 | return updatedNozzle ? { ...existingNozzle, ...updatedNozzle } : existingNozzle; 82 | }); 83 | } 84 | 85 | // Update the rest of the fields 86 | machineStatus = { 87 | done: done ?? machineStatus.done, 88 | total: total ?? machineStatus.total, 89 | nozzles: machineStatus.nozzles, // Already updated above 90 | state: state ?? machineStatus.state 91 | }; 92 | 93 | // Broadcast status update to renderer process 94 | if (win) { 95 | win.webContents.send('machine-status-updated', machineStatus) 96 | } 97 | 98 | res.json({ 99 | message: 'Status updated successfully' 100 | }) 101 | }) 102 | 103 | // Get current status 104 | serverApp.get('/status', (req, res) => { 105 | res.json(machineStatus) 106 | }) 107 | 108 | // Start the server 109 | const server = serverApp.listen(PORT, () => { 110 | console.log(`Machine Status API running on port ${PORT}`) 111 | }) 112 | 113 | return serverApp 114 | } 115 | 116 | async function createWindow() { 117 | const primaryDisplay = screen.getPrimaryDisplay(); 118 | const { width, height } = primaryDisplay.workAreaSize; // Excludes taskbar area 119 | win = new BrowserWindow({ 120 | title: 'Main window', 121 | icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'), 122 | width: width, // Set width to the full screen width 123 | height: height, // Set height to the full work area height 124 | frame: false, 125 | webPreferences: { 126 | preload, 127 | // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production 128 | // nodeIntegration: true, 129 | 130 | // Consider using contextBridge.exposeInMainWorld 131 | // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation 132 | // contextIsolation: false, 133 | }, 134 | }) 135 | 136 | if (VITE_DEV_SERVER_URL) { // #298 137 | win.loadURL(VITE_DEV_SERVER_URL) 138 | // Open devTool if the app is not packaged 139 | win.webContents.openDevTools() 140 | } else { 141 | win.loadFile(indexHtml) 142 | } 143 | 144 | // Test actively push message to the Electron-Renderer 145 | win.webContents.on('did-finish-load', () => { 146 | win?.webContents.send('main-process-message', new Date().toLocaleString()) 147 | }) 148 | 149 | // Make all links open with the browser, not with the application 150 | win.webContents.setWindowOpenHandler(({ url }) => { 151 | if (url.startsWith('https:')) shell.openExternal(url) 152 | return { action: 'deny' } 153 | }) 154 | // win.webContents.on('will-navigate', (event, url) => { }) #344 155 | 156 | // Start the status server 157 | statusServer = startStatusServer() 158 | } 159 | 160 | app.whenReady().then(createWindow) 161 | 162 | app.on('window-all-closed', () => { 163 | win = null 164 | if (process.platform !== 'darwin') app.quit() 165 | }) 166 | 167 | app.on('second-instance', () => { 168 | if (win) { 169 | // Focus on the main window if the user tried to open another 170 | if (win.isMinimized()) win.restore() 171 | win.focus() 172 | } 173 | }) 174 | 175 | app.on('activate', () => { 176 | const allWindows = BrowserWindow.getAllWindows() 177 | if (allWindows.length) { 178 | allWindows[0].focus() 179 | } else { 180 | createWindow() 181 | } 182 | }) 183 | 184 | // New window example arg: new windows url 185 | ipcMain.handle('open-win', (_, arg) => { 186 | const childWindow = new BrowserWindow({ 187 | webPreferences: { 188 | preload, 189 | nodeIntegration: true, 190 | contextIsolation: false, 191 | }, 192 | }) 193 | 194 | if (VITE_DEV_SERVER_URL) { 195 | childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`) 196 | } else { 197 | childWindow.loadFile(indexHtml, { hash: arg }) 198 | } 199 | }) 200 | 201 | // Optional: Add a handler to stop the server when the app is quitting 202 | app.on('will-quit', () => { 203 | // If you need to do any cleanup when the server is stopping 204 | if (statusServer) { 205 | // If you want to close the server explicitly 206 | // Note: Express server closes automatically when the app quits 207 | } 208 | }) -------------------------------------------------------------------------------- /electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, contextBridge } from 'electron' 2 | 3 | // --------- Expose some API to the Renderer process --------- 4 | contextBridge.exposeInMainWorld('ipcRenderer', { 5 | on(...args: Parameters) { 6 | const [channel, listener] = args 7 | return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args)) 8 | }, 9 | off(...args: Parameters) { 10 | const [channel, ...omit] = args 11 | return ipcRenderer.off(channel, ...omit) 12 | }, 13 | send(...args: Parameters) { 14 | const [channel, ...omit] = args 15 | return ipcRenderer.send(channel, ...omit) 16 | }, 17 | invoke(...args: Parameters) { 18 | const [channel, ...omit] = args 19 | return ipcRenderer.invoke(channel, ...omit) 20 | }, 21 | onMachineStatusUpdate: (callback) => 22 | ipcRenderer.on('machine-status-updated', callback) 23 | }) 24 | 25 | // --------- Preload scripts loading --------- 26 | function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { 27 | return new Promise((resolve) => { 28 | if (condition.includes(document.readyState)) { 29 | resolve(true) 30 | } else { 31 | document.addEventListener('readystatechange', () => { 32 | if (condition.includes(document.readyState)) { 33 | resolve(true) 34 | } 35 | }) 36 | } 37 | }) 38 | } 39 | 40 | const safeDOM = { 41 | append(parent: HTMLElement, child: HTMLElement) { 42 | if (!Array.from(parent.children).find(e => e === child)) { 43 | return parent.appendChild(child) 44 | } 45 | }, 46 | remove(parent: HTMLElement, child: HTMLElement) { 47 | if (Array.from(parent.children).find(e => e === child)) { 48 | return parent.removeChild(child) 49 | } 50 | }, 51 | } 52 | 53 | /** 54 | * https://tobiasahlin.com/spinkit 55 | * https://connoratherton.com/loaders 56 | * https://projects.lukehaas.me/css-loaders 57 | * https://matejkustec.github.io/SpinThatShit 58 | */ 59 | function useLoading() { 60 | const className = `loaders-css__square-spin` 61 | const styleContent = ` 62 | @keyframes square-spin { 63 | 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 64 | 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 65 | 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 66 | 100% { transform: perspective(100px) rotateX(0) rotateY(0); } 67 | } 68 | .${className} > div { 69 | animation-fill-mode: both; 70 | width: 50px; 71 | height: 50px; 72 | background: #fff; 73 | animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; 74 | } 75 | .app-loading-wrap { 76 | position: fixed; 77 | top: 0; 78 | left: 0; 79 | width: 100vw; 80 | height: 100vh; 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | background: #282c34; 85 | z-index: 9; 86 | } 87 | ` 88 | const oStyle = document.createElement('style') 89 | const oDiv = document.createElement('div') 90 | 91 | oStyle.id = 'app-loading-style' 92 | oStyle.innerHTML = styleContent 93 | oDiv.className = 'app-loading-wrap' 94 | oDiv.innerHTML = `
` 95 | 96 | return { 97 | appendLoading() { 98 | safeDOM.append(document.head, oStyle) 99 | safeDOM.append(document.body, oDiv) 100 | }, 101 | removeLoading() { 102 | safeDOM.remove(document.head, oStyle) 103 | safeDOM.remove(document.body, oDiv) 104 | }, 105 | } 106 | } 107 | 108 | // ---------------------------------------------------------------------- 109 | 110 | const { appendLoading, removeLoading } = useLoading() 111 | domReady().then(appendLoading) 112 | 113 | window.onmessage = (ev) => { 114 | ev.data.payload === 'removeLoading' && removeLoading() 115 | } 116 | 117 | setTimeout(removeLoading, 4999) 118 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Electron + Vite + Vue 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openpnp-dashboard", 3 | "version": "1.0.0", 4 | "main": "dist-electron/main/index.js", 5 | "description": "OpenPnP Dashboard.", 6 | "author": "Corebb ", 7 | "license": "MIT", 8 | "private": true, 9 | "keywords": [ 10 | "electron", 11 | "rollup", 12 | "vite", 13 | "vue3", 14 | "vue" 15 | ], 16 | "debug": { 17 | "env": { 18 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/" 19 | } 20 | }, 21 | "type": "module", 22 | "scripts": { 23 | "dev": "vite", 24 | "build": "vue-tsc --noEmit && vite build && electron-builder", 25 | "preview": "vite preview" 26 | }, 27 | "devDependencies": { 28 | "@vitejs/plugin-vue": "^5.0.4", 29 | "autoprefixer": "^10.4.20", 30 | "electron": "^29.1.1", 31 | "electron-builder": "^24.13.3", 32 | "postcss": "^8.4.49", 33 | "tailwindcss": "^3.4.16", 34 | "typescript": "~5.6.3", 35 | "vite": "^5.1.5", 36 | "vite-plugin-electron": "^0.28.4", 37 | "vite-plugin-electron-renderer": "^0.14.5", 38 | "vue": "^3.4.21", 39 | "vue-tsc": "^2.0.0" 40 | }, 41 | "dependencies": { 42 | "body-parser": "^1.20.3", 43 | "express": "^4.21.2", 44 | "tailwindcss-motion": "^1.0.0", 45 | "vue3-lottie": "^3.3.1" 46 | } 47 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/nozzle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /scripts/Events/Job.Placement.Complete.js: -------------------------------------------------------------------------------- 1 | function asyncHttpPostJson(url, jsonData) { 2 | var URL = java.net.URL; 3 | var HttpURLConnection = java.net.HttpURLConnection; 4 | 5 | var thread = new java.lang.Thread(function () { 6 | try { 7 | var connection = new URL(url).openConnection(); 8 | connection.setRequestMethod("POST"); 9 | 10 | // Set request headers for JSON 11 | connection.setRequestProperty("Content-Type", "application/json"); 12 | connection.setRequestProperty("Accept", "application/json"); 13 | connection.setDoOutput(true); 14 | 15 | // Convert JSON object to string 16 | var jsonString = JSON.stringify(jsonData); 17 | 18 | // Write JSON data to request body 19 | var outputStream = connection.getOutputStream(); 20 | var writer = new java.io.OutputStreamWriter(outputStream, "UTF-8"); 21 | writer.write(jsonString); 22 | writer.close(); 23 | 24 | // Get response 25 | var responseCode = connection.getResponseCode(); 26 | 27 | var reader = new java.io.BufferedReader(new java.io.InputStreamReader(connection.getInputStream())); 28 | 29 | var response = ""; 30 | var inputLine; 31 | while ((inputLine = reader.readLine()) !== null) { 32 | response += inputLine; 33 | } 34 | reader.close(); 35 | 36 | // Parse response back to object 37 | var responseData = JSON.parse(response); 38 | 39 | print("[Dashboard]", "Status Code: " + responseCode, "Response: " + response); 40 | 41 | return responseData; 42 | } catch (error) { 43 | print("Error: " + error); 44 | return null; 45 | } 46 | }); 47 | 48 | thread.start(); 49 | } 50 | 51 | var activePlacements = job.getActivePlacements(job.getRootPanelLocation()); 52 | var totalActivePlacements = job.getTotalActivePlacements(job.getRootPanelLocation()); 53 | 54 | // Example usage 55 | var postData = { 56 | done: totalActivePlacements - activePlacements, 57 | total: totalActivePlacements, 58 | }; 59 | 60 | asyncHttpPostJson("http://127.0.0.1:10064/update-status", postData); 61 | -------------------------------------------------------------------------------- /scripts/Events/Nozzle.AfterPick.js: -------------------------------------------------------------------------------- 1 | function asyncHttpPostJson(url, jsonData) { 2 | var URL = java.net.URL; 3 | var HttpURLConnection = java.net.HttpURLConnection; 4 | 5 | var thread = new java.lang.Thread(function () { 6 | try { 7 | var connection = new URL(url).openConnection(); 8 | connection.setRequestMethod("POST"); 9 | 10 | // Set request headers for JSON 11 | connection.setRequestProperty("Content-Type", "application/json"); 12 | connection.setRequestProperty("Accept", "application/json"); 13 | connection.setDoOutput(true); 14 | 15 | // Convert JSON object to string 16 | var jsonString = JSON.stringify(jsonData); 17 | 18 | // Write JSON data to request body 19 | var outputStream = connection.getOutputStream(); 20 | var writer = new java.io.OutputStreamWriter(outputStream, "UTF-8"); 21 | writer.write(jsonString); 22 | writer.close(); 23 | 24 | // Get response 25 | var responseCode = connection.getResponseCode(); 26 | 27 | var reader = new java.io.BufferedReader(new java.io.InputStreamReader(connection.getInputStream())); 28 | 29 | var response = ""; 30 | var inputLine; 31 | while ((inputLine = reader.readLine()) !== null) { 32 | response += inputLine; 33 | } 34 | reader.close(); 35 | 36 | // Parse response back to object 37 | var responseData = JSON.parse(response); 38 | 39 | print("[Dashboard]", "Status Code: " + responseCode, "Response: " + response); 40 | 41 | return responseData; 42 | } catch (error) { 43 | print("Error: " + error); 44 | return null; 45 | } 46 | }); 47 | 48 | thread.start(); 49 | } 50 | 51 | var nozzleName = nozzle.getName(); 52 | 53 | // Example usage 54 | var postData = { 55 | nozzles: [{ id: nozzleName, isVacActive: true, isPicking: false, isPlacing: false, hasComponent: true }], 56 | }; 57 | 58 | asyncHttpPostJson("http://127.0.0.1:10064/update-status", postData); 59 | -------------------------------------------------------------------------------- /scripts/Events/Nozzle.AfterPlace.js: -------------------------------------------------------------------------------- 1 | function asyncHttpPostJson(url, jsonData) { 2 | var URL = java.net.URL; 3 | var HttpURLConnection = java.net.HttpURLConnection; 4 | 5 | var thread = new java.lang.Thread(function () { 6 | try { 7 | var connection = new URL(url).openConnection(); 8 | connection.setRequestMethod("POST"); 9 | 10 | // Set request headers for JSON 11 | connection.setRequestProperty("Content-Type", "application/json"); 12 | connection.setRequestProperty("Accept", "application/json"); 13 | connection.setDoOutput(true); 14 | 15 | // Convert JSON object to string 16 | var jsonString = JSON.stringify(jsonData); 17 | 18 | // Write JSON data to request body 19 | var outputStream = connection.getOutputStream(); 20 | var writer = new java.io.OutputStreamWriter(outputStream, "UTF-8"); 21 | writer.write(jsonString); 22 | writer.close(); 23 | 24 | // Get response 25 | var responseCode = connection.getResponseCode(); 26 | 27 | var reader = new java.io.BufferedReader(new java.io.InputStreamReader(connection.getInputStream())); 28 | 29 | var response = ""; 30 | var inputLine; 31 | while ((inputLine = reader.readLine()) !== null) { 32 | response += inputLine; 33 | } 34 | reader.close(); 35 | 36 | // Parse response back to object 37 | var responseData = JSON.parse(response); 38 | 39 | print("[Dashboard]", "Status Code: " + responseCode, "Response: " + response); 40 | 41 | return responseData; 42 | } catch (error) { 43 | print("Error: " + error); 44 | return null; 45 | } 46 | }); 47 | 48 | thread.start(); 49 | } 50 | 51 | var nozzleName = nozzle.getName(); 52 | 53 | // Example usage 54 | var postData = { 55 | nozzles: [{ id: nozzleName, isVacActive: false, isPicking: false, isPlacing: false, hasComponent: false }], 56 | }; 57 | 58 | asyncHttpPostJson("http://127.0.0.1:10064/update-status", postData); 59 | -------------------------------------------------------------------------------- /scripts/Events/Nozzle.BeforePick.js: -------------------------------------------------------------------------------- 1 | function asyncHttpPostJson(url, jsonData) { 2 | var URL = java.net.URL; 3 | var HttpURLConnection = java.net.HttpURLConnection; 4 | 5 | var thread = new java.lang.Thread(function () { 6 | try { 7 | var connection = new URL(url).openConnection(); 8 | connection.setRequestMethod("POST"); 9 | 10 | // Set request headers for JSON 11 | connection.setRequestProperty("Content-Type", "application/json"); 12 | connection.setRequestProperty("Accept", "application/json"); 13 | connection.setDoOutput(true); 14 | 15 | // Convert JSON object to string 16 | var jsonString = JSON.stringify(jsonData); 17 | 18 | // Write JSON data to request body 19 | var outputStream = connection.getOutputStream(); 20 | var writer = new java.io.OutputStreamWriter(outputStream, "UTF-8"); 21 | writer.write(jsonString); 22 | writer.close(); 23 | 24 | // Get response 25 | var responseCode = connection.getResponseCode(); 26 | 27 | var reader = new java.io.BufferedReader(new java.io.InputStreamReader(connection.getInputStream())); 28 | 29 | var response = ""; 30 | var inputLine; 31 | while ((inputLine = reader.readLine()) !== null) { 32 | response += inputLine; 33 | } 34 | reader.close(); 35 | 36 | // Parse response back to object 37 | var responseData = JSON.parse(response); 38 | 39 | print("[Dashboard]", "Status Code: " + responseCode, "Response: " + response); 40 | 41 | return responseData; 42 | } catch (error) { 43 | print("Error: " + error); 44 | return null; 45 | } 46 | }); 47 | 48 | thread.start(); 49 | } 50 | 51 | var nozzleName = nozzle.getName(); 52 | 53 | // Example usage 54 | var postData = { 55 | nozzles: [{ id: nozzleName, isVacActive: true, isPicking: true, isPlacing: false, hasComponent: true }], 56 | }; 57 | 58 | asyncHttpPostJson("http://127.0.0.1:10064/update-status", postData); 59 | -------------------------------------------------------------------------------- /scripts/Events/Nozzle.BeforePlace.js: -------------------------------------------------------------------------------- 1 | function asyncHttpPostJson(url, jsonData) { 2 | var URL = java.net.URL; 3 | var HttpURLConnection = java.net.HttpURLConnection; 4 | 5 | var thread = new java.lang.Thread(function () { 6 | try { 7 | var connection = new URL(url).openConnection(); 8 | connection.setRequestMethod("POST"); 9 | 10 | // Set request headers for JSON 11 | connection.setRequestProperty("Content-Type", "application/json"); 12 | connection.setRequestProperty("Accept", "application/json"); 13 | connection.setDoOutput(true); 14 | 15 | // Convert JSON object to string 16 | var jsonString = JSON.stringify(jsonData); 17 | 18 | // Write JSON data to request body 19 | var outputStream = connection.getOutputStream(); 20 | var writer = new java.io.OutputStreamWriter(outputStream, "UTF-8"); 21 | writer.write(jsonString); 22 | writer.close(); 23 | 24 | // Get response 25 | var responseCode = connection.getResponseCode(); 26 | 27 | var reader = new java.io.BufferedReader(new java.io.InputStreamReader(connection.getInputStream())); 28 | 29 | var response = ""; 30 | var inputLine; 31 | while ((inputLine = reader.readLine()) !== null) { 32 | response += inputLine; 33 | } 34 | reader.close(); 35 | 36 | // Parse response back to object 37 | var responseData = JSON.parse(response); 38 | 39 | print("[Dashboard]", "Status Code: " + responseCode, "Response: " + response); 40 | 41 | return responseData; 42 | } catch (error) { 43 | print("Error: " + error); 44 | return null; 45 | } 46 | }); 47 | 48 | thread.start(); 49 | } 50 | 51 | var nozzleName = nozzle.getName(); 52 | 53 | // Example usage 54 | var postData = { 55 | nozzles: [{ id: nozzleName, isVacActive: true, isPicking: false, isPlacing: true, hasComponent: false }], 56 | }; 57 | 58 | asyncHttpPostJson("http://127.0.0.1:10064/update-status", postData); 59 | -------------------------------------------------------------------------------- /scripts/httpBasic.js: -------------------------------------------------------------------------------- 1 | function asyncHttpPostJson(url, jsonData) { 2 | var URL = java.net.URL; 3 | var HttpURLConnection = java.net.HttpURLConnection; 4 | 5 | var thread = new java.lang.Thread(function() { 6 | try { 7 | var connection = new URL(url).openConnection(); 8 | connection.setRequestMethod("POST"); 9 | 10 | // Set request headers for JSON 11 | connection.setRequestProperty("Content-Type", "application/json"); 12 | connection.setRequestProperty("Accept", "application/json"); 13 | connection.setDoOutput(true); 14 | 15 | // Convert JSON object to string 16 | var jsonString = JSON.stringify(jsonData); 17 | 18 | // Write JSON data to request body 19 | var outputStream = connection.getOutputStream(); 20 | var writer = new java.io.OutputStreamWriter(outputStream, "UTF-8"); 21 | writer.write(jsonString); 22 | writer.close(); 23 | 24 | // Get response 25 | var responseCode = connection.getResponseCode(); 26 | 27 | var reader = new java.io.BufferedReader( 28 | new java.io.InputStreamReader(connection.getInputStream()) 29 | ); 30 | 31 | var response = ""; 32 | var inputLine; 33 | while ((inputLine = reader.readLine()) !== null) { 34 | response += inputLine; 35 | } 36 | reader.close(); 37 | 38 | // Parse response back to object 39 | var responseData = JSON.parse(response); 40 | 41 | print("[Dashboard]","Status Code: " + responseCode,"Response: " + response); 42 | 43 | return responseData; 44 | } catch (error) { 45 | print("Error: " + error); 46 | return null; 47 | } 48 | }); 49 | 50 | thread.start(); 51 | } 52 | 53 | // Example usage 54 | var postData = { 55 | "done": 32, 56 | "total": 100, 57 | "nozzles": [ 58 | { "id": "L", "isVacActive": true, "isPicking": true, "isPlacing":false,"hasComponent":true }, 59 | { "id": "R", "isVacActive": false, "isPicking": false }], 60 | "state": "" 61 | } 62 | 63 | asyncHttpPostJson('http://127.0.0.1:10064/update-status', postData); -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/airFlow.json: -------------------------------------------------------------------------------- 1 | {"v":"5.7.5","fr":100,"ip":0,"op":100,"w":300,"h":900,"nm":"Comp 1","ddd":0,"metadata":{"backgroundColor":{"r":3,"g":18,"b":28}},"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Top Outlines","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Top Outlines","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[47.134,-40.277],[87.061,-0.426],[47.212,39.503],[46.547,39.503],[-109.97595977783203,40.29800796508789]],"i":[[0,0],[-0.022,-22.03],[22.03,-0.023],[0,0],[0,0]],"o":[[22.03,-0.021],[0.021,22.03],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":100,"ix":2},"e":{"a":0,"k":70,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[-413.99999999999994],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":100,"s":[-58],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":90,"ix":2},"w":{"a":0,"k":30,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[189.0670928955078,308.4999084472656],"ix":2},"a":{"a":0,"k":[-0.010990739610747369,0.01049074064746236],"ix":2},"s":{"a":0,"k":[46.48003876209259,46.48003876209259],"ix":2},"r":{"a":0,"k":90,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Middle Outlines","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Middle Outlines","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[57.447,-0.646],[126.378,-69.712],[195.444,-0.782],[126.512,68.286],[125.363,68.281],[-333.9002380371094,69.7490005493164]],"i":[[0,0],[-38.108,0.037],[-0.037,-38.106],[38.108,-0.037],[0,0],[0,0]],"o":[[-0.037,-38.107],[38.106,-0.038],[0.037,38.108],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":70,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[-720],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":100,"s":[-360],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":90,"ix":2},"w":{"a":0,"k":30,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[182.84901428222656,359.14898681640625],"ix":2},"a":{"a":0,"k":[-0.018483382307749707,0.01848247297739647],"ix":2},"s":{"a":0,"k":[46.48003876209259,46.48003876209259],"ix":2},"r":{"a":0,"k":90,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bottom Outlines","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Bottom Outlines","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[35.766,0.349],[86.567,51.051],[137.269,0.25],[86.468,-50.452],[85.621,-50.452],[-332.6119079589844,-51.153377532958984]],"i":[[0,0],[-28.03,0.027],[0.027,28.03],[28.029,-0.027],[0,0],[0,0]],"o":[[0.027,28.029],[28.029,-0.028],[-0.027,-28.029],[0,0],[0,0],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":70,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[-2],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":100,"s":[360],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":90,"ix":2},"w":{"a":0,"k":30,"ix":2},"lc":2,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[107.51300811767578,386.1111145019531],"ix":2},"a":{"a":0,"k":[-0.013487969531809085,-0.013987969531790156],"ix":2},"s":{"a":0,"k":[46.48003876209259,46.48003876209259],"ix":2},"r":{"a":0,"k":90,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"nozzle_convert_white.svg 1","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"hd":true,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","nm":"SVG","it":[{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1220.3778396060707,663.5],[1185.3778396060707,370.5],[1150.3778396060707,83.5],[1030.3778396060707,70.5],[905.3778396060706,93.5],[870.3778396060706,370.5],[834.3778396060706,653.5],[828.3778396060706,680.5],[1024.3778396060707,680.5],[1220.3778396060707,663.5],[1220.3778396060707,663.5]],"i":[[0,0],[19,151],[0,6],[89,0],[5,-22],[17,-140],[3,-16],[2,-9],[-65.33333333333337,0],[0,16],[0,0]],"o":[[0,-10],[-19,-151],[0,-10],[-116.00000000000011,0],[-2,12],[-17,140],[-2,9],[65.33333333333337,0],[180,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1460.3778396060707,780.5],[1460.3778396060707,750.5],[1025.3778396060707,750.5],[590.3778396060706,750.5],[590.3778396060706,780.5],[590.3778396060706,810.5],[1025.3778396060707,810.5],[1460.3778396060707,810.5],[1460.3778396060707,780.5],[1460.3778396060707,780.5]],"i":[[0,0],[0,10],[145,0],[145,0],[0,-10],[0,-10],[-145.0000000000001,0],[-145,0],[0,10],[0,0]],"o":[[0,-10],[-145,0],[-145.0000000000001,0],[0,10],[0,10],[145,0],[145,0],[0,-10],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1290.3778396060707,970.5],[1290.3778396060707,880.5],[1025.3778396060707,880.5],[760.3778396060706,880.5],[760.3778396060706,963.5],[767.3778396060706,1053.5],[1032.3778396060707,1060.5],[1290.3778396060707,1060.5],[1290.3778396060707,970.5],[1290.3778396060707,970.5]],"i":[[0,0],[0,30],[88.33333333333326,0],[88.33333333333337,0],[0,-27.666666666666742],[-4,-3],[-142.0000000000001,0],[-86,0],[0,30],[0,0]],"o":[[0,-30],[-88.33333333333326,0],[-88.33333333333348,0],[0,27.666666666666742],[0,46],[3,4],[86,0],[0,-30],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1193.3778396060707,1133.5],[863.3778396060706,1133.5],[1025.3778396060707,1136.5],[1193.3778396060707,1133.5],[1193.3778396060707,1133.5]],"i":[[0,0],[93,-2],[-181.0000000000001,0],[89,1],[0,0]],"o":[[-89,-2],[-92,1],[182,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1570.3778396060707,1655.5],[1570.3778396060707,1210.5],[1025.3778396060707,1210.5],[480.37783960607055,1210.5],[480.37783960607055,1648.5],[487.37783960607055,2093.5],[1032.3778396060707,2100.5],[1570.3778396060707,2100.5],[1570.3778396060707,1655.5],[1570.3778396060707,1655.5]],"i":[[0,0],[0,148.33333333333348],[181.66666666666674,0],[181.66666666666674,0],[0,-146],[-4,-3],[-296.0000000000001,0],[-179.33333333333348,0],[0,148.33333333333348],[0,0]],"o":[[0,-148.33333333333326],[-181.66666666666674,0],[-181.66666666666674,0],[0,146],[0,241],[3,4],[179.33333333333326,0],[0,-148.33333333333348],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1980.3778396060707,2225.5],[1980.3778396060707,2170.5],[1025.3778396060707,2170.5],[70.37783960607057,2170.5],[70.37783960607057,2225.5],[70.37783960607057,2280.5],[1025.3778396060707,2280.5],[1980.3778396060707,2280.5],[1980.3778396060707,2225.5],[1980.3778396060707,2225.5]],"i":[[0,0],[0,18.333333333333485],[318.3333333333335,0],[318.33333333333337,0],[0,-18.333333333333485],[0,-18.333333333333485],[-318.3333333333335,0],[-318.3333333333335,0],[0,18.333333333333485],[0,0]],"o":[[0,-18.333333333333485],[-318.33333333333326,0],[-318.33333333333337,0],[0,18.333333333333485],[0,18.333333333333485],[318.3333333333333,0],[318.33333333333326,0],[0,-18.333333333333485],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1570.3778396060707,2550.5],[1570.3778396060707,2360.5],[1025.3778396060707,2360.5],[480.37783960607055,2360.5],[480.37783960607055,2550.5],[480.37783960607055,2740.5],[1025.3778396060707,2740.5],[1570.3778396060707,2739.5],[1570.3778396060707,2550.5],[1570.3778396060707,2550.5]],"i":[[0,0],[0,63.333333333333485],[181.66666666666674,0],[181.66666666666674,0],[0,-63.333333333333485],[0,-63.333333333333485],[-181.66666666666686,0],[-181.66666666666674,0.3333333333334849],[0,63],[0,0]],"o":[[0,-63.333333333333485],[-181.66666666666674,0],[-181.66666666666674,0],[0,63.333333333333485],[0,63.333333333333485],[181.66666666666663,0],[181.66666666666674,-0.3333333333334849],[0,-63],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1575.3778396060707,2870.5],[1516.3778396060707,2810.5],[1026.3778396060707,2810.5],[536.3778396060706,2810.5],[483.37783960607055,2862.5],[430.37783960607055,2922.5],[1032.3778396060707,2930.5],[1634.3778396060707,2930.5],[1575.3778396060707,2870.5],[1575.3778396060707,2870.5]],"i":[[0,0],[19.666666666666742,20],[163.33333333333348,0],[163.33333333333337,0],[17.66666666666663,-17.333333333333485],[0,-4],[-331.0000000000001,0],[-200.66666666666674,0],[19.666666666666742,20],[0,0]],"o":[[-19.666666666666742,-20],[-163.33333333333326,0],[-163.33333333333337,0],[-17.66666666666663,17.333333333333485],[-29,29],[0,4],[200.66666666666674,0],[-19.666666666666742,-20],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1940.3778396060707,3010.5],[1030.3778396060707,3000.5],[120.37783960607057,3010.5],[1030.3778396060707,3020.5],[1940.3778396060707,3010.5],[1940.3778396060707,3010.5]],"i":[[0,0],[600,0],[0,-7],[-600.0000000000001,0],[0,7],[0,0]],"o":[[0,-7],[-600.0000000000001,0],[0,7],[600,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1605.3778396060707,3335.5],[1605.3778396060707,3095.5],[1028.3778396060707,3092.5],[450.37783960607055,3090.5],[450.37783960607055,3335.5],[450.37783960607055,3580.5],[1028.3778396060707,3578.5],[1605.3778396060707,3575.5],[1605.3778396060707,3335.5],[1605.3778396060707,3335.5]],"i":[[0,0],[0,80],[192.33333333333348,1],[192.66666666666674,0.6666666666665151],[0,-81.66666666666652],[0,-81.66666666666652],[-192.66666666666686,0.6666666666665151],[-192.33333333333348,1],[0,80],[0,0]],"o":[[0,-80],[-192.33333333333326,-1],[-192.66666666666674,-0.6666666666665151],[0,81.66666666666652],[0,81.66666666666652],[192.66666666666663,-0.6666666666665151],[192.33333333333326,-1],[0,-80],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1750.3778396060707,5981.5],[1826.3778396060707,5180.5],[1870.3778396060707,5180.5],[1870.3778396060707,4425.5],[1870.3778396060707,3670.5],[1030.3778396060707,3670.5],[190.37783960607055,3670.5],[190.37783960607055,4425.5],[190.37783960607055,5180.5],[238.37783960607055,5180.5],[298.37783960607055,5192.5],[310.37783960607055,5987.5],[310.37783960607055,6770.5],[1030.3778396060707,6770.5],[1750.3778396060707,6770.5],[1750.3778396060707,5981.5],[1750.3778396060707,5981.5]],"i":[[0,0],[-84,0],[-14.666666666666742,0],[0,251.66666666666697],[0,251.66666666666652],[280,0],[280,0],[0,-251.66666666666697],[0,-251.66666666666697],[-16,0],[-7,-7],[0,-596],[0,-261],[-240.0000000000001,0],[-240,0],[0,263],[0,0]],"o":[[0,-883],[14.666666666666742,0],[0,-251.66666666666697],[0,-251.66666666666697],[-280,0],[-280.0000000000001,0],[0,251.66666666666652],[0,251.66666666666697],[16,0],[26,0],[9,9],[0,261],[240,0],[240,0],[0,-263],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1310.3778396060707,6930.5],[1030.3778396060707,6920.5],[750.3778396060706,6930.5],[1030.3778396060707,6940.5],[1310.3778396060707,6930.5],[1310.3778396060707,6930.5]],"i":[[0,0],[180,0],[0,-6],[-180.0000000000001,0],[0,6],[0,0]],"o":[[0,-6],[-180.0000000000001,0],[0,6],[180,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1370.3778396060707,7055.5],[1326.3778396060707,7010.5],[1028.3778396060707,7010.5],[730.3778396060706,7010.5],[690.3778396060706,7046.5],[650.3778396060706,7091.5],[1032.3778396060707,7100.5],[1414.3778396060707,7100.5],[1370.3778396060707,7055.5],[1370.3778396060707,7055.5]],"i":[[0,0],[14.666666666666742,15],[99.33333333333326,0],[99.33333333333337,0],[13.333333333333371,-12],[0,-5],[-214.0000000000001,0],[-127.33333333333326,0],[14.666666666666742,15],[0,0]],"o":[[-14.666666666666742,-15],[-99.33333333333326,0],[-99.33333333333348,0],[-13.333333333333371,12],[-22,20],[0,5],[127.33333333333326,0],[-14.666666666666742,-15],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1460.3778396060707,7610.5],[1460.3778396060707,7170.5],[1025.3778396060707,7170.5],[590.3778396060706,7170.5],[590.3778396060706,7610.5],[590.3778396060706,8050.5],[1025.3778396060707,8050.5],[1460.3778396060707,8050.5],[1460.3778396060707,7610.5],[1460.3778396060707,7610.5]],"i":[[0,0],[0,146.66666666666697],[145,0],[145,0],[0,-146.66666666666697],[0,-146.66666666666697],[-145.0000000000001,0],[-145,0],[0,146.66666666666697],[0,0]],"o":[[0,-146.66666666666697],[-145,0],[-145.0000000000001,0],[0,146.66666666666697],[0,146.66666666666697],[145,0],[145,0],[0,-146.66666666666697],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[1370.3778396060707,8165.5],[1414.3778396060707,8120.5],[1030.3778396060707,8120.5],[646.3778396060706,8120.5],[690.3778396060706,8165.5],[734.3778396060706,8210.5],[1030.3778396060707,8210.5],[1326.3778396060707,8210.5],[1370.3778396060707,8165.5],[1370.3778396060707,8165.5]],"i":[[0,0],[-14.666666666666742,15],[128,0],[128,0],[-14.666666666666629,-15],[-14.666666666666629,-15],[-98.66666666666686,0],[-98.66666666666674,0],[-14.666666666666742,15],[0,0]],"o":[[14.666666666666742,-15],[-128,0],[-128.0000000000001,0],[14.666666666666629,15],[14.666666666666629,15],[98.66666666666663,0],[98.66666666666674,0],[14.666666666666742,-15],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[607.3778396060706,8192.5],[520.3778396060706,8105.5],[520.3778396060706,7610.5],[520.3778396060706,7116.5],[601.3778396060706,7033.5],[678.3778396060706,6938.5],[572.3778396060706,6922.5],[455.37783960607055,6890.5],[439.37783960607055,6860.5],[345.37783960607055,6860.5],[245.37783960607055,6850.5],[237.37783960607055,6047.5],[235.37783960607055,5255.5],[188.37783960607055,5255.5],[131.37783960607055,5245.5],[120.37783960607057,4430.5],[120.37783960607057,3613.5],[268.37783960607055,3577.5],[375.37783960607055,3575.5],[378.37783960607055,3333.5],[380.37783960607055,3090.5],[231.37783960607055,3090.5],[62.377839606070566,3066.5],[50.377839606070566,2995.5],[68.37783960607057,2938.5],[197.37783960607055,2929.5],[315.37783960607055,2931.5],[373.37783960607055,2872.5],[421.37783960607055,2806.5],[410.37783960607055,2579.5],[410.37783960607055,2360.5],[217.37783960607055,2360.5],[12.377839606070566,2348.5],[12.377839606070566,2104.5],[214.37783960607055,2096.5],[405.37783960607055,2095.5],[410.37783960607055,1621.5],[415.37783960607055,1147.5],[543.3778396060706,1109.5],[680.3778396060706,1065.5],[690.3778396060706,970.5],[690.3778396060706,881.5],[608.3778396060706,878.5],[525.3778396060706,875.5],[525.3778396060706,780.5],[525.3778396060706,685.5],[643.3778396060706,682.5],[760.3778396060706,674.5],[845.3778396060706,13.5],[1207.3778396060707,13.5],[1255.3778396060707,350.5],[1295.3778396060707,675.5],[1410.3778396060707,678.5],[1532.3778396060707,693.5],[1528.3778396060707,868.5],[1443.3778396060707,880.5],[1370.3778396060707,880.5],[1370.3778396060707,973.5],[1370.3778396060707,1067.5],[1498.3778396060707,1104.5],[1638.3778396060707,1152.5],[1650.3778396060707,1632.5],[1650.3778396060707,2101.5],[1846.3778396060707,2099.5],[2051.3778396060707,2111.5],[2060.3778396060707,2227.5],[2044.3778396060707,2344.5],[1839.3778396060707,2360.5],[1650.3778396060707,2360.5],[1650.3778396060707,2572.5],[1635.3778396060707,2800.5],[1680.3778396060707,2874.5],[1740.3778396060707,2931.5],[1855.3778396060707,2929.5],[2010.3778396060707,3010.5],[1827.3778396060707,3090.5],[1680.3778396060707,3090.5],[1680.3778396060707,3336.5],[1680.3778396060707,3581.5],[1790.3778396060707,3579.5],[1920.3778396060707,3595.5],[1940.3778396060707,4426.5],[1922.3778396060707,5249.5],[1862.3778396060707,5253.5],[1820.3778396060707,5247.5],[1818.3778396060707,6051.5],[1815.3778396060707,6855.5],[1727.3778396060707,6858.5],[1628.3778396060707,6890.5],[1618.3778396060707,6920.5],[1499.3778396060707,6920.5],[1380.3778396060707,6943.5],[1460.3778396060707,7044.5],[1540.3778396060707,7121.5],[1540.3778396060707,7608.5],[1540.3778396060707,8096.5],[1448.3778396060707,8188.5],[1356.3778396060707,8280.5],[1025.3778396060707,8280.5],[695.3778396060706,8280.5],[607.3778396060706,8192.5],[607.3778396060706,8192.5]],"i":[[0,0],[29,29],[0,165],[0,164.66666666666697],[-27,27.66666666666697],[2,7],[71,2],[15,29],[5.333333333333371,10],[31.333333333333314,0],[3,6],[1,436],[0.6666666666666856,264],[15.666666666666686,0],[5,5],[0,543],[0,7],[-104,1],[-35.666666666666686,0.6666666666665151],[-1,80.66666666666652],[-0.6666666666666856,81],[49.666666666666686,0],[19,24],[-6,37],[-6,6],[-71.99999999999999,-1],[-39.33333333333337,-0.6666666666665151],[-19.333333333333314,19.666666666666515],[7,4],[0,149],[0,73],[64.33333333333337,0],[9,9],[-17,10],[-104.99999999999999,0],[-63.666666666666686,0.3333333333334849],[-1.6666666666666856,158],[-1.6666666666666856,158],[-42.66666666666663,12.666666666666742],[-5,3],[0,49],[0,29.666666666666742],[27.33333333333337,1],[27.66666666666663,1],[0,31.66666666666663],[0,31.66666666666663],[-39.33333333333337,1],[0,3],[-4,10],[-12,-18],[-22,-179],[-13.333333333333258,-108.33333333333337],[-38.33333333333326,-1],[-5,-8],[13,-13],[43,0],[24.333333333333258,0],[0,-31],[0,-31.333333333333258],[-42.66666666666674,-12.333333333333258],[-7,-5],[0,-364],[0,-156.33333333333348],[-65.33333333333326,0.6666666666665151],[-7,-11],[0,-56],[13,-12],[159,0],[63,0],[0,-70.66666666666652],[12,-14],[-55,-52],[-20,-19],[-38.33333333333326,0.6666666666665151],[0,-75],[173,0],[49,0],[0,-82],[0,-81.66666666666652],[-36.66666666666674,0.6666666666665151],[-17,-16],[0,-802],[17,-9],[23,3],[14,2],[0.6666666666667425,-268],[1,-268],[29.333333333333258,-1],[11,-29],[3.3333333333332575,-10],[39.66666666666674,0],[0,-23],[-55,-53],[-26.666666666666742,-25.66666666666697],[0,-162.33333333333303],[0,-162.66666666666697],[30.666666666666742,-30.66666666666606],[30.666666666666742,-30.66666666666606],[110.33333333333326,0],[110,0],[29.33333333333337,29.33333333333394],[0,0]],"o":[[-29,-29],[0,-165],[0,-164.66666666666697],[27,-27.66666666666697],[44,-45],[-3,-9],[-102,-3],[-5.333333333333371,-10],[-31.333333333333314,0],[-52,0],[-3,-6],[-0.6666666666666856,-264],[-15.666666666666686,0],[-26,0],[-6.999999999999986,-7],[0,-443],[0,-24],[35.666666666666686,-0.6666666666665151],[1,-80.66666666666652],[0.6666666666666856,-81],[-49.666666666666686,0],[-147,0],[-15,-20],[4,-25],[6,-6],[39.333333333333314,0.6666666666665151],[19.333333333333314,-19.666666666666515],[36,-37],[-7,-5],[0,-73],[-64.33333333333331,0],[-137,0],[-16,-16],[6,-4],[63.666666666666686,-0.3333333333334849],[1.6666666666666856,-158],[1.6666666666666856,-158],[42.66666666666663,-12.666666666666742],[70,-21],[6,-3],[0,-29.666666666666742],[-27.33333333333337,-1],[-27.66666666666663,-1],[0,-31.66666666666663],[0,-31.66666666666663],[39.33333333333337,-1],[64,-1],[0,-19],[6,-18],[4,6],[13.333333333333258,108.33333333333337],[38.33333333333326,1],[76,2],[13,20],[-7,7],[-24.333333333333258,0],[0,31],[0,31.333333333333258],[42.66666666666674,12.333333333333258],[70,21],[9,8],[0,156.33333333333326],[65.33333333333326,-0.6666666666665151],[159,-2],[5,8],[0,79],[-13,13],[-63,0],[0,70.66666666666652],[0,174],[-14,15],[20,19],[38.33333333333326,-0.6666666666665151],[139,-3],[0,75],[-49,0],[0,82],[0,81.66666666666652],[36.66666666666674,-0.6666666666665151],[94,-2],[20,18],[0,774],[-10,5],[-14,-2],[-0.6666666666667425,268],[-1,268],[-29.333333333333258,1],[-88,3],[-3.3333333333332575,10],[-39.66666666666674,0],[-118,0],[0,17],[26.666666666666742,25.66666666666697],[0,162.33333333333303],[0,162.66666666666697],[-30.666666666666742,30.66666666666606],[-30.666666666666742,30.66666666666606],[-110.33333333333326,0],[-110.00000000000011,0],[-29.33333333333337,-29.33333333333394],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[110.98113250732422,426.0249938964844],"ix":2},"a":{"a":0,"k":[1030.18896484375,4140.25],"ix":2},"s":{"a":0,"k":[10.000000149011612,-10.000000149011612],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":7.0167096047110005e-15,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[150,450],"ix":2},"a":{"a":0,"k":[110.98113250732422,426.02498149871826],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":101,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /src/assets/electron.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/DashBoard.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 93 | 94 | 102 | -------------------------------------------------------------------------------- /src/demos/ipc.ts: -------------------------------------------------------------------------------- 1 | 2 | window.ipcRenderer.on('main-process-message', (_event, ...args) => { 3 | console.log('[Receive Main-process message]:', ...args) 4 | }) 5 | -------------------------------------------------------------------------------- /src/demos/node.ts: -------------------------------------------------------------------------------- 1 | import { lstat } from 'node:fs/promises' 2 | import { cwd } from 'node:process' 3 | 4 | lstat(cwd()).then(stats => { 5 | console.log('[fs.lstat]', stats) 6 | }).catch(err => { 7 | console.error(err) 8 | }) 9 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import Vue3Lottie from 'vue3-lottie' 4 | 5 | import './style.css' 6 | 7 | import './demos/ipc' 8 | // If you want use Node.js, the`nodeIntegration` needs to be enabled in the Main process. 9 | // import './demos/node' 10 | 11 | const app = createApp(App) 12 | app.use(Vue3Lottie) 13 | app.mount('#app') 14 | .$nextTick(() => { 15 | postMessage({ payload: 'removeLoading' }, '*') 16 | }) 17 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | #app{ 6 | width:100vw; 7 | height:100vh; 8 | background-color: #03121c; 9 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | 9 | interface Window { 10 | // expose in the `electron/preload/index.ts` 11 | ipcRenderer: import('electron').IpcRenderer 12 | } 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 4 | darkMode: ["selector", '[class="p-dark"]'], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [require("tailwindcss-motion")], 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { "path": "./tsconfig.node.json" } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts", "package.json", "electron"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.flat.txt: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'node:fs' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import electron from 'vite-plugin-electron' 5 | import renderer from 'vite-plugin-electron-renderer' 6 | import pkg from './package.json' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(({ command }) => { 10 | rmSync('dist-electron', { recursive: true, force: true }) 11 | 12 | const isServe = command === 'serve' 13 | const isBuild = command === 'build' 14 | const sourcemap = isServe || !!process.env.VSCODE_DEBUG 15 | 16 | return { 17 | plugins: [ 18 | vue(), 19 | electron([ 20 | { 21 | // Main process entry file of the Electron App. 22 | entry: 'electron/main/index.ts', 23 | onstart({ startup }) { 24 | if (process.env.VSCODE_DEBUG) { 25 | console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') 26 | } else { 27 | startup() 28 | } 29 | }, 30 | vite: { 31 | build: { 32 | sourcemap, 33 | minify: isBuild, 34 | outDir: 'dist-electron/main', 35 | rollupOptions: { 36 | // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, 37 | // we can use `external` to exclude them to ensure they work correctly. 38 | // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built. 39 | // Of course, this is not absolute, just this way is relatively simple. :) 40 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 41 | }, 42 | }, 43 | }, 44 | }, 45 | { 46 | entry: 'electron/preload/index.ts', 47 | onstart({ reload }) { 48 | // Notify the Renderer process to reload the page when the Preload scripts build is complete, 49 | // instead of restarting the entire Electron App. 50 | reload() 51 | }, 52 | vite: { 53 | build: { 54 | sourcemap: sourcemap ? 'inline' : undefined, // #332 55 | minify: isBuild, 56 | outDir: 'dist-electron/preload', 57 | rollupOptions: { 58 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 59 | }, 60 | }, 61 | }, 62 | } 63 | ]), 64 | // Use Node.js API in the Renderer process 65 | renderer(), 66 | ], 67 | server: process.env.VSCODE_DEBUG && (() => { 68 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) 69 | return { 70 | host: url.hostname, 71 | port: +url.port, 72 | } 73 | })(), 74 | clearScreen: false, 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import electron from 'vite-plugin-electron/simple' 5 | import pkg from './package.json' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(({ command }) => { 9 | fs.rmSync('dist-electron', { recursive: true, force: true }) 10 | 11 | const isServe = command === 'serve' 12 | const isBuild = command === 'build' 13 | const sourcemap = isServe || !!process.env.VSCODE_DEBUG 14 | 15 | return { 16 | plugins: [ 17 | vue(), 18 | electron({ 19 | main: { 20 | // Shortcut of `build.lib.entry` 21 | entry: 'electron/main/index.ts', 22 | onstart({ startup }) { 23 | if (process.env.VSCODE_DEBUG) { 24 | console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') 25 | } else { 26 | startup() 27 | } 28 | }, 29 | vite: { 30 | build: { 31 | sourcemap, 32 | minify: isBuild, 33 | outDir: 'dist-electron/main', 34 | rollupOptions: { 35 | // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, 36 | // we can use `external` to exclude them to ensure they work correctly. 37 | // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built. 38 | // Of course, this is not absolute, just this way is relatively simple. :) 39 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 40 | }, 41 | }, 42 | }, 43 | }, 44 | preload: { 45 | // Shortcut of `build.rollupOptions.input`. 46 | // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. 47 | input: 'electron/preload/index.ts', 48 | vite: { 49 | build: { 50 | sourcemap: sourcemap ? 'inline' : undefined, // #332 51 | minify: isBuild, 52 | outDir: 'dist-electron/preload', 53 | rollupOptions: { 54 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 55 | }, 56 | }, 57 | }, 58 | }, 59 | // Ployfill the Electron and Node.js API for Renderer process. 60 | // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process. 61 | // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer 62 | renderer: {}, 63 | }), 64 | ], 65 | server: process.env.VSCODE_DEBUG && (() => { 66 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) 67 | return { 68 | host: url.hostname, 69 | port: +url.port, 70 | } 71 | })(), 72 | clearScreen: false, 73 | } 74 | }) 75 | --------------------------------------------------------------------------------