83 Commits

Author SHA1 Message Date
fdfa9a1bb4 Design improvement 2022-10-27 21:58:02 +02:00
19f13237da Removed broken dependencies 2022-10-27 21:47:07 +02:00
77fe9cfd6e Fixed a vulnerabilitie 2022-09-06 20:39:31 +02:00
Lino Schmidt
39b36b59ce Merge pull request #2 from LinoSchmidt/dependabot/npm_and_yarn/terser-5.15.0
Bump terser from 5.14.0 to 5.15.0
2022-08-26 12:03:51 +02:00
dependabot[bot]
3421c632ee Bump terser from 5.14.0 to 5.15.0
Bumps [terser](https://github.com/terser/terser) from 5.14.0 to 5.15.0.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.14.0...v5.15.0)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-26 09:27:45 +00:00
1a1d5cc03e Fixed python dependency not found in vscode 2022-07-08 22:53:31 +02:00
2c89830bb7 More efficient log-loading system 2022-07-07 16:18:57 +02:00
2c0e374ed2 App close message when rendering preview removed 2022-07-07 12:14:03 +02:00
50897e005d Fixed settings wont repair itself 2022-07-07 12:10:30 +02:00
3c70be134a Fixed taskbar finishes before render 2022-07-07 11:51:27 +02:00
f6d1ed0477 Fixed icon paths after build 2022-07-07 11:28:51 +02:00
f8c2d56c8f Fixed finish icon not looking good 2022-07-07 01:41:16 +02:00
2f087eb7c6 Added flashing and new badge on finish 2022-07-07 01:40:38 +02:00
c4061822a2 Added finish icon 2022-07-07 01:27:39 +02:00
c009a098c7 Added notification badge 2022-07-07 01:21:51 +02:00
2809f2d4e8 Added minimize option when rendering 2022-07-06 18:43:37 +02:00
a213f06961 Added settings not resetting on new optiones 2022-07-06 18:08:42 +02:00
7607d41652 Renamed every side to page 2022-07-06 14:24:59 +02:00
df414f1a47 Fixed initial log table state 2022-07-06 14:06:10 +02:00
557f37aaa4 Added video player 2022-07-06 14:05:15 +02:00
1a0f0fda89 Added other video formats 2022-07-06 14:04:23 +02:00
a746e6aea6 Added finish side and notification 2022-06-29 22:54:44 +02:00
5d53da42f9 Updated progress design 2022-06-27 20:28:59 +02:00
aac54a6cf4 Added date format for other countrys 2022-06-27 19:59:53 +02:00
28a778e0ca Added date infos for logs 2022-06-27 15:38:04 +02:00
Lino Schmidt
aa4e557561 Merge pull request #1 from TheEpicco/main
Updated README.md
2022-06-25 09:28:42 +02:00
Epicco
274f622d6c Updated README.md 2022-06-25 02:32:15 +02:00
3e98ff73f7 Added progress bar 2022-06-24 23:25:20 +02:00
1a22c55572 Added warning message on exit while rendering 2022-06-23 12:34:40 +02:00
3607233df6 Improved Top Bar design 2022-06-23 01:21:29 +02:00
790f27d7d6 Fixed linux Top Bar 2022-06-22 21:56:54 +02:00
7b67559589 New Top Bar design 2022-06-22 20:10:26 +02:00
ade3cdd6fe Fixed paths on other platforms 2022-06-19 01:05:32 +02:00
9db98a12b4 Fixed open folder button for logs 2022-06-19 00:55:35 +02:00
a639d8e50f Fixed empty output path on abort 2022-06-19 00:45:05 +02:00
0485b9c0aa Fixed open folder button on other platforms 2022-06-19 00:41:43 +02:00
a99da677c2 Fixed full path logs on linux 2022-06-18 23:49:55 +02:00
7fc8c2efa2 Added multiplatform paths support 2022-06-18 23:23:05 +02:00
92df42bbee Added "could not start blender" error message 2022-06-18 13:18:58 +02:00
25cc105057 Added linux icons 2022-06-17 18:40:30 +02:00
bd14104f2f Removed unused files from bundel 2022-06-17 16:51:42 +02:00
cd586ed0ec Fixed script installs unused blender versions 2022-06-17 14:11:07 +02:00
f1b35ed671 Updated add log error message 2022-06-17 13:34:50 +02:00
7c082ee546 Added file links to the logs and output 2022-06-17 12:55:58 +02:00
5239cdb8f7 Fixed blender status on preview rendering 2022-06-17 12:24:14 +02:00
fc97efade5 Removed file extension from log list 2022-06-17 11:13:05 +02:00
243ec6aecf Fixed blender-download on other platform 2022-06-15 16:47:44 +02:00
b6515aadf6 Fixed design 2022-06-15 02:21:37 +02:00
e4b758bb11 Updated design 2022-06-15 02:16:03 +02:00
9d6daa069e Sharp render preview 2022-06-15 01:08:02 +02:00
62f414a9d3 Clean up assets 2022-06-15 01:03:08 +02:00
e3cfa63745 Added easier npm install process 2022-06-15 00:57:52 +02:00
4d84861b62 Added download script for blender 2022-06-15 00:55:18 +02:00
c8c06c42bc Removed unneeded variable 2022-06-15 00:54:09 +02:00
fe0778fe71 Updated README.md 2022-06-15 00:53:04 +02:00
fc14114e02 Fixed empty log setting 2022-06-14 21:11:16 +02:00
7c9ed18252 Added auto update 2022-06-13 22:38:35 +02:00
999b90e54d Fixed setting resetting not displayed 2022-06-13 21:47:01 +02:00
7c58e244e5 Improved Design, new render Side 2022-06-13 21:33:21 +02:00
96223ba81b Fixed empty log in menu 2022-06-13 20:30:47 +02:00
0aba2a0de0 Fixed rendering preview faild when logs/output empty 2022-06-13 20:27:28 +02:00
179d7f9d7b Fixed default output path not found 2022-06-13 19:45:10 +02:00
2b0bfb9429 Better log menu 2022-06-13 19:32:31 +02:00
0e76ddb42d Better protection against paths 2022-06-13 19:32:05 +02:00
9628d2029c Better default paths 2022-06-13 19:30:49 +02:00
4546532e68 Full path of logs on hover 2022-06-13 13:30:51 +02:00
3c57b44d8f Only log names are displayed 2022-06-13 13:26:34 +02:00
f0c48eac9a Fixed settings not repairing if file is broken 2022-06-13 01:56:47 +02:00
6f6c84dc46 added webpack dev server, slightly improved ui and render preview 2022-06-13 01:15:17 +02:00
7ea3b514ce Slightly better ui, added react and typescript 2022-06-06 14:22:05 +02:00
Lino Schmidt
3e1a54a49e Update README.md 2022-05-23 09:07:58 +02:00
e76f2d4b2a Updated README.md 2022-05-18 23:05:03 +02:00
a730c36c13 More build functions 2022-05-18 23:03:17 +02:00
9eb63142c8 Better instructions 2022-05-18 23:02:48 +02:00
30c4f6c81e Better and easier build software 2022-05-17 19:34:40 +02:00
41b6470f7a Merge branch 'main' of https://github.com/LinoSchmidt/StickExporterTX 2022-05-17 15:42:13 +02:00
ce9b2adb22 Removed unused script 2022-05-17 15:41:56 +02:00
LinoSchmidt
27db1432e7 Update README.md 2022-05-17 15:16:07 +02:00
b425eded3b Merge branch 'main' of https://github.com/LinoSchmidt/StickExporterTX 2022-05-16 23:45:21 +02:00
429e4a28a3 Fixed blenderScript.py permission Problem 2022-05-16 23:45:15 +02:00
bf131e1f13 Added Icons for other platforms 2022-05-16 23:44:18 +02:00
LinoSchmidt
195e153403 Added Licence to README.md 2022-05-16 22:29:44 +02:00
LinoSchmidt
87290b8437 Create LICENSE.md 2022-05-16 22:24:27 +02:00
58 changed files with 16896 additions and 7755 deletions

15
.babelrc Normal file
View File

@@ -0,0 +1,15 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
]
]
}

29
.eslintrc Normal file
View File

@@ -0,0 +1,29 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"react-hooks",
"import"
],
"extends": [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/prop-types": "off",
"global-require": "off",
"import/no-dynamic-require": "off"
},
"settings": {
"react": {
"pragma": "React",
"version": "detect"
}
}
}

7
.gitignore vendored
View File

@@ -263,6 +263,9 @@ out/
output/ output/
blender/ dependencies/windows/
dependencies/darwin/
dependencies/linux/
release-builds/ index.build.js
index.build.js.LICENSE.txt

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"python.analysis.extraPaths": [
"./dependencies/windows/blender/3.2/scripts/modules",
"./dependencies/linux/blender/3.2/scripts/modules"
]
}

128
2D.py
View File

@@ -1,128 +0,0 @@
import csv
from tkinter.filedialog import askopenfilename
import pygame
import time
Pixelsize = 10
Size = 500
Height = Size/2
Width = Size
HeightMiddle = Height/2
WidthMiddle = Width/2
joystickScaleHeight = Height/2048
joystickScaleWidth = Width/2048
screen = pygame.display.set_mode((Width,Height+1))
pygame.display.set_caption('Log Test')
pygame.draw.rect(screen, (0,0,255), (WidthMiddle, 0, 1, Height))
pygame.display.flip()
log = askopenfilename()
ltime = []
rud = []
ele = []
thr = []
ail = []
try:
with open(log, newline='') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
ltime.append(row['Time'])
rud.append(row['Rud'])
ele.append(row['Ele'])
thr.append(row['Thr'])
ail.append(row['Ail'])
except:
print("Can't read Log File!")
exit()
i = 0
while i != len(rud):
iRud = int(rud[i])
iEle = int(ele[i])
iThr = int(thr[i])
iAil = int(ail[i])
if iRud >= 0:
rud[i] = (iRud + 1024)*(joystickScaleWidth/2)
else:
rud[i] = (1024 - abs(iRud))*(joystickScaleWidth/2)
if iEle >= 0:
ele[i] = (iEle + 1024)*joystickScaleHeight
else:
ele[i] = (1024 - abs(iEle))*joystickScaleHeight
if iThr >= 0:
thr[i] = (iThr + 1024)*joystickScaleHeight
else:
thr[i] = (1024 - abs(iThr))*joystickScaleHeight
if iAil >= 0:
ail[i] = (iAil + 1024)*(joystickScaleWidth/2)
else:
ail[i] = (1024 - abs(iAil))*(joystickScaleWidth/2)
i+=1
splitTime = []
for e in ltime:
splitTime.append(e.split(":").pop(2).replace(".", ""))
meanTime = []
i = 0
f = False
for e in splitTime:
if f != False:
if int(e) < int(splitTime[i]):
meanTime.append(60000 - int(splitTime[i]) + int(e))
else:
meanTime.append(int(e) - int(splitTime[i]))
i+=1
else:
f = True
totalTime = 0
for e in meanTime:
totalTime+=e
timelineStep = Width/totalTime
while True:
i = 0
lastTime = 0
while i < len(meanTime):
if(i == 0):
pygame.draw.circle(screen, (255,0,0), (WidthMiddle+ail[0], Height-ele[0]), Pixelsize)
pygame.draw.circle(screen, (255,0,0), (rud[0], Height-thr[0]), Pixelsize)
pygame.display.flip()
startTime = int(time.time()*1000)
i+=1
else:
while(startTime+lastTime+meanTime[i-1] >= int(time.time()*1000)):
multiplier = (int(time.time()*1000)-(startTime+lastTime))/meanTime[i-1]
ailP = (WidthMiddle+ail[i-1])+((WidthMiddle+ail[i])-(WidthMiddle+ail[i-1]))*multiplier
eleP = (Height-ele[i-1])+((Height-ele[i])-(Height-ele[i-1]))*multiplier
rudP = (rud[i-1])+((rud[i])-(rud[i-1]))*multiplier
thrP = (Height-thr[i-1])+((Height-thr[i])-(Height-thr[i-1]))*multiplier
pygame.draw.rect(screen, (0,0,0), (0, 0, Width, Height+1))
pygame.draw.rect(screen, (0,0,255), (WidthMiddle, 0, 1, Height))
pygame.draw.circle(screen, (255,0,0), (ailP, eleP), Pixelsize)
pygame.draw.circle(screen, (255,0,0), (rudP, thrP), Pixelsize)
pygame.draw.rect(screen, (0,255,0), (0, Height, (int(time.time()*1000)-startTime)*timelineStep, 1))
pygame.display.flip()
i+=1
if i < len(meanTime):
lastTime += meanTime[i]
for e in pygame.event.get():
if e.type == pygame.QUIT:
pygame.quit()
exit()

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Lino Schmidt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,21 +1,23 @@
# StickExporterTX # StickExporterTX
A 3D Sticks Exporter for EdgeTX/OpenTX logs. StickExporterTX is a 3D Stick Exporter for EdgeTX/OpenTX logs.
Setup: ## Quick Start Guide
------
- Download [Blender (Portable)](https://www.blender.org/download) and unpack it with the name `blender` in the folder `<Project folder>/assets`.
- Start a console in the project folder and execute the command `npm install` to install the necessary packages.
Test: To log your sticks on each model, go to `settings -> global functions` in your RC controller.
------
To test the program, execute the command `npm start` in the project folder.
Build (Windows): ![global-functions](readme/pictures/global-functions.bmp)
------
To build the program, execute the command `npm run package-win` in the project folder.
After that, open the file `installer-builder.iss` with the program [Inno Setup Compiler](https://jrsoftware.org/isdl.php#stable) and compile it.
The finished installer should be inside `<Project Folder>/output`.
There you can select and edit one of the empty fields.
With `Switch` you select the one that should activate logging. For example, you can set your drone's arm switch to log it as long as the drone is armed.
`Func` must be set to `SD Logs` and `Value` should be as low as possible to get a smoother animation.
Copyright © 2022 Lino Schmidt. All rights reserved. ![function-edit](readme/pictures/function-edit.bmp)
If you only want to set up logging for one model, go to `model -> special functions` in your RC controller and repeat the previous step.
![special-functions](readme/pictures/special-functions.bmp)
## Licence:
This project is released under the MIT license, for more information, check the [LICENSE](LICENSE) file.

View File

@@ -1,165 +0,0 @@
import csv
from importlib.resources import path
import logging
import math
import sys
import bpy
import xml.etree.ElementTree as ET
settings = ET.parse("")
logger = logging.getLogger('simple_example')
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(message)s')
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
console_handler.setLevel(logging.INFO)
logger.addHandler(console_handler)
def _map(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
settingsRoot = settings.getroot()
FPS = int(settingsRoot[0].text)
Width = int(settingsRoot[1].text)
StickDistance = _map(int(settingsRoot[2].text), 0, 100, 5, 105)
StickMode = settingsRoot[3].text
if(StickMode == "true"):
StickMode = 2
else:
StickMode = 1
lyMax = 0.436
lyMin = -0.436
lxMax = -0.436
lxMin = 0.436
ryMax = -0.436
ryMin = 0.436
rxMax = 0.436
rxMin = -0.436
logs = settingsRoot[4].text[1:][:-1].split("\"\"")
logCount = len(logs)
logNumber = 1
for log in logs:
logger.info("Lognr:" + ((str)(logNumber)) + ":")
logTime = []
rud = []
ele = []
thr = []
ail = []
try:
with open(log, newline='') as csvFile:
reader = csv.DictReader(csvFile)
for row in reader:
logTime.append(row['Time'].split(":").pop(2).replace(".", ""))
rud.append(int(row['Rud']))
ele.append(int(row['Ele']))
thr.append(int(row['Thr']))
ail.append(int(row['Ail']))
meanTime = []
i = 0
while i < len(logTime)-1:
if int(logTime[i]) > int(logTime[i+1]):
meanTime.append(60000 - int(logTime[i]) + int(logTime[i+1]))
else:
meanTime.append(int(logTime[i+1]) - int(logTime[i]))
i+=1
totalTime = 0
for e in meanTime:
totalTime+=e
frameCount = math.floor(totalTime/1000*FPS-1)
FPSxxx = 1000/FPS
except Exception as e:
print("Can't read Log!")
exit()
GimbalL = bpy.data.objects["GimbalL"]
StickL = bpy.data.objects["StickL"]
GimbalR = bpy.data.objects["GimbalR"]
StickR = bpy.data.objects["StickR"]
GimbalCoverR = bpy.data.objects["GimbalCoverR"]
TrailR = bpy.data.objects["TrailR"]
Camera = bpy.data.objects["Camera"]
Plane = bpy.data.objects["Plane.001"]
scn = bpy.context.scene
scn.render.resolution_x = Width
GimbalCoverR.location[0] = StickDistance
GimbalR.location[0] = StickDistance
TrailR.location[0] = StickDistance
Plane.location[0] = StickDistance
Camera.location[0] = StickDistance/2
Camera.data.ortho_scale = StickDistance+5
scn.render.resolution_y = int(Width/_map(StickDistance, 5, 105, 2, 21.6))
bpy.context.scene.render.filepath = settingsRoot[5].text + "\\" + log.split("/")[-1].split("\\")[-1].replace(".csv", ".mov")
scn.render.fps = 1000
scn.render.fps_base = FPSxxx
scn.frame_start = 0
scn.frame_end = frameCount+1
logger.info("Frames:" + str(frameCount+1) + ":")
frame = 0
log = 0
pastTime = 0
while frame <= frameCount:
currentTime = math.floor(FPSxxx*frame)
while currentTime >= pastTime+meanTime[log]:
pastTime+=meanTime[log]
log+=1
multiplier = (currentTime-pastTime)/meanTime[log]
ailP = _map(ail[log]+(ail[log+1]-ail[log])*multiplier, -1024, 1024, rxMin, rxMax)
eleP = _map(ele[log]+(ele[log+1]-ele[log])*multiplier, -1024, 1024, ryMin, ryMax)
rudP = _map(rud[log]+(rud[log+1]-rud[log])*multiplier, -1024, 1024, lyMin, lyMax)
thrP = _map(thr[log]+(thr[log+1]-thr[log])*multiplier, -1024, 1024, lxMin, lxMax)
bpy.context.scene.frame_set(frame)
if StickMode == "1":
StickL.rotation_euler=[0,0,0]
StickL.rotation_euler.rotate_axis("Y", ailP)
StickL.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalL.rotation_euler=[0,0,0]
GimbalL.rotation_euler.rotate_axis("X", eleP)
GimbalL.keyframe_insert(data_path="rotation_euler", index=-1)
StickR.rotation_euler=[0,0,0]
StickR.rotation_euler.rotate_axis("Y", rudP)
StickR.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalR.rotation_euler=[0,0,0]
GimbalR.rotation_euler.rotate_axis("X", thrP)
GimbalR.keyframe_insert(data_path="rotation_euler", index=-1)
else:
StickL.rotation_euler=[0,0,0]
StickL.rotation_euler.rotate_axis("Y", rudP)
StickL.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalL.rotation_euler=[0,0,0]
GimbalL.rotation_euler.rotate_axis("X", thrP)
GimbalL.keyframe_insert(data_path="rotation_euler", index=-1)
StickR.rotation_euler=[0,0,0]
StickR.rotation_euler.rotate_axis("Y", ailP)
StickR.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalR.rotation_euler=[0,0,0]
GimbalR.rotation_euler.rotate_axis("X", eleP)
GimbalR.keyframe_insert(data_path="rotation_euler", index=-1)
logger.info("Init:" + ((str)(frame)) + ":")
frame+=1
bpy.ops.render.render(animation=True)
if(logCount <= logNumber):
logger.info("Finished")
logNumber+=1

BIN
assets/icon.icns Normal file

Binary file not shown.

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

BIN
assets/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
assets/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/icons/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/icons/96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,50 @@
import path from 'path';
import ESLintPlugin from "eslint-webpack-plugin";
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
export default {
module: {
rules: [
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
output: {
path: path.join(__dirname, '../src/.webpack'),
libraryTarget: 'commonjs2',
},
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'],
},
plugins: [
new ForkTsCheckerWebpackPlugin({
async: false
}),
new ESLintPlugin({
extensions: ["js", "jsx", "ts", "tsx"],
}),
],
node: {
__dirname: false,
__filename: false,
},
experiments: {
topLevelAwait: true,
},
}

View File

@@ -0,0 +1,11 @@
import baseConfig from "./webpack.base.config.js";
import {merge} from 'webpack-merge';
export default merge(baseConfig, {
mode: 'development',
target: 'electron-main',
entry: './src/index.ts',
output: {
filename: '../index.build.js'
},
});

View File

@@ -0,0 +1,11 @@
import baseConfig from "./webpack.base.config.js";
import {merge} from 'webpack-merge';
export default merge(baseConfig, {
mode: 'production',
target: 'electron-main',
entry: './src/index.ts',
output: {
filename: '../index.build.js'
},
});

View File

@@ -0,0 +1,31 @@
import baseConfig from "./webpack.base.config.js";
import {merge} from 'webpack-merge';
import { spawn } from 'child_process';
const host = process.env.HOST || 'localhost';
const port = process.env.PORT || 3000;
export default merge(baseConfig, {
mode: 'development',
target: 'electron-renderer',
entry: './src/renderer.tsx',
output: {
filename: 'renderer.js',
},
devServer: {
compress: true,
hot: true,
host,
port,
onBeforeSetupMiddleware() {
console.log('Starting Main Process...');
spawn('npm', ['run', 'start:main'], {
shell: true,
env: process.env,
stdio: 'inherit',
})
.on('close', (code) => process.exit(code))
.on('error', (spawnError) => console.error(spawnError));
},
},
});

View File

@@ -0,0 +1,11 @@
import baseConfig from "./webpack.base.config.js";
import {merge} from 'webpack-merge';
export default merge(baseConfig, {
mode: 'production',
target: 'electron-renderer',
entry: './src/renderer.tsx',
output: {
filename: 'renderer.js'
},
});

259
dependencies/blenderScript.py vendored Normal file
View File

@@ -0,0 +1,259 @@
from ast import Str
import csv
import logging
import math
import sys
import time
import bpy
import xml.etree.ElementTree as ET
argv = sys.argv
argv = argv[argv.index("--") + 1:]
logger = logging.getLogger('simple_example')
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(message)s')
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
console_handler.setLevel(logging.INFO)
logger.addHandler(console_handler)
GimbalL = bpy.data.objects["GimbalL"]
StickL = bpy.data.objects["StickL"]
GimbalR = bpy.data.objects["GimbalR"]
StickR = bpy.data.objects["StickR"]
GimbalCoverR = bpy.data.objects["GimbalCoverR"]
TrailR = bpy.data.objects["TrailR"]
Camera = bpy.data.objects["Camera"]
Plane = bpy.data.objects["Plane.001"]
scn = bpy.context.scene
lyMax = 0.436
lyMin = -0.436
lxMax = -0.436
lxMin = 0.436
ryMax = -0.436
ryMin = 0.436
rxMax = 0.436
rxMin = -0.436
def _map(x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
logger.info("Blender started successfully!")
while True:
command = input("Waiting for command: ")
time.sleep(0.5)
settingsRoot = ET.parse(argv[0]+"/settings.xml").getroot()
StickMode = settingsRoot[3].text
if(StickMode == "true"):
StickMode = 2
else:
StickMode = 1
width = int(settingsRoot[1].text)
StickDistance = _map(int(settingsRoot[2].text), 0, 100, 5, 105)
if(command == "startRendering"):
fps = int(settingsRoot[0].text)
videoFormat = settingsRoot[4].text
logs = settingsRoot[5].text[1:][:-1].split("\"\"")
output = settingsRoot[6].text
logCount = len(logs)
logNumber = 1
for log in logs:
logger.info("Lognr:" + ((str)(logNumber)) + ":")
logTime = []
rud = []
ele = []
thr = []
ail = []
try:
with open(log, newline='') as csvFile:
reader = csv.DictReader(csvFile)
for row in reader:
logTime.append(row['Time'].split(":").pop(2).replace(".", ""))
rud.append(int(row['Rud']))
ele.append(int(row['Ele']))
thr.append(int(row['Thr']))
ail.append(int(row['Ail']))
meanTime = []
i = 0
while i < len(logTime)-1:
if int(logTime[i]) > int(logTime[i+1]):
meanTime.append(60000 - int(logTime[i]) + int(logTime[i+1]))
else:
meanTime.append(int(logTime[i+1]) - int(logTime[i]))
i+=1
totalTime = 0
for e in meanTime:
totalTime+=e
frameCount = math.floor(totalTime/1000*fps-1)
FPSxxx = 1000/fps
except Exception as e:
logger.error("Can't read Log: " + (Str)(e))
bpy.context.scene.render.image_settings.file_format = 'FFMPEG'
if(videoFormat == "mp4"):
bpy.context.scene.render.ffmpeg.format = 'MPEG4'
bpy.context.scene.render.ffmpeg.codec = 'H264'
bpy.context.scene.render.image_settings.color_mode = 'RGB'
elif(videoFormat == "mov"):
bpy.context.scene.render.ffmpeg.format = 'QUICKTIME'
bpy.context.scene.render.ffmpeg.codec = 'QTRLE'
bpy.context.scene.render.image_settings.color_mode = 'RGBA'
elif(videoFormat == "avi"):
bpy.context.scene.render.ffmpeg.format = 'AVI'
bpy.context.scene.render.ffmpeg.codec = 'FFV1'
bpy.context.scene.render.image_settings.color_mode = 'RGBA'
elif(videoFormat == "webm"):
bpy.context.scene.render.ffmpeg.format = 'WEBM'
bpy.context.scene.render.ffmpeg.codec = 'WEBM'
bpy.context.scene.render.image_settings.color_mode = 'RGBA'
elif(videoFormat == "mkv"):
bpy.context.scene.render.ffmpeg.format = 'MKV'
bpy.context.scene.render.ffmpeg.codec = 'WEBM'
bpy.context.scene.render.image_settings.color_mode = 'RGBA'
scn.render.resolution_x = width
GimbalCoverR.location[0] = StickDistance
GimbalR.location[0] = StickDistance
TrailR.location[0] = StickDistance
Plane.location[0] = StickDistance
Camera.location[0] = StickDistance/2
Camera.data.ortho_scale = StickDistance+5
scn.render.resolution_y = int(width/_map(StickDistance, 5, 105, 2, 21.6))
bpy.context.scene.render.filepath = output + "\\" + log.split("/")[-1].split("\\")[-1].replace(".csv", "."+videoFormat)
scn.render.fps = 1000
scn.render.fps_base = FPSxxx
scn.frame_start = 0
scn.frame_end = frameCount
logger.info("Frames:" + str(frameCount) + ":")
frame = 0
log = 0
pastTime = 0
while frame <= frameCount:
currentTime = math.floor(FPSxxx*frame)
while currentTime >= pastTime+meanTime[log]:
pastTime+=meanTime[log]
log+=1
multiplier = (currentTime-pastTime)/meanTime[log]
ailP = _map(ail[log]+(ail[log+1]-ail[log])*multiplier, -1024, 1024, rxMin, rxMax)
eleP = _map(ele[log]+(ele[log+1]-ele[log])*multiplier, -1024, 1024, ryMin, ryMax)
rudP = _map(rud[log]+(rud[log+1]-rud[log])*multiplier, -1024, 1024, lyMin, lyMax)
thrP = _map(thr[log]+(thr[log+1]-thr[log])*multiplier, -1024, 1024, lxMin, lxMax)
bpy.context.scene.frame_set(frame)
StickL.rotation_euler=[0,0,0]
GimbalL.rotation_euler=[0,0,0]
StickR.rotation_euler=[0,0,0]
GimbalR.rotation_euler=[0,0,0]
if StickMode == "1":
StickL.rotation_euler.rotate_axis("Y", ailP)
GimbalL.rotation_euler.rotate_axis("X", eleP)
StickR.rotation_euler.rotate_axis("Y", rudP)
GimbalR.rotation_euler.rotate_axis("X", thrP)
else:
StickL.rotation_euler.rotate_axis("Y", rudP)
GimbalL.rotation_euler.rotate_axis("X", thrP)
StickR.rotation_euler.rotate_axis("Y", ailP)
GimbalR.rotation_euler.rotate_axis("X", eleP)
StickL.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalL.keyframe_insert(data_path="rotation_euler", index=-1)
StickR.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalR.keyframe_insert(data_path="rotation_euler", index=-1)
logger.info("Init:" + ((str)(frame)) + ":")
frame+=1
bpy.ops.render.render(animation=True)
if(logCount <= logNumber):
logger.info("Finished")
logNumber+=1
elif(command == "getRender"):
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.context.scene.render.filepath = argv[0] + "\\render.png"
scn.render.resolution_x = width
GimbalCoverR.location[0] = StickDistance
GimbalR.location[0] = StickDistance
TrailR.location[0] = StickDistance
Plane.location[0] = StickDistance
Camera.location[0] = StickDistance/2
Camera.data.ortho_scale = StickDistance+5
scn.render.resolution_y = int(width/_map(StickDistance, 5, 105, 2, 21.6))
bpy.context.scene.frame_set(0)
StickL.rotation_euler=[0,0,0]
GimbalL.rotation_euler=[0,0,0]
StickR.rotation_euler=[0,0,0]
GimbalR.rotation_euler=[0,0,0]
if(StickMode == 2):
StickL.rotation_euler.rotate_axis("Y", 0)
GimbalL.rotation_euler.rotate_axis("X", 0.436)
StickR.rotation_euler.rotate_axis("Y", 0)
GimbalR.rotation_euler.rotate_axis("X", 0)
else:
StickL.rotation_euler.rotate_axis("Y", 0)
GimbalL.rotation_euler.rotate_axis("X", 0)
StickR.rotation_euler.rotate_axis("Y", 0)
GimbalR.rotation_euler.rotate_axis("X", 0.436)
StickL.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalL.keyframe_insert(data_path="rotation_euler", index=-1)
StickR.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalR.keyframe_insert(data_path="rotation_euler", index=-1)
bpy.context.scene.frame_set(1)
StickL.rotation_euler=[0,0,0]
GimbalL.rotation_euler=[0,0,0]
StickR.rotation_euler=[0,0,0]
GimbalR.rotation_euler=[0,0,0]
if(StickMode == 2):
StickL.rotation_euler.rotate_axis("Y", 0)
GimbalL.rotation_euler.rotate_axis("X", 0.436)
StickR.rotation_euler.rotate_axis("Y", 0)
GimbalR.rotation_euler.rotate_axis("X", 0)
else:
StickL.rotation_euler.rotate_axis("Y", 0)
GimbalL.rotation_euler.rotate_axis("X", 0)
StickR.rotation_euler.rotate_axis("Y", 0)
GimbalR.rotation_euler.rotate_axis("X", 0.436)
StickL.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalL.keyframe_insert(data_path="rotation_euler", index=-1)
StickR.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalR.keyframe_insert(data_path="rotation_euler", index=-1)
bpy.context.scene.frame_set(0)
bpy.ops.render.render(write_still=True)

BIN
icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,49 +0,0 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "StickExporterTX"
#define MyAppVersion "0.6.1"
#define MyAppPublisher "Lino Schmidt"
#define MyAppURL "https://stickexportertx.lino3d.de"
#define MyAppExeName "stickexportertx.exe"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{11FB4A05-6479-45DE-8B5C-436F0E63B2D3}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
; Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
OutputDir=output
OutputBaseFilename=stickexportertx-setup
SetupIconFile=icon.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "release-builds\stickexportertx-win32-ia32\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "assets\*"; DestDir: "{app}\assets"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

21005
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +1,158 @@
{ {
"version": "0.8.1",
"name": "stickexportertx", "name": "stickexportertx",
"productName": "StickExporterTX", "productName": "StickExporterTX",
"version": "0.6.1", "description": "3D stick exporter for EdgeTX/OpenTX logs",
"description": "A 3D Stick Exporter for EdgeTX/OpenTX Logs.", "main": "src/index.build.js",
"main": "src/index.js",
"scripts": { "scripts": {
"start": "electron-forge start", "install:win32": "python ./scripts/download-blender.py",
"package": "electron-forge package", "install:linux": "python3 ./scripts/download-blender.py",
"make": "electron-forge make", "install:darwin": "python3 ./scripts/download-blender.py",
"publish": "electron-forge publish", "install": "run-script-os",
"lint": "echo \"No linting configured\"", "build:main": "cross-env NODE_ENV=production webpack --config configs/webpack.main.prod.config.babel.js",
"package-win": "electron-packager . stickexportertx --overwrite --asar=true --platform=win32 --arch=ia32 --icon=icon.ico --prune=true --out=release-builds --version-string.CompanyName=\"Lino Schmidt\" --version-string.FileDescription=\"3D Stick Exporter for EdgeTX/OpenTX Logs\" --version-string.ProductName=StickExporterTX" "build:renderer": "cross-env NODE_ENV=production webpack --config configs/webpack.renderer.prod.config.babel.js",
"build": "cross-env npm run build:main && cross-env npm run build:renderer",
"bundle": "cross-env npm run build && electron-builder build",
"start:renderer": "cross-env NODE_ENV=development webpack serve --config configs/webpack.renderer.dev.config.babel.js",
"start:main": "cross-env NODE_ENV=development webpack --config configs/webpack.main.dev.config.babel.js && cross-env NODE_ENV=development electron .",
"start": "cross-env npm run start:renderer"
}, },
"keywords": [], "keywords": [],
"author": { "author": {
"name": "Lino Schmidt", "name": "Lino Schmidt",
"email": "linoschmidt@lino3d.de" "email": "linoschmidt@lino3d.de",
"url": "https://link.lino3d.de"
}, },
"license": "MIT", "license": "MIT",
"config": { "build": {
"forge": { "appId": "de.lino3d.stickexportertx",
"packagerConfig": {}, "productName": "StickExporterTX",
"makers": [ "files": [
"src/.webpack/renderer.js",
"src/index.build.js",
"src/index.html"
],
"extraResources": [
"dependencies/template.blend",
"dependencies/blenderScript.py",
"assets/icon.png",
"assets/render_finished_icon.png"
],
"win": {
"icon": "assets/icon.png",
"target": [
"nsis"
],
"extraResources": [
{ {
"name": "@electron-forge/maker-squirrel", "from": "dependencies/windows/",
"config": { "to": "dependencies/windows/",
"name": "stickexportertx" "filter": [
} "**/*"
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
] ]
} }
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"license": "LICENSE"
},
"linux": {
"icon": "assets/icons",
"category": "graphics",
"target": [
"zip",
"deb",
"rpm"
],
"extraResources": [
{
"from": "dependencies/linux/",
"to": "dependencies/linux/",
"filter": [
"**/*"
]
}
]
},
"mac": {
"icon": "assets/icon.icns",
"category": "public.app-category.graphics-design",
"target": [
"dmg"
],
"extraResources": [
{
"from": "dependencies/darwin/",
"to": "dependencies/darwin/",
"filter": [
"**/*"
]
}
]
},
"publish": {
"provider": "github",
"owner": "LinoSchmidt",
"repo": "StickExporterTX"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/LinoSchmidt/StickExporterTX"
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.8", "@electron/remote": "^2.0.8",
"csv-parse": "^5.2.0",
"electron-log": "^4.4.7", "electron-log": "^4.4.7",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"line-replace": "^2.0.1", "electron-updater": "^5.0.1",
"is-valid-path": "^0.1.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"video.js": "^7.19.2",
"xml-formatter": "^2.6.1" "xml-formatter": "^2.6.1"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63", "@babel/core": "^7.18.2",
"@electron-forge/maker-deb": "^6.0.0-beta.63", "@babel/plugin-transform-runtime": "^7.18.2",
"@electron-forge/maker-rpm": "^6.0.0-beta.63", "@babel/preset-env": "^7.18.2",
"@electron-forge/maker-squirrel": "^6.0.0-beta.63", "@babel/preset-react": "^7.17.12",
"@electron-forge/maker-zip": "^6.0.0-beta.63", "@babel/preset-typescript": "^7.17.12",
"@babel/register": "^7.17.7",
"@babel/runtime": "^7.18.3",
"@types/is-valid-path": "^0.1.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5",
"@types/video.js": "^7.3.42",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.2",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"@vercel/webpack-asset-relocator-loader": "^1.7.2",
"babel-loader": "^8.2.5",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1",
"electron": "18.1.0", "electron": "18.1.0",
"electron-packager": "^15.5.1" "electron-builder": "^23.0.3",
"eslint": "^8.16.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.30.0",
"eslint-plugin-react-hooks": "^4.5.0",
"eslint-webpack-plugin": "^3.1.1",
"fork-ts-checker-webpack-plugin": "^7.2.11",
"node-loader": "^2.0.0",
"run-script-os": "^1.1.6",
"sass": "^1.52.1",
"sass-loader": "^13.0.0",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.0",
"typescript": "^4.7.2",
"webpack": "^5.72.1",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.1",
"webpack-merge": "^5.8.0"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

View File

@@ -0,0 +1,60 @@
from io import BytesIO
import platform
import urllib.request
from zipfile import ZipFile
import tarfile
import os
import shutil
windowsURL = 'https://ftp.halifax.rwth-aachen.de/blender/release/Blender3.2/blender-3.2.0-windows-x64.zip'
linuxURL = 'https://ftp.halifax.rwth-aachen.de/blender/release/Blender3.2/blender-3.2.0-linux-x64.tar.xz'
if(platform.system() == 'Windows'):
if(os.path.exists('./dependencies/windows')):
print("Removing old windows folder")
shutil.rmtree('./dependencies/windows')
print("Downloading windows version")
with urllib.request.urlopen(windowsURL) as zipresp:
with ZipFile(BytesIO(zipresp.read())) as zfile:
zfile.extractall('./dependencies/windows')
print("Adjust windows version")
oldWindowsName = windowsURL.split('/')[-1].replace('.zip', '')
os.rename('./dependencies/windows/' + oldWindowsName, './dependencies/windows/blender')
if(platform.system() == 'Linux'):
if(os.path.exists('./dependencies/linux')):
print("Removing old linux folder")
shutil.rmtree('./dependencies/linux')
print("Downloading linux version")
os.mkdir('./dependencies/linux')
urllib.request.urlretrieve(linuxURL, './dependencies/linux/blender.tar.xz')
print("Extracting linux version")
with tarfile.open('./dependencies/linux/blender.tar.xz') as tfile:
tfile.extractall('./dependencies/linux')
print("Adjust linux version")
oldLinuxName = linuxURL.split('/')[-1].replace('.tar.xz', '')
os.rename('./dependencies/linux/' + oldLinuxName, './dependencies/linux/blender')
print("Clean up linux folder")
os.remove('./dependencies/linux/blender.tar.xz')
if(platform.system() == 'Darwin'):
if(os.path.exists('./dependencies/darwin')):
print("Removing old darwin folder")
shutil.rmtree('./dependencies/darwin')
print("Darwin is not supported yet!")
# TODO: Add darwin support as following:
# - look for blender
# -- if blender not found, ask user to install blender or let the script do it
# --- if user wants the script to install blender, download the installer
# --- ask the user to follow the installer
# - try copy the blender files into the dependencies blender folder
# -- if it doesn't work, ask the user for admin rights and try to copy again
# remove the "Darwin is not supported yet!" comment

View File

@@ -0,0 +1,256 @@
import { blenderPath, blenderScriptPath, dataPath, templatePath, finsishedIconPath } from "./paths";
import {spawn} from "child_process";
import logger from "./logger";
import { setBlenderLoading, setBlenderStatus } from "./ui/menu";
import { setLogNumber, setPastTime, setRemainingTime, setRenderDisplayProgress, setStatus, setPastTimeNow, setRemainingTimeNow } from "./ui/renderingPage";
import {imageLoading, imageLoaded} from "./ui/settingsPage";
import { getLogList, getLogSize, settingList } from "./settings";
import isValid from "is-valid-path";
import { pageSetRendering, setProgress, openPage, Page } from "../renderer";
import { ipcRenderer } from "electron";
// import { getDoNotDisturb } from "electron-notification-state";
export const renderInfo = {
time: "0min 0sec",
startTime: 0,
endTime: 0
}
const blenderStartString = [
templatePath,
"--background",
"--python",
blenderScriptPath,
"--",
dataPath.replaceAll("\\", "/")
]
let blenderConsole = spawn(blenderPath, blenderStartString).on('error', function(err) {
logger.errorMSG("Could not start blender: " + err.toString());
});
let readyToAcceptCommand = false;
let renderingPicture = false;
let renderingVideo = false;
let waitingForRender = false;
let logPortionList:number[] = [];
let currentLogPortion = 0;
const estimatedRenderPortion = 0.97;
function setRenderProgress(log:number, init:boolean, frameCount:number, frame:number) {
let progress = 0;
if(init) {
progress = logPortionList[log-1] * (frame / frameCount * (1 - estimatedRenderPortion)) + currentLogPortion;
} else {
progress = logPortionList[log-1] * (frame / frameCount * estimatedRenderPortion + (1 - estimatedRenderPortion)) + currentLogPortion;
}
setProgress(progress);
setRenderDisplayProgress(parseFloat((progress*100).toFixed(2)));
const timeNow = new Date().getTime();
const timeDiff = timeNow - renderInfo.startTime;
let timeDiffSeconds = timeDiff / 1000;
let timeDiffMinutes = 0;
while(timeDiffSeconds > 60) {
timeDiffMinutes++;
timeDiffSeconds -= 60;
}
renderInfo.time = timeDiffMinutes + "m " + timeDiffSeconds.toFixed(0) + "s";
setPastTimeNow(renderInfo.time);
if(progress > 0) {
const timeRemaining = (timeDiff / progress) * (1 - progress);
timeDiffSeconds = timeRemaining / 1000;
timeDiffMinutes = 0;
while(timeDiffSeconds > 60) {
timeDiffMinutes++;
timeDiffSeconds -= 60;
}
setRemainingTimeNow(timeDiffMinutes + "m " + timeDiffSeconds.toFixed(0) + "s");
}
}
function startBlender() {
let frames = "0";
let lastFrame = "0";
let log = "0";
blenderConsole.stdout.on('data', function(data) {
const dataStr = data.toString();
logger.info("Blender: " + dataStr);
if (dataStr.includes("Blender started successfully")) {
renderingPicture = false;
renderingVideo = false;
setBlenderStatus("Started");
}
if (dataStr.includes("Blender quit")) {
if(renderingPicture) {
logger.errorMSG("Rendering preview Failed!");
} else if(renderingVideo) {
logger.errorMSG("Rendering video Failed!");
}
readyToAcceptCommand = false;
renderingPicture = false;
renderingVideo = false;
setBlenderStatus("Restarting");
setBlenderLoading(true);
setProgress();
restartBlender();
}
if(dataStr.includes("Frames:")) {
frames = dataStr.split(":")[1];
renderingVideo = true;
readyToAcceptCommand = false;
setRenderProgress(parseInt(log), true, 0, 0);
}
if(dataStr.includes("Fra:") && renderingVideo) {
lastFrame = dataStr.split(":")[1].split(" ")[0];
setStatus("Rendering Frame " + lastFrame + "/" + frames);
setRenderProgress(parseInt(log), false, parseInt(frames), parseInt(lastFrame));
}
if(dataStr.includes("Finished") && renderingVideo) {
pageSetRendering(false);
renderInfo.endTime = new Date().getTime();
if(lastFrame == frames) {
openPage(Page.RenderFinish);
ipcRenderer.send("renderFinished");
// TODO: only show notification if not in do not disturb mode, currently not working. Details: https://github.com/felixrieseberg/macos-notification-state/issues/30
new Notification("Render Finished", {
body: "Rendering finished successfully!",
icon: finsishedIconPath
}).onclick = function() {
ipcRenderer.send("openApp");
}
} else {
logger.errorMSG("Render Failed!");
}
}
if(dataStr.includes("Init:") && renderingVideo) {
setStatus("Initialize Frame " + dataStr.split(":")[1] + "/" + frames);
setRenderProgress(parseInt(log), true, parseInt(frames), parseInt(dataStr.split(":")[1]));
}
if(dataStr.includes("Lognr:") && renderingVideo) {
log = dataStr.split(":")[1];
if(log !== "1") {
currentLogPortion += logPortionList[parseInt(log)-2];
}
setLogNumber(log);
}
if(dataStr.includes("Waiting for command")) {
pageSetRendering(false);
setProgress();
if(renderingPicture) {
imageLoaded();
}
if(!waitingForRender) {
readyToAcceptCommand = true;
renderingPicture = false;
renderingVideo = false;
setBlenderStatus("Ready");
setBlenderLoading(false);
} else {
waitingForRender = false;
renderingPicture = true;
blenderConsole.stdin.write("getRender\n");
setBlenderStatus("Rendering");
setBlenderLoading(true);
imageLoading();
}
}
});
blenderConsole.stderr.on('data', function(data:string) {
logger.errorMSG("Blender: " + data);
});
}
function restartBlender() {
pageSetRendering(false);
blenderConsole.kill();
blenderConsole = spawn(blenderPath, blenderStartString);
startBlender();
}
enum blenderCmd {
getRender,
startRendering,
stopRendering,
}
function blender(command:blenderCmd) {
if(command === blenderCmd.getRender) {
if(readyToAcceptCommand) {
readyToAcceptCommand = false;
renderingPicture = true;
imageLoading();
setBlenderStatus("Rendering");
setBlenderLoading(true);
blenderConsole.stdin.write("getRender\n");
} else {
waitingForRender = true;
}
} else if(command === blenderCmd.startRendering) {
if(readyToAcceptCommand) {
if(settingList.log == "") {
logger.warningMSG("No log selected!");
} else if(!isValid(settingList.log)) {
logger.warningMSG("Output path is invalid!");
} else {
currentLogPortion = 0;
const logSizeList:number[] = [];
getLogList().forEach(function (value, index) {
logSizeList.push(getLogSize(index));
});
let fullLogSize = 0;
for(let i = 0; i < logSizeList.length; i++) {
fullLogSize += logSizeList[i];
}
logPortionList = [];
logSizeList.forEach(function (value) {
logPortionList.push(value / fullLogSize);
});
readyToAcceptCommand = false;
renderingVideo = true;
pageSetRendering(true);
setBlenderStatus("Rendering");
setBlenderLoading(true);
blenderConsole.stdin.write("startRendering\n");
renderInfo.startTime = new Date().getTime();
setPastTime("0min 0sec");
setRemainingTime("calculating...");
}
}
} else if(command === blenderCmd.stopRendering) {
readyToAcceptCommand = false;
renderingPicture = false;
renderingVideo = false;
restartBlender();
}
}
ipcRenderer.on("isRenderActiveClose", () => {
if(renderingVideo) {
ipcRenderer.send("renderActiveClose");
} else {
ipcRenderer.send("renderInactiveClose");
}
});
export {
blender,
blenderCmd,
startBlender,
renderingPicture
}

View File

@@ -0,0 +1,8 @@
function formatDate(year:number, month:number, day:number) {
const date = new Date(year, month, day);
return date.toLocaleDateString();
}
export {
formatDate
}

213
src/components/logReader.ts Normal file
View File

@@ -0,0 +1,213 @@
import logger from "./logger";
import {parse as csvParse} from "csv-parse";
import {settingList} from "./settings";
import {platformCharacter} from "./paths";
import {formatDate} from "./dateFormat";
async function openLogFile(filePath:string, rawData:boolean) {
const data = await fetch(filePath).then(function(response) {
return response.text();
}).then(function(data) {
return data;
}).catch(function(error) {
logger.error("Could not load log file: " + error.toString());
return "";
});
if(rawData) {
return data;
} else {
let logData:string[]|undefined = undefined;
csvParse(data, {}, (err, output:string[]) => {
if(err) {
logger.errorMSG(`Error parsing csv file: ${err}`);
logData = [];
} else {
logData = output;
}
});
while(typeof logData === "undefined") {
await new Promise(resolve => setTimeout(resolve, 100));
}
return logData;
}
}
async function getLogTime(filePath:string) {
return await openLogFile(filePath, false).then(function(data) {
if(data.length > 0) {
const time = data[1][1];
const date = data[1][0];
const endTime = data[data.length - 1][1];
const endDate = data[data.length - 1][0];
const dateList = date.split("-");
const endDateList = endDate.split("-");
const timeList = time.split(":");
timeList.push(timeList[2].split(".")[1]);
timeList[2] = timeList[2].split(".")[0];
const endTimeList = endTime.split(":");
endTimeList.push(endTimeList[2].split(".")[1]);
endTimeList[2] = endTimeList[2].split(".")[0];
const year = parseInt(dateList[0]);
const month = parseInt(dateList[1]);
const day = parseInt(dateList[2]);
const hour = parseInt(timeList[0]);
const minute = parseInt(timeList[1]);
const second = parseInt(timeList[2]);
const millisecond = parseInt(timeList[3]);
let pastYears = parseInt(endDateList[0]) - year;
let pastMonths = parseInt(endDateList[1]) - month;
let pastDays = parseInt(endDateList[2]) - day;
let pastHours = parseInt(endTimeList[0]) - hour;
let pastMinutes = parseInt(endTimeList[1]) - minute;
let pastSeconds = parseInt(endTimeList[2]) - second;
let pastMilliseconds = parseInt(endTimeList[3]) - millisecond;
if(pastMilliseconds < 0) {
pastMilliseconds += 1000;
pastSeconds--;
}
if(pastSeconds < 0) {
pastSeconds += 60;
pastMinutes--;
}
if(pastMinutes < 0) {
pastMinutes += 60;
pastHours--;
}
if(pastHours < 0) {
pastHours += 24;
pastDays--;
}
if(pastDays < 0) {
pastDays += 30;
pastMonths--;
}
if(pastMonths < 0) {
pastMonths += 12;
pastYears--;
}
const logTimeFormatted = formatDate(year, month, day) + " " + hour + ":" + minute + ":" + second;
let logLengthFormatted = "00:00:00";
if(pastYears > 0) {
logLengthFormatted = pastYears + "y " + pastMonths + "m " + pastDays + "d " + pastHours + "h " + pastMinutes + "m " + pastSeconds + "s";
} else if(pastMonths > 0) {
logLengthFormatted = pastMonths + "m " + pastDays + "d " + pastHours + "h " + pastMinutes + "m " + pastSeconds + "s";
} else if(pastDays > 0) {
logLengthFormatted = pastDays + "d " + pastHours + "h " + pastMinutes + "m " + pastSeconds + "s";
} else {
logLengthFormatted = pastHours + "h " + pastMinutes + "m " + pastSeconds + "s";
}
return {
start: {
year,
month,
day,
hour,
minute,
second,
millisecond,
formatted: logTimeFormatted
},
length: {
years: pastYears,
months: pastMonths,
days: pastDays,
hours: pastHours,
minutes: pastMinutes,
seconds: pastSeconds,
milliseconds: pastMilliseconds,
formatted: logLengthFormatted
}
}
} else {
return {
start: {
year: 0,
month: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
formatted: "Not Found"
},
length: {
years: 0,
months: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
formatted: "Not Found"
}
};
}
});
}
async function getAllLogs() {
const loadList = [];
if(settingList.log.length > 0) {
const logs = settingList.log.substring(1).slice(0, -1).split('""');
for(const log of logs) {
loadList.push({
name: log.split(platformCharacter())[log.split(platformCharacter()).length - 1].replace(".csv", ""),
path: log,
time: await getLogTime(log)
});
}
}
return loadList;
}
let logList = await getAllLogs();
async function reloadAllLogs() {
logList = await getAllLogs();
}
async function updateLogs() {
if(settingList.log.length > 0) {
const logs = settingList.log.substring(1).slice(0, -1).split('""');
for(const log of logs) {
if(!logList.some(x => x.path === log)) {
logList.push({
name: log.split(platformCharacter())[log.split(platformCharacter()).length - 1].replace(".csv", ""),
path: log,
time: await getLogTime(log)
});
}
}
for(const log of logList) {
if(!logs.some(x => x === log.path)) {
logList.splice(logList.indexOf(log), 1);
}
}
} else {
logList = [];
}
}
export {
reloadAllLogs,
logList,
updateLogs
};

51
src/components/logger.ts Normal file
View File

@@ -0,0 +1,51 @@
import electronLog from 'electron-log';
import {dataPath} from './paths';
import {dialog} from '@electron/remote';
import path from 'path';
import {exec} from "child_process";
electronLog.transports.console.format = "{h}:{i}:{s} {text}";
electronLog.transports.file.getFile();
electronLog.transports.file.resolvePath = () => path.join(dataPath, "logs", "main.log");
const logger = {
info: function (message:string) {
electronLog.info(message);
},
error: function (message:string) {
electronLog.error(message);
},
warning: function (message:string) {
electronLog.warn(message);
},
errorMSG: function (message:string) {
logger.error(message);
dialog.showMessageBox({
type: 'error',
noLink: true,
buttons: ['Open Log', 'OK'],
defaultId: 1,
title: 'Something went wrong!',
message: 'An error has occurred:',
detail: message
}).then(res => {
if(res.response === 0) {
exec('start "" "' + path.join(dataPath, "logs") + '"');
}
});
},
warningMSG: function (message:string) {
logger.warning(message);
dialog.showMessageBox({
type: 'warning',
buttons: ['OK'],
defaultId: 1,
title: 'Warning!',
message: message
});
}
}
export default logger;

View File

@@ -0,0 +1,19 @@
import {spawn} from 'child_process';
import logger from './logger';
import { Platform, platform } from './platform';
export default function openFolder(path:string) {
if (platform === Platform.Mac) {
spawn('open', [path]).on('error', (err) => {
logger.errorMSG(err.message);
});
} else if (platform === Platform.Windows) {
spawn('explorer', [path]).on('error', (err) => {
logger.errorMSG(err.message);
});
} else if (platform === Platform.Linux) {
spawn('xdg-open', [path]).on('error', (err) => {
logger.errorMSG(err.message);
});
}
}

23
src/components/paths.ts Normal file
View File

@@ -0,0 +1,23 @@
import path from 'path';
import {app} from '@electron/remote';
import { platformFolder, platform, Platform } from './platform';
export const dataPath = app.getPath('userData');
export const appPath = app.getAppPath().replace("app.asar", "");
export const SettingPath = path.join(dataPath, "settings.xml");
export const defaultOutputPath = path.join(app.getPath('videos'), "StickExporterTX");
export function platformCharacter() {
let platformCharacterTEMP = "/";
if (platform === Platform.Windows) {
platformCharacterTEMP = "\\";
}
return platformCharacterTEMP;
}
export const blenderPath = path.join(appPath, "dependencies", platformFolder, "blender", "blender");
export const templatePath = path.join(appPath, "dependencies", "template.blend");
export const blenderScriptPath = path.join(appPath, "dependencies", "blenderScript.py");
export const finsishedIconPath = path.join(appPath, "assets", "render_finished_icon.png");

View File

@@ -0,0 +1,15 @@
const enum Platform {
Windows = 'win32',
Mac = 'darwin',
Linux = 'linux'
}
const platform = process.platform;
const platformFolder = platform === Platform.Windows ? 'windows' : platform === Platform.Mac ? 'darwin' : 'linux';
export {
platform,
Platform,
platformFolder
}

152
src/components/settings.ts Normal file
View File

@@ -0,0 +1,152 @@
import formatXML from "xml-formatter";
import {SettingPath, defaultOutputPath} from './paths';
import fs from "fs";
import logger from "./logger";
function getXMLChild(doc:Document, child:string) {
return String(doc.getElementsByTagName(child)[0].childNodes[0].nodeValue);
}
enum VideoFormat {
mp4="mp4",
mov="mov",
webm="webm",
avi="avi",
mkv="mkv",
}
const defaultSettings = {
fps: 30,
width: 540,
stickDistance: 5,
stickMode2: true,
videoFormat: VideoFormat.webm,
log: '',
output: defaultOutputPath
}
let allSettingsFound = true;
function catchSetting(tryFunc:()=>string, catchFunc:()=>string) {
let val;
try {
val = tryFunc();
} catch(err) {
logger.info("Failed to get setting value. Using default value:" + String(err));
allSettingsFound = false;
val = catchFunc();
}
return val;
}
const settingList = await fetch(SettingPath).then(function(response) {
return response.text();
}).catch(function(err) {
logger.info(err);
return "fileLoadFailed";
}).then(function(data) {
if(data === "fileLoadFailed") {
allSettingsFound = false;
return defaultSettings;
}
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(data, 'text/xml');
return {
fps: parseInt(catchSetting(function() {return getXMLChild(xmlDoc, "fps");},function() {return defaultSettings.fps.toString();})),
width: parseInt(catchSetting(function() {return getXMLChild(xmlDoc, "width");},function() {return defaultSettings.width.toString();})),
stickDistance: parseInt(catchSetting(function() {return getXMLChild(xmlDoc, "stickDistance");},function() {return defaultSettings.stickDistance.toString();})),
stickMode2: catchSetting(function() {return getXMLChild(xmlDoc, "stickMode2");},function() {return defaultSettings.stickMode2.toString();}) === "true",
videoFormat: catchSetting(function() {return getXMLChild(xmlDoc, "videoFormat");},function() {return defaultSettings.videoFormat.toString();}) as VideoFormat as VideoFormat,
log: catchSetting(function() {return (getXMLChild(xmlDoc, "log") === "None")? "":getXMLChild(xmlDoc, "log");},function() {return defaultSettings.log;}),
output: catchSetting(function() {return getXMLChild(xmlDoc, "output");},function() {return defaultSettings.output;})
}
});
if(!allSettingsFound) {
updateSettings({});
}
function updateSettings(optiones:{fps?:number, width?:number, stickDistance?:number, stickMode2?:boolean, videoFormat?:VideoFormat, log?:string, output?:string}) {
if(optiones.fps === undefined) {
optiones.fps = settingList.fps;
} else {
settingList.fps = optiones.fps;
}
if(optiones.width === undefined) {
optiones.width = settingList.width;
} else {
settingList.width = optiones.width;
}
if(optiones.stickDistance === undefined) {
optiones.stickDistance = settingList.stickDistance;
} else {
settingList.stickDistance = optiones.stickDistance;
}
if(optiones.stickMode2 === undefined) {
optiones.stickMode2 = settingList.stickMode2;
} else {
settingList.stickMode2 = optiones.stickMode2;
}
if(optiones.videoFormat === undefined) {
optiones.videoFormat = settingList.videoFormat;
} else {
settingList.videoFormat = optiones.videoFormat;
}
if(optiones.log === undefined) {
optiones.log = settingList.log;
} else {
settingList.log = optiones.log;
}
if(optiones.output === undefined) {
optiones.output = settingList.output;
} else {
settingList.output = optiones.output;
}
const xmlStr = `
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<fps>${optiones.fps}</fps>
<width>${optiones.width}</width>
<stickDistance>${optiones.stickDistance}</stickDistance>
<stickMode2>${optiones.stickMode2}</stickMode2>
<videoFormat>${optiones.videoFormat}</videoFormat>
<log>${(optiones.log === "")?"None":optiones.log}</log>
<output>${optiones.output}</output>
</settings>
`;
fs.writeFile(SettingPath, formatXML(xmlStr, {collapseContent: true}), function(err) {
if(err) {
logger.errorMSG(String(err));
}
});
}
function settingListLoadDefault() {
updateSettings({
fps:defaultSettings.fps,
width:defaultSettings.width,
stickDistance:defaultSettings.stickDistance,
stickMode2:defaultSettings.stickMode2,
videoFormat:defaultSettings.videoFormat,
});
}
function getLogList() {
return settingList.log.split("\"\"");
}
function getLogSize(index:number) {
const logList = settingList.log.substring(1).slice(0, -1).split('""');
return fs.statSync(logList[index]).size;
}
export {
updateSettings,
settingListLoadDefault,
settingList,
getLogList,
getLogSize,
VideoFormat
}

View File

@@ -0,0 +1,126 @@
import React, {useEffect, useState} from "react";
import { dialog } from "@electron/remote";
import { settingList, updateSettings } from "../settings";
import logger from "../logger";
import {blender, blenderCmd} from "../blenderController";
import openFolder from "../openFolder";
import {platformCharacter} from "../paths";
import {logList, reloadAllLogs, updateLogs} from "../logReader";
function MainPage() {
const [output, setOutput] = useState(settingList.output);
const [logTable, setLogTable] = useState([<tr key={0}></tr>]);
useEffect(() => {
updateLogTable(setLogTable);
}, []);
return (
<div id="content">
<button id="start-render" onClick={() => blender(blenderCmd.startRendering)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
{/* <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
<path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z"/>
</svg>
</button>
<h4 className="noMarginBottom">Logs:</h4>
<table>
<tbody>
{logTable}
</tbody>
</table>
<div className="dataDiv">
<button id="openLogButton" onClick={() => addLog(setLogTable)}>Add Log(s)</button>
<button id="deleteLogsButton" onClick={async () => {
updateSettings({log:""});
await reloadAllLogs();
updateLogTable(setLogTable);
}}>Delete All</button>
</div>
<div className="dataDiv" id="outputDiv">
<h4>Output Folder:</h4>
<p id="output" onClick={() => openFolder(settingList.output)}>{output}</p>
<button onClick={() => openVid(setOutput)}>Select Folder</button>
</div>
</div>
)
}
function updateLogTable(setLogTable:React.Dispatch<React.SetStateAction<JSX.Element[]>>) {
function getData() {
setLogTable(logList.map((log, index) => {
return <tr key={index}>
<td style={{
fontWeight: "bold",
fontStyle: "italic"
}}>{index+1}.</td>
<td id="logList-Name" title={log.path+"\n"+log.time.start.formatted+"\n"+log.time.length.formatted} onClick={() => openFolder(log.path.substring(0, log.path.lastIndexOf(platformCharacter())))}>{log.name}</td>
<td style={{
fontStyle: "italic",
fontWeight: "lighter"
}}>({log.time.length.formatted})</td>
<td><button className="listButton" onClick={async () => {
const newLogs = settingList.log.replace('"'+log.path+'"', "");
updateSettings({log:newLogs});
await updateLogs();
updateLogTable(setLogTable);
}}>Delete</button></td>
</tr>
}));
}
if(settingList.log == "") {
setLogTable([]);
} else {
getData();
}
}
function addLog(setLogTable:React.Dispatch<React.SetStateAction<JSX.Element[]>>) {
dialog.showOpenDialog({
properties: [
"multiSelections"
],
filters: [
{
name: "TX-Logs",
extensions: [
"csv"
]
}
]
}).then(async result => {
let logStr = "";
result.filePaths.forEach(value => {
const logToAdd = "\"" + value + "\"";
if(settingList.log.includes(logToAdd)) {
logger.warningMSG("Log " + logToAdd + " already added.");
} else {
logStr += "\"" + String(value) + "\"";
}
});
const newLogs = settingList.log + logStr;
updateSettings({log:newLogs});
await updateLogs();
updateLogTable(setLogTable);
}).catch(err => {
logger.errorMSG(err);
});
}
function openVid(updateHook:React.Dispatch<React.SetStateAction<string>>) {
dialog.showOpenDialog({
properties: [
"openDirectory"
]
}).then(result => {
if(result.filePaths.length > 0) {
updateSettings({output:String(result.filePaths)});
updateHook(String(result.filePaths));
}
}).catch(err => {
logger.errorMSG(err);
});
}
export default MainPage;

133
src/components/ui/menu.tsx Normal file
View File

@@ -0,0 +1,133 @@
import React, {useState} from "react";
import { openPage, Page } from "../../renderer";
import { platform, Platform } from "../platform";
import { ipcRenderer } from "electron";
const WinButtons = () => (
<>
<div className="winMenuButtons" id="winMinimize" onClick={() => ipcRenderer.send('minimize')}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
{/* <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
<path d="M400 288h-352c-17.69 0-32-14.32-32-32.01s14.31-31.99 32-31.99h352c17.69 0 32 14.3 32 31.99S417.7 288 400 288z"/>
</svg>
</div>
<div className="winMenuButtons" id="winClose" onClick={() => ipcRenderer.send('closeApp')}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
{/* <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
<path d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/>
</svg>
</div>
</>
)
const MainPageButtonsWin = () => (
<div id="settings-button-win" onClick={() => openPage(Page.Settings)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
{/* Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
<path
d="M495.9 166.6C499.2 175.2 496.4 184.9 489.6 191.2L446.3 230.6C447.4 238.9 448 247.4 448 256C448 264.6 447.4 273.1 446.3 281.4L489.6 320.8C496.4 327.1 499.2 336.8 495.9 345.4C491.5 357.3 486.2 368.8 480.2 379.7L475.5 387.8C468.9 398.8 461.5 409.2 453.4 419.1C447.4 426.2 437.7 428.7 428.9 425.9L373.2 408.1C359.8 418.4 344.1 427 329.2 433.6L316.7 490.7C314.7 499.7 307.7 506.1 298.5 508.5C284.7 510.8 270.5 512 255.1 512C241.5 512 227.3 510.8 213.5 508.5C204.3 506.1 197.3 499.7 195.3 490.7L182.8 433.6C167 427 152.2 418.4 138.8 408.1L83.14 425.9C74.3 428.7 64.55 426.2 58.63 419.1C50.52 409.2 43.12 398.8 36.52 387.8L31.84 379.7C25.77 368.8 20.49 357.3 16.06 345.4C12.82 336.8 15.55 327.1 22.41 320.8L65.67 281.4C64.57 273.1 64 264.6 64 256C64 247.4 64.57 238.9 65.67 230.6L22.41 191.2C15.55 184.9 12.82 175.3 16.06 166.6C20.49 154.7 25.78 143.2 31.84 132.3L36.51 124.2C43.12 113.2 50.52 102.8 58.63 92.95C64.55 85.8 74.3 83.32 83.14 86.14L138.8 103.9C152.2 93.56 167 84.96 182.8 78.43L195.3 21.33C197.3 12.25 204.3 5.04 213.5 3.51C227.3 1.201 241.5 0 256 0C270.5 0 284.7 1.201 298.5 3.51C307.7 5.04 314.7 12.25 316.7 21.33L329.2 78.43C344.1 84.96 359.8 93.56 373.2 103.9L428.9 86.14C437.7 83.32 447.4 85.8 453.4 92.95C461.5 102.8 468.9 113.2 475.5 124.2L480.2 132.3C486.2 143.2 491.5 154.7 495.9 166.6V166.6zM256 336C300.2 336 336 300.2 336 255.1C336 211.8 300.2 175.1 256 175.1C211.8 175.1 176 211.8 176 255.1C176 300.2 211.8 336 256 336z" />
</svg>
</div>
)
const MainPageButtonsLinux = () => (
<div id="settings-button-linux" onClick={() => openPage(Page.Settings)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
{/* Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
<path
d="M495.9 166.6C499.2 175.2 496.4 184.9 489.6 191.2L446.3 230.6C447.4 238.9 448 247.4 448 256C448 264.6 447.4 273.1 446.3 281.4L489.6 320.8C496.4 327.1 499.2 336.8 495.9 345.4C491.5 357.3 486.2 368.8 480.2 379.7L475.5 387.8C468.9 398.8 461.5 409.2 453.4 419.1C447.4 426.2 437.7 428.7 428.9 425.9L373.2 408.1C359.8 418.4 344.1 427 329.2 433.6L316.7 490.7C314.7 499.7 307.7 506.1 298.5 508.5C284.7 510.8 270.5 512 255.1 512C241.5 512 227.3 510.8 213.5 508.5C204.3 506.1 197.3 499.7 195.3 490.7L182.8 433.6C167 427 152.2 418.4 138.8 408.1L83.14 425.9C74.3 428.7 64.55 426.2 58.63 419.1C50.52 409.2 43.12 398.8 36.52 387.8L31.84 379.7C25.77 368.8 20.49 357.3 16.06 345.4C12.82 336.8 15.55 327.1 22.41 320.8L65.67 281.4C64.57 273.1 64 264.6 64 256C64 247.4 64.57 238.9 65.67 230.6L22.41 191.2C15.55 184.9 12.82 175.3 16.06 166.6C20.49 154.7 25.78 143.2 31.84 132.3L36.51 124.2C43.12 113.2 50.52 102.8 58.63 92.95C64.55 85.8 74.3 83.32 83.14 86.14L138.8 103.9C152.2 93.56 167 84.96 182.8 78.43L195.3 21.33C197.3 12.25 204.3 5.04 213.5 3.51C227.3 1.201 241.5 0 256 0C270.5 0 284.7 1.201 298.5 3.51C307.7 5.04 314.7 12.25 316.7 21.33L329.2 78.43C344.1 84.96 359.8 93.56 373.2 103.9L428.9 86.14C437.7 83.32 447.4 85.8 453.4 92.95C461.5 102.8 468.9 113.2 475.5 124.2L480.2 132.3C486.2 143.2 491.5 154.7 495.9 166.6V166.6zM256 336C300.2 336 336 300.2 336 255.1C336 211.8 300.2 175.1 256 175.1C211.8 175.1 176 211.8 176 255.1C176 300.2 211.8 336 256 336z" />
</svg>
</div>
)
const SettingsPageButtonsWin = () => (
<div id="backButton-win" onClick={() => openPage(Page.Main)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
{/* <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
<path d="M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM310.6 345.4c12.5 12.5 12.5 32.75 0 45.25s-32.75 12.5-45.25 0l-112-112C147.1 272.4 144 264.2 144 256s3.125-16.38 9.375-22.62l112-112c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L221.3 256L310.6 345.4z"/>
</svg>
</div>
)
const SettingsPageButtonsLinux = () => (
<button className="back-button-linux" onClick={() => openPage(Page.Main)}>Back</button>
)
const BlenderLoadingSVG = () => (
<svg id="blender-loading-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
{/* Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
<path d="M304 48C304 74.51 282.5 96 256 96C229.5 96 208 74.51 208 48C208 21.49 229.5 0 256 0C282.5 0 304 21.49 304 48zM304 464C304 490.5 282.5 512 256 512C229.5 512 208 490.5 208 464C208 437.5 229.5 416 256 416C282.5 416 304 437.5 304 464zM0 256C0 229.5 21.49 208 48 208C74.51 208 96 229.5 96 256C96 282.5 74.51 304 48 304C21.49 304 0 282.5 0 256zM512 256C512 282.5 490.5 304 464 304C437.5 304 416 282.5 416 256C416 229.5 437.5 208 464 208C490.5 208 512 229.5 512 256zM74.98 437C56.23 418.3 56.23 387.9 74.98 369.1C93.73 350.4 124.1 350.4 142.9 369.1C161.6 387.9 161.6 418.3 142.9 437C124.1 455.8 93.73 455.8 74.98 437V437zM142.9 142.9C124.1 161.6 93.73 161.6 74.98 142.9C56.24 124.1 56.24 93.73 74.98 74.98C93.73 56.23 124.1 56.23 142.9 74.98C161.6 93.73 161.6 124.1 142.9 142.9zM369.1 369.1C387.9 350.4 418.3 350.4 437 369.1C455.8 387.9 455.8 418.3 437 437C418.3 455.8 387.9 455.8 369.1 437C350.4 418.3 350.4 387.9 369.1 369.1V369.1z"/>
</svg>
)
const BlenderReadySVG = () => (
<svg id="blender-ready-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
{/* Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
<path d="M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"/>
</svg>
)
function WindowsMenu({page, blenderLoading, blenderStatus}:{page:Page, blenderLoading:boolean, blenderStatus:string}) {
return (
<header id="winHeader">
<h4>
{(page == Page.Main)? "StickExporterTX" : null}
{(page == Page.Settings)? "Settings" : null}
{(page == Page.Rendering)? "Rendering" : null}
{(page == Page.RenderFinish)? "Render Finished" : null}
</h4>
<div id="blender-info-win">
<div id="blender-icon-win">
{blenderLoading? <BlenderLoadingSVG/> : <BlenderReadySVG/>}
</div>
<p>{blenderStatus}</p>
</div>
{(page == Page.Main)? <MainPageButtonsWin/> : null}
{(page == Page.Settings)? <SettingsPageButtonsWin/> : null}
<WinButtons/>
</header>
)
}
function LinuxMenu({page, blenderLoading, blenderStatus}:{page:Page, blenderLoading:boolean, blenderStatus:string}) {
return (
<header id="linuxHeader">
<h1>
{(page == Page.Main)? "StickExporterTX" : null}
{(page == Page.Settings)? "Settings" : null}
{(page == Page.Rendering)? "Rendering" : null}
{(page == Page.RenderFinish)? "Render Finished" : null}
</h1>
<div id="blender-info-linux">
<div id="blender-icon-linux">
{blenderLoading? <BlenderLoadingSVG/> : <BlenderReadySVG/>}
</div>
<p>{blenderStatus}</p>
</div>
{(page == Page.Main)? <MainPageButtonsLinux/> : null}
{(page == Page.Settings)? <SettingsPageButtonsLinux/> : null}
</header>
)
}
let setBlenderLoading:React.Dispatch<React.SetStateAction<boolean>>;
let setBlenderStatus:React.Dispatch<React.SetStateAction<string>>;
function Menu({page}:{page:Page}) {
const [blenderLoading, setBlenderLoadingInner] = useState(true);
setBlenderLoading = setBlenderLoadingInner;
const [blenderStatus, setBlenderStatusInner] = useState("Starting");
setBlenderStatus = setBlenderStatusInner;
return (
<>
{(platform === Platform.Windows)? <WindowsMenu page={page} blenderLoading={blenderLoading} blenderStatus={blenderStatus}/> : null}
{(platform === Platform.Linux)? <LinuxMenu page={page} blenderLoading={blenderLoading} blenderStatus={blenderStatus}/> : null}
</>
)
}
export default Menu;
export {
setBlenderLoading,
setBlenderStatus
}

View File

@@ -0,0 +1,94 @@
import React, { CSSProperties } from "react";
import {openPage, Page} from "../../renderer";
import {renderInfo} from "../blenderController";
import openFolder from "../openFolder";
import {settingList} from "../settings";
import VideoPlayer from "./videoPlayer";
import path from 'path';
import {platformCharacter} from "../paths";
import {VideoJsPlayerOptions} from "video.js";
import {logList} from "../logReader";
const detailsStyle:CSSProperties = {
display: "flex",
marginBottom: "10px",
}
const detailsInnerStyle:CSSProperties = {
margin: "0px",
marginRight: "5px",
}
function RenderFinishPage() {
const [logPlaying, setLogPlaying] = React.useState(path.join(settingList.output, settingList.log.substring(1).slice(0, -1).split('""')[0].split(platformCharacter())[settingList.log.substring(1).slice(0, -1).split('""')[0].split(platformCharacter()).length - 1].replace(".csv", "."+settingList.videoFormat)));
const [outputList, setOutputList] = React.useState([<li key={0}></li>]);
const videoPlayerOptions:VideoJsPlayerOptions = {
controls: true,
muted: true,
fluid: true,
controlBar: {
volumePanel: false,
}
};
const [videoSource, setVideoSource] = React.useState({src: logPlaying, type: 'video/'+settingList.videoFormat.toUpperCase()});
React.useEffect(() => {
setOutputList(logList.map((inputLog, index) => {
const outputLogPath = path.join(settingList.output, inputLog.name+"."+settingList.videoFormat);
return <li key={index}>
<p style={{
textDecoration: logPlaying === outputLogPath ? "underline" : "none",
cursor: logPlaying === outputLogPath ? "default" : "pointer",
}} onClick={() => {
setLogPlaying(outputLogPath);
}} title={outputLogPath}>{inputLog.name}</p>
</li>
}));
setVideoSource({
src: logPlaying,
type: 'video/'+settingList.videoFormat.toUpperCase()
});
}, [logPlaying]);
return (
<div id="content">
<h3 style={{
color: "#00c24a",
}}>Render finished Successfully!</h3>
<div style={detailsStyle}>
<h4 style={detailsInnerStyle}>Duration:</h4>
<p style={detailsInnerStyle}>{renderInfo.time}</p>
</div>
<div style={detailsStyle}>
<h4 style={detailsInnerStyle}>Start Time:</h4>
<p style={detailsInnerStyle}>{new Date(renderInfo.startTime).toLocaleString().replace(",", "")}</p>
</div>
<div style={detailsStyle}>
<h4 style={detailsInnerStyle}>End Time:</h4>
<p style={detailsInnerStyle}>{new Date(renderInfo.endTime).toLocaleString().replace(",", "")}</p>
</div>
<button style={{
marginTop: "10px",
marginRight: "10px",
backgroundColor: "#00c24a",
}} onClick={() => {
openPage(Page.Main);
}}>Finish</button>
<button onClick={() => openFolder(settingList.output)}>Open Output Folder</button>
<div style={{
marginTop: "10px"
}}>
<VideoPlayer options={videoPlayerOptions} src={videoSource} />
<ol>
{outputList}
</ol>
</div>
</div>
);
}
export default RenderFinishPage;

View File

@@ -0,0 +1,94 @@
import React, {useEffect, useState} from "react";
import { settingList, getLogList } from "../settings";
import openFolder from "../openFolder";
import { blenderCmd, blender } from "../blenderController";
let setLogNumber:React.Dispatch<React.SetStateAction<string>>;
let setStatus:React.Dispatch<React.SetStateAction<string>>;
let setRenderDisplayProgress:React.Dispatch<React.SetStateAction<number>>;
let setPastTime:React.Dispatch<React.SetStateAction<string>>;
let setRemainingTime:React.Dispatch<React.SetStateAction<string>>;
let pastTimeNow = "0m 0s";
let remainingTimeNow = "calculating...";
function RenderingPage() {
const [logNumber, setLogNumberInner] = useState("0");
setLogNumber = setLogNumberInner;
const [status, setStatusInner] = useState("Idle");
setStatus = setStatusInner;
const [renderDisplayProgress, setRenderDisplayProgressInner] = useState(0);
setRenderDisplayProgress = setRenderDisplayProgressInner;
const [pastTime, setPastTimeInner] = useState("0m 0s");
setPastTime = setPastTimeInner;
const [remainingTime, setRemainingTimeInner] = useState("calculating...");
setRemainingTime = setRemainingTimeInner;
useEffect(() => {
const interval = setInterval(() => {
setPastTime(pastTimeNow);
setRemainingTime(remainingTimeNow);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div id="content">
<p>{"Log " + logNumber + "/" + getLogList().length}</p>
<p>{status}</p>
<div className="progress">
<div className="progress-done" style={{
width: renderDisplayProgress + "%"
}}></div>
<label>{renderDisplayProgress}%</label>
</div>
<div style={
{
display: "flex",
justifyContent: "flex-end",
marginTop: "5px",
marginBottom: "10px"
}
}>
<h4 style={{margin: "0"}}>Past Time:</h4>
<p style={
{
margin: "0",
marginRight: "auto",
marginLeft: "10px",
display: "flex",
width: "auto"
}
}>{pastTime}</p>
<h4 style={{margin: "0"}}>Remaining Time:</h4>
<p style={
{
margin: "0",
marginLeft: "10px"
}
}>{remainingTime}</p>
</div>
<button id="stopRenderButton" onClick={() => blender(blenderCmd.stopRendering)}>Stop</button>
<button onClick={() => openFolder(settingList.output)}>Open Output Folder</button>
</div>
)
}
function setPastTimeNow(time:string) {
pastTimeNow = time;
}
function setRemainingTimeNow(time:string) {
remainingTimeNow = time;
}
export default RenderingPage;
export {
setLogNumber,
setStatus,
setRenderDisplayProgress,
setPastTime,
setRemainingTime,
setPastTimeNow,
setRemainingTimeNow
};

View File

@@ -0,0 +1,174 @@
import React, {useState, useEffect, CSSProperties} from "react";
import { settingList, updateSettings, settingListLoadDefault, VideoFormat } from "../settings";
import {blender, blenderCmd, renderingPicture} from "../blenderController";
import {dataPath} from "../paths";
import path from "path";
let setRenderImg:React.Dispatch<React.SetStateAction<string>>;
let setRenderLoading:React.Dispatch<React.SetStateAction<boolean>>;
let pageLoaded = false;
function picturePath() {
return path.join(dataPath, "render.png?t="+Date.now());
}
const RenderLoadingSpinner = () => (
<div id="renderLoadingDiv">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
{/* Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
<path d="M304 48C304 74.51 282.5 96 256 96C229.5 96 208 74.51 208 48C208 21.49 229.5 0 256 0C282.5 0 304 21.49 304 48zM304 464C304 490.5 282.5 512 256 512C229.5 512 208 490.5 208 464C208 437.5 229.5 416 256 416C282.5 416 304 437.5 304 464zM0 256C0 229.5 21.49 208 48 208C74.51 208 96 229.5 96 256C96 282.5 74.51 304 48 304C21.49 304 0 282.5 0 256zM512 256C512 282.5 490.5 304 464 304C437.5 304 416 282.5 416 256C416 229.5 437.5 208 464 208C490.5 208 512 229.5 512 256zM74.98 437C56.23 418.3 56.23 387.9 74.98 369.1C93.73 350.4 124.1 350.4 142.9 369.1C161.6 387.9 161.6 418.3 142.9 437C124.1 455.8 93.73 455.8 74.98 437V437zM142.9 142.9C124.1 161.6 93.73 161.6 74.98 142.9C56.24 124.1 56.24 93.73 74.98 74.98C93.73 56.23 124.1 56.23 142.9 74.98C161.6 93.73 161.6 124.1 142.9 142.9zM369.1 369.1C387.9 350.4 418.3 350.4 437 369.1C455.8 387.9 455.8 418.3 437 437C418.3 455.8 387.9 455.8 369.1 437C350.4 418.3 350.4 387.9 369.1 369.1V369.1z"/>
</svg>
</div>
);
function VideoFormatWarning({videoFormat}:{videoFormat:VideoFormat}) {
let message = "";
switch(videoFormat) {
case VideoFormat.mp4:
message = "mp4 has no support for alpha channel and the background will be black.";
break;
case VideoFormat.avi:
message = "avi can't be played in the preview of this app.";
break;
case VideoFormat.mkv:
message = "mkv can't be played in the preview of this app.";
break;
case VideoFormat.mov:
message = "mov can't be played in the preview of this app.";
break;
}
const style:CSSProperties = {
height: "30px",
width: "30px",
fill: "orange",
paddingLeft: "5px",
};
return (
<div id="videoFormatWarning" title={message} style={style}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
{/* <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
<path d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/>
</svg>
</div>
);
}
function SettingsPage() {
const [fps, setFps] = useState(settingList.fps);
const [width, setWidth] = useState(settingList.width);
const [stickDistance, setStickDistance] = useState(settingList.stickDistance);
const [stickMode2, setStickMode2] = useState(settingList.stickMode2);
const [videoFormat, setVideoFormat] = useState(settingList.videoFormat);
const [renderImg, setRenderImgInner] = useState(picturePath());
setRenderImg = setRenderImgInner;
const [renderLoading, setRenderLoadingInner] = useState(renderingPicture);
setRenderLoading = setRenderLoadingInner;
useEffect(() => {
const timer = setTimeout(() => {
updateSettings({width, stickDistance, stickMode2});
blender(blenderCmd.getRender);
}, 500);
return () => clearTimeout(timer);
}, [width, stickDistance, stickMode2]);
useEffect(() => {
const timer = setTimeout(() => {
updateSettings({fps, videoFormat});
}, 500);
return () => clearTimeout(timer);
}, [fps, videoFormat]);
pageLoaded = true;
const VideoFormatOptions = Object.keys(VideoFormat).filter((el) => { return isNaN(Number(el)) }).map(key => {
return <option key={key} value={key}>{key}</option>;
});
return (
<div id="content">
<div id="settingRow">
<span className="inputSpan">
<label>FPS</label>
<input id="fpsInput" type="number" value={fps.toString()} min="1" step="1" onChange={e => {
if(e.target.value.trim().length !== 0) setFps(parseInt(e.target.value));
}}/>
</span>
<span className="inputSpan">
<label>Width</label>
<input id="widthInput" type="number" value={width.toString()} min="1" step="1" onChange={e => {
if(e.target.value.trim().length !== 0) setWidth(parseInt(e.target.value));
}}/>
</span>
<span className="inputSpan">
<label>Stick Distance</label>
<input id="stickDistanceInput" type="number" value={stickDistance.toString()} min="0" step="1" onChange={e => {
if(e.target.value.trim().length !== 0) setStickDistance(parseInt(e.target.value));
}}/>
</span>
</div>
<div id="settingRow">
<div className="dataDiv">
<p>Stick Mode</p>
<label htmlFor="stickMode" className="toggle-switchy" data-style="rounded" data-text="12">
<input checked={stickMode2} type="checkbox" id="stickMode" onChange={e => {
setStickMode2(e.target.checked);
}}/>
<span className="toggle">
<span className="switch"></span>
</span>
</label>
</div>
<span className="selectSpan">
<label className="selectSpanLabel">Format</label>
<label className="selectSpanSelect" htmlFor="slct">
<select id="slct" required={true} value={videoFormat} onChange={e => {
setVideoFormat(e.target.value as unknown as VideoFormat);
}}>
{VideoFormatOptions}
</select>
</label>
{videoFormat === VideoFormat.mov? <VideoFormatWarning videoFormat={videoFormat}/> : null}
{videoFormat === VideoFormat.mp4? <VideoFormatWarning videoFormat={videoFormat}/> : null}
{videoFormat === VideoFormat.avi? <VideoFormatWarning videoFormat={videoFormat}/> : null}
</span>
<button id="resetSettingsButton" onClick={() => {
settingListLoadDefault();
setFps(settingList.fps);
setWidth(settingList.width);
setStickDistance(settingList.stickDistance);
setStickMode2(settingList.stickMode2);
setVideoFormat(settingList.videoFormat);
}}>Reset Settings</button>
</div>
<div id="renderImgDiv">
<img id="render-ex" src={renderImg}></img>
{renderLoading? <RenderLoadingSpinner/> : null}
</div>
</div>
)
}
function imageLoading() {
if(pageLoaded) setRenderLoading(true);
}
function imageLoaded() {
if(pageLoaded) {
setRenderImg(picturePath());
setRenderLoading(false);
}
}
export default SettingsPage;
export {
imageLoading,
imageLoaded
};

View File

@@ -0,0 +1,34 @@
import React from "react";
import videojs from "video.js";
import "video.js/dist/video-js.css";
const VideoPlayer = ({options, src}:{options:videojs.PlayerOptions, src:{src:string, type:string}}) => {
const videoNode = React.useRef<HTMLVideoElement>(null);
const player = React.useRef<videojs.Player>();
const srcLoaded = React.useRef<boolean>(false);
const initialized = React.useRef<boolean>(false);
React.useEffect(() => {
if(!srcLoaded.current) {
if(videoNode.current && !initialized.current) {
videojs(videoNode.current, {
...options,
...{
sources: [src]
}
}).ready(function() {
player.current = this;
srcLoaded.current = true;
});
initialized.current = true;
}
} else {
player.current?.src(src);
player.current?.load();
}
}, [options, src]);
return <video ref={videoNode} className="video-js" />;
};
export default VideoPlayer;

View File

@@ -1,14 +0,0 @@
body {
margin: 0;
overflow: hidden;
background-color: #172336;
color: white;
font-family: Arial, Helvetica, sans-serif;
}
.dataDiv {
display: flex;
}
.dataDiv p {
margin-right: 10px;
}

431
src/index.css Normal file
View File

@@ -0,0 +1,431 @@
.dataDiv {
display: flex;
}
.dataDiv p {
margin-right: 10px;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #9DA8B9;
border-radius: 5px;
}
::-webkit-scrollbar-track {
background: #0d131e;
}
header {
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #0d131e;
color: #9DA8B9;
margin-bottom: 25px;
}
#winHeader {
padding: 0px 8px;
padding-right: 0px;
height: 31px;
-webkit-app-region: drag;
}
#linuxHeader {
padding: 20px 25px;
height: 20px;
}
header h4 {
margin-right: auto;
display: flex;
width: auto;
}
header h1 {
margin-right: auto;
display: flex;
width: auto;
}
.back-button-win {
cursor: pointer;
-webkit-app-region: no-drag;
padding: 6px 10px;
height: 100%;
background-color: #2196F3;
color: white;
border: none;
border-radius: 15px;
font-size: large;
}
.back-button-linux {
cursor: pointer;
padding: 10px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 20px;
display: flex;
font-size: large;
}
.back-button-win:hover {
background-color: #0b6ab9;
transform: scale(1);
}
.back-button-linux:hover {
transform: scale(1.05);
text-decoration: none;
}
#backButton-win {
cursor: pointer;
-webkit-app-region: no-drag;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
border: none;
outline: none;
}
#backButton-win svg {
height: 18px;
fill: #9DA8B9;
}
#backButton-win:hover {
background-color: #2d323c;
transform: scale(1);
}
#stopRenderButton {
background-color: #e1334e;
margin-right: 10px;
}
#content {
padding-left: 25px;
padding-right: 25px;
}
#settings-button-win {
cursor: pointer;
-webkit-app-region: no-drag;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
border: none;
outline: none;
}
#settings-button-win svg {
height: 18px;
fill: #9DA8B9;
}
#settings-button-win:hover {
background-color: #2d323c;
transform: scale(1);
}
#settings-button-linux {
cursor: pointer;
background-color: #2196F3;
border-radius: 50%;
width: 45px;
height: 45px;
align-items: center;
border: none;
outline: none;
}
#settings-button-linux svg {
height: 35px;
fill: white;
margin-left: 5px;
margin-top: 5px;
}
#settings-button-linux:hover {
transform: scale(1.05);
}
#blender-info-win {
margin-right: auto;
display: flex;
align-items: center;
}
#blender-info-win p {
padding-left: 5px;
}
#blender-info-linux {
margin-right: auto;
}
#blender-info-linux p {
margin-top: 5px;
margin-bottom: 0;
}
#blender-icon-win {
width: 20px;
height: 20px;
align-items: center;
margin-left: auto;
margin-right: auto;
}
#blender-icon-linux {
width: 25px;
height: 25px;
align-items: center;
margin-left: auto;
margin-right: auto;
}
#blender-icon-win svg {
width: 20px;
height: 20px;
fill: white;
}
#blender-icon-linux svg {
width: 25px;
height: 25px;
fill: white;
}
#blender-loading-icon {
animation: rotate-icon 1.2s linear infinite;
}
#blender-loading-icon path {
fill: #2196F3;
}
#blender-ready-icon path {
fill: #00c24a;
}
#renderLoadingDiv {
position: absolute;
background-color: rgba(0,0,0,0.9);
width: 100%;
height: 100%;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
}
#renderLoadingDiv svg {
height: 50%;
fill: #2196F3;
animation: rotate-icon 1.2s linear infinite;
}
@keyframes rotate-icon {
to {
transform: rotate(1turn);
}
}
#start-render {
cursor: pointer;
background-color: #00c24a;
border-radius: 50%;
width: 100px;
height: 100px;
align-items: center;
border: none;
outline: none;
margin-left: auto;
margin-right: auto;
}
#start-render svg {
width: 50px;
height: 50px;
fill: white;
margin-left: 5px;
margin-top: 5px;
}
#start-render:hover {
transform: scale(1.05);
}
#render-ex {
width: 100%;
height: auto;
display: block;
border-radius: 10px;
}
#renderImgDiv {
position: relative;
margin-top: 15px;
width: 100%;
image-rendering: pixelated;
}
.noMarginBottom {
margin-bottom: 0;
}
#outputDiv {
display: flex;
align-items: center;
}
#output {
margin-left: 5px;
}
#output:hover {
cursor: pointer;
text-decoration: underline;
}
#openLogButton {
margin-top: 5px;
}
#deleteLogsButton {
background-color: #e1334e;
margin-top: 5px;
margin-left: 5px;
}
#openOutputMain {
margin-top: 15px;
display: block;
}
.listButton {
background-color: #e1334e;
padding: 5px;
border-radius: 12px;
}
button {
cursor: pointer;
padding: 10px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 20px;
font-weight: bolder;
}
button:hover {
transform: scale(1.05);
}
.inputSpan {
position: relative;
display: inline-block;
align-items: center;
padding-bottom: 15px;
}
.inputSpan input {
width: 75px;
padding: 8px 0;
border: 0;
border-top-right-radius: 18px;
border-bottom-right-radius: 18px;
text-align: center;
font-size: large;
line-height: 0px;
}
.inputSpan label {
padding: 10px 10px;
background: #00c24a;
border-top-left-radius: 18px;
border-bottom-left-radius: 18px;
font-weight: bolder;
}
#settingRow {
display: flex;
justify-content: space-around;
align-items: center;
}
#resetSettingsButton {
height: 35px;
}
#logList-Name:hover {
cursor: pointer;
text-decoration: underline;
}
.winMenuButtons {
fill: #9DA8B9;
height: 100%;
width: 47px;
align-items: center;
display: flex;
justify-content: center;
-webkit-app-region: no-drag;
}
.winMenuButtons:hover {
background-color: #2d323c;
}
.winMenuButtons svg {
height: 17px;
width: 17px;
}
#winClose:hover {
background-color: red;
}
#winMinimize {
margin-left: 5px;
}
.progress {
height: 25px;
width: 100%;
background-color: #0d131e;
border-radius: 15px;
position: relative;
overflow: hidden;
}
.progress-done {
height: 100%;
background: repeating-linear-gradient(
-45deg,
#2196F3,
#2196F3 10px,
#0b6ab9 10px,
#0b6ab9 20px
);
border-radius: 15px;
transition: width 1s;
}
.progress label {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: large;
}
.selectSpan {
display: flex;
align-items: center;
}
.selectSpanSelect select {
padding: 8px 0;
border: 0;
border-top-right-radius: 18px;
border-bottom-right-radius: 18px;
cursor: pointer;
text-align: center;
font-size: large;
}
.selectSpanLabel {
padding: 10px 10px;
background: #00c24a;
border-top-left-radius: 18px;
border-bottom-left-radius: 18px;
font-weight: bolder;
}
#videoFormatWarning:hover {
transform: scale(1.05);
}

View File

@@ -1,50 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8" />
<title>StickExporterTX</title> <title>StickExporterTX</title>
<link rel="stylesheet" href="./css/style.css"/>
<link rel="stylesheet" href="./css/toggle-switchy.css"/>
</head> </head>
<body> <body style="background-color: #172336;margin:0;padding:0;color:white;font-family:sans-serif;">
<button onclick="startRender()">Start Render</button> <div id="root"></div>
<p id="logNumber">Log 0/0</p>
<div class="dataDiv"> <script>
<p id="status">Idle</p> if(process.env.NODE_ENV === 'development') {
<button onclick="openOutputFolder()">Open Output Folder</button> const port = process.env.PORT || 3000;
</div> document.write('<script src="http://localhost:'+port+'/renderer.js"><\/script>');
<hr> } else {
<div class="dataDiv"> document.write('<script src="./.webpack/renderer.js"><\/script>');
<p>FPS: </p> }
<input id="fpsInput" type="number" value="25" min="1" step="1" onchange="setFPS();"> </script>
</div>
<div class="dataDiv">
<p>Width: </p>
<input id="widthInput" type="number" value="540" min="1" step="1" onchange="setWidth();">
</div>
<div class="dataDiv">
<p>Stick Distance: </p>
<input id="stickDistanceInput" type="number" value="540" min="0" step="1" onchange="setStickDistance();">
</div>
<div class="dataDiv">
<p>Stick Mode:</p>
<label for="stickMode" class="toggle-switchy" data-style="rounded" data-text="12">
<input checked type="checkbox" id="stickMode" onchange="setStickMode();">
<span class="toggle">
<span class="switch"></span>
</span>
</label>
</div>
<div class="dataDiv">
<p id="log">Logs:<br/></p>
<button onclick="openLog();">Open Log</button>
</div>
<div class="dataDiv">
<p id="output">Output Folder: </p>
<button onclick="openVid();">Open Video</button>
</div>
<script src="./js/render.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,57 +0,0 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
// eslint-disable-line global-require
app.quit();
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true
}
});
mainWindow.setMenu(null);
require('@electron/remote/main').initialize();
require("@electron/remote/main").enable(mainWindow.webContents);
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'index.html'));
// Open the DevTools.
// mainWindow.webContents.openDevTools();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

169
src/index.ts Normal file
View File

@@ -0,0 +1,169 @@
import {app, BrowserWindow, dialog, ipcMain, NativeImage} from 'electron';
import {initialize as remoteInitialize, enable as remoteEnable} from '@electron/remote/main';
import path from 'path';
import { autoUpdater } from "electron-updater";
import logger from 'electron-log';
import { Platform, platform } from './components/platform';
logger.transports.console.format = "{h}:{i}:{s} {text}";
logger.transports.file.getFile();
logger.transports.file.resolvePath = () => path.join(app.getPath('userData'), "logs", "start.log");
const appIconPath = path.join(app.getAppPath().replace("app.asar", ""), "assets", "icon.png");
const finsishedIconPath = path.join(app.getAppPath().replace("app.asar", ""), "assets", "render_finished_icon.png");
autoUpdater.autoDownload = false;
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-available', async () => {
const response = await dialog.showMessageBox({
type: 'info',
title: 'Update Available',
message: 'Found updates, do you want update now?',
buttons: ['Yes', 'Later'],
});
if (response.response === 0) {
logger.log('Downloading Update');
autoUpdater.downloadUpdate();
await dialog.showMessageBox({
type: 'info',
title: 'Update Downloading',
message:
'Update is being downloaded, you will be notified when it is ready to install',
buttons: [],
});
}
});
autoUpdater.on('update-downloaded', async () => {
const response = await dialog.showMessageBox({
type: 'info',
buttons: ['Restart', 'Later'],
title: 'Application Update',
message: 'Update',
detail:
'A new version has been downloaded. Restart the application to apply the updates.',
});
if (response.response === 0) {
setImmediate(() => autoUpdater.quitAndInstall());
}
});
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
// eslint-disable-line global-require
app.quit();
}
if(platform === Platform.Windows) {
app.setAppUserModelId(app.name);
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
minWidth: 600,
minHeight: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
titleBarStyle: 'hidden'
});
mainWindow.setMenu(null);
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
remoteInitialize();
remoteEnable(mainWindow.webContents);
// load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'index.html'));
ipcMain.on('closeApp', () => {
mainWindow.webContents.send('isRenderActiveClose');
});
ipcMain.on('minimize', () => {
mainWindow.minimize();
});
ipcMain.on('renderInactiveClose', () => {
app.quit();
});
ipcMain.on('renderActiveClose', async () => {
const response = await dialog.showMessageBox({
type: 'warning',
noLink: true,
buttons: ['Cancel','Minimize', 'Exit'],
defaultId: 0,
title: 'Close',
message: 'A video is still being renderd!',
detail:
'If you close the application, the progress will be lost!',
});
if (response.response === 2) {
app.quit();
} else if (response.response === 1) {
mainWindow.minimize();
}
});
ipcMain.on('setProgress', (event, arg) => {
const progress = parseFloat(arg);
mainWindow.setProgressBar(progress);
});
ipcMain.on('renderFinished', () => {
if(!mainWindow.isFocused()) {
mainWindow.setOverlayIcon(finsishedIconPath as unknown as NativeImage, 'Rendering Complete');
mainWindow.flashFrame(true);
}
});
mainWindow.on('focus', () => {
mainWindow.setOverlayIcon(null, '');
mainWindow.flashFrame(false);
});
mainWindow.on('close', () => {
mainWindow.webContents.send('closeApp');
});
ipcMain.on('openApp', () => {
mainWindow.show();
});
mainWindow.setIcon(appIconPath);
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (platform !== Platform.Mac) {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

View File

@@ -1,220 +0,0 @@
var lastData = "Initialise..."
var frames = "0";
var lastFrame = "0";
var logCount = 0;
var fps = 25
var width = 540
var stickDistance = 5
var stickMode2 = true
var log = '"None"'
var output = "None"
const statusDisplay = document.getElementById("status");
const fpsDisplay = document.getElementById("fpsInput");
const widthDistplay = document.getElementById("widthInput");
const stickDistanceDisplay = document.getElementById("stickDistanceInput");
const stickModeDisplay = document.getElementById("stickMode");
const logDisplay = document.getElementById("log");
const outputDisplay = document.getElementById("output");
const logNumberDisplay = document.getElementById("logNumber");
const logger = require('electron-log');
const fs = require("fs");
const formatXml = require("xml-formatter");
const {dialog, app} = require("@electron/remote");
const path = require('path');
const lineReplace = require('line-replace');
const dataFolder = app.getPath('userData');
const SettingFolder = path.join(dataFolder, "settings.xml");
const blenderPath = path.join("assets", "blender", "blender");
const templatePath = path.join("assets", "template.blend");
const blenderScriptPath = path.join("assets", "blenderScript.py");
lineReplace({
file: blenderScriptPath,
line: 9,
text: 'settings = ET.parse("' + SettingFolder.replaceAll('\\', '/') + '")',
addNewLine: true,
callback: ({error}) => {
if(error != null) {
statusDisplay.innerHTML = "Something went wrong! Check Logs.";
statusDisplay.style.color = "red";
logger.error(error);
}
}
});
logger.transports.console.format = "{h}:{i}:{s} {text}";
logger.transports.file.getFile();
logger.transports.file.resolvePath = () => path.join(dataFolder, "logs", "main.log");
function startRender() {
const {exec} = require("child_process");
var blenderCons = exec('"' + blenderPath + '" "' + templatePath + '" --background --python "' + blenderScriptPath + '"', {maxBuffer: Infinity});
frames = "0";
lastFrame = "0";
statusDisplay.innerHTML = lastData = "Initialise...";
statusDisplay.style.color = "white";
logNumberDisplay.innerHTML = "Log 0/" + String(logCount);
blenderCons.stdout.on("data", (data) => {
var dataStr = String(data);
logger.info(dataStr);
if(dataStr.startsWith("Frames:")) {
frames = dataStr.split(":")[1];
} else if(dataStr.startsWith("Fra:")) {
lastFrame = dataStr.split(":")[1].split(" ")[0]
lastData = "Render Frame " + lastFrame + "/" + frames;
} else if(dataStr.startsWith("Finished")) {
if(lastFrame == frames) {
lastData = "Finished Render Successfully!"
statusDisplay.style.color = "white";
} else {
lastData = "Something went wrong! Check Logs."
statusDisplay.style.color = "red";
}
} else if(dataStr.includes("Blender quit")) {
if(lastData != "Finished Render Successfully!") {
lastData = "Something went wrong! Check Logs.";
statusDisplay.style.color = "red";
}
} else if(dataStr.startsWith("Init:")) {
lastData = "Initialize Frame " + dataStr.split(":")[1] + "/" + frames;
} else if(dataStr.startsWith("Lognr:")) {
logNumberDisplay.innerHTML = "Log " + dataStr.split(":")[1] + "/" + String(logCount);
}
if(statusDisplay.innerHTML != lastData) {
statusDisplay.innerHTML = lastData;
}
});
}
function getXMLChild(doc, child) {
return String(doc.getElementsByTagName(child)[0].childNodes[0].nodeValue);
}
function updateSettingDisplay() {
fpsDisplay.value = String(fps);
widthDistplay.value = String(width);
stickDistanceDisplay.value = String(stickDistance);
stickModeDisplay.checked = stickMode2;
logDisplay.innerHTML = "Logs:<br/>" + log.substring(1).slice(0, -1).replaceAll("\"\"", "<br/>");
outputDisplay.innerHTML = "Output Folder: " + output;
logCount = log.split("\"\"").length;
logNumberDisplay.innerHTML = "Log 0/" + String(logCount);
}
function updateSettings() {
var xmlStr = '<?xml version="1.0"?><settings><fps>' + String(fps) +
'</fps><width>' + String(width) +
'</width><stickDistance>' + String(stickDistance) +
'</stickDistance><stickMode2>' + ((stickMode2)?"true":"false") +
'</stickMode2><log>' + log +
'</log><output>' + output +
'</output></settings>';
fs.writeFile(SettingFolder, formatXml(xmlStr, {collapseContent: true}), function(err) {
if(err) {
statusDisplay.innerHTML = "Couldn't write Log! Check Logs.";
statusDisplay.style.color = "red";
logger.error(err);
}
});
}
fetch(SettingFolder).then(function(response){
return response.text();
}).then(function(data){
let parser = new DOMParser();
let xmlDoc = parser.parseFromString(data, 'text/xml');
fps = parseInt(getXMLChild(xmlDoc, "fps"));
width = parseInt(getXMLChild(xmlDoc, "width"));
stickDistance = parseInt(getXMLChild(xmlDoc, "stickDistance"));
if(getXMLChild(xmlDoc, "stickMode2") == "false") {
stickMode2 = false;
} else {
stickMode2 = true;
}
log = getXMLChild(xmlDoc, "log");
output = getXMLChild(xmlDoc, "output");
updateSettingDisplay();
}).catch(function(error) {
logger.error(error);
updateSettingDisplay();
updateSettings();
});
function openLog() {
dialog.showOpenDialog({
properties: [
"multiSelections"
],
filters: [
{
name: "TX-Logs",
extensions: [
"csv"
]
}
]
}).then(result => {
logStr = "";
result.filePaths.forEach(value => {
logStr += "\"" + String(value) + "\"";
});
log = logStr;
updateSettingDisplay();
updateSettings();
}).catch(err => {
statusDisplay.innerHTML = "Something went wrong! Check Logs.";
statusDisplay.style.color = "red";
logger.error(err);
});
}
function openVid() {
dialog.showOpenDialog({
properties: [
"openDirectory"
]
}).then(result => {
output = String(result.filePaths);
updateSettingDisplay();
updateSettings();
}).catch(err => {
statusDisplay.innerHTML = "Something went wrong! Check Logs.";
statusDisplay.style.color = "red";
logger.error(err);
});
}
function setFPS() {
fps = parseInt(fpsDisplay.value);
updateSettings();
}
function setWidth() {
width = parseInt(widthDistplay.value);
updateSettings();
}
function setStickDistance() {
stickDistance = parseInt(stickDistanceDisplay.value);
updateSettings();
}
function setStickMode() {
stickMode2 = stickModeDisplay.checked;
updateSettings();
}
function openOutputFolder() {
require("child_process").exec('start "" "' + output + '"');
}

71
src/renderer.tsx Normal file
View File

@@ -0,0 +1,71 @@
import React from "react";
import ReactDOM from "react-dom/client";
import Menu from "./components/ui/menu";
import MainPage from "./components/ui/mainPage";
import SettingsPage from "./components/ui/settingsPage";
import RenderingPage from "./components/ui/renderingPage";
import RenderFinishPage from "./components/ui/renderFinishPage";
import "./index.css";
import "./toggle-switchy.css";
import { startBlender } from "./components/blenderController";
import {ipcRenderer} from "electron";
enum Page {
Main,
Rendering,
Settings,
RenderFinish
}
let rendering = false;
let currentPage = Page.Main;
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
function openPage(page:Page) {
if(page == Page.Main && rendering) {
page = Page.Rendering;
}
currentPage = page;
root.render(
<React.StrictMode>
<Menu page={page}/>
{(page === Page.Main)? <MainPage/> : null}
{(page === Page.Settings)? <SettingsPage/> : null}
{(page === Page.Rendering)? <RenderingPage/> : null}
{(page === Page.RenderFinish)? <RenderFinishPage/> : null}
</React.StrictMode>
);
}
openPage(currentPage);
startBlender();
function pageSetRendering(value:boolean) {
rendering = value;
if(value) {
if(currentPage === Page.Main) {
openPage(Page.Rendering);
}
} else {
if(currentPage === Page.Rendering) {
openPage(Page.Main);
}
}
}
function setProgress(value?:number) {
if(value === undefined) {
ipcRenderer.send("setProgress", -1);
} else {
ipcRenderer.send("setProgress", value);
}
}
export {
openPage,
Page,
pageSetRendering,
setProgress
}

View File

@@ -17,9 +17,9 @@ Releases: https://github.com/adamculpepper/toggle-switchy/releases
.toggle-switchy > input + .toggle:after {content:'OFF';} .toggle-switchy > input + .toggle:after {content:'OFF';}
.toggle-switchy > input + .toggle > .switch {background:#fff;} .toggle-switchy > input + .toggle > .switch {background:#fff;}
.toggle-switchy > input + .toggle + .label {color:#000;} .toggle-switchy > input + .toggle + .label {color:#000;}
.toggle-switchy > input:checked + .toggle {background:#3498db;} .toggle-switchy > input:checked + .toggle {background:#00c24a;}
.toggle-switchy > input:not(:checked) + .toggle {background:#ccc;} .toggle-switchy > input:not(:checked) + .toggle {background:#ccc;}
.toggle-switchy > input:checked + .toggle > .switch {border:3px solid #3498db;} .toggle-switchy > input:checked + .toggle > .switch {border:3px solid #00c24a;}
.toggle-switchy > input:not(:checked) + .toggle > .switch {border:3px solid #ccc;} .toggle-switchy > input:not(:checked) + .toggle > .switch {border:3px solid #ccc;}
.toggle-switchy > input:focus + .toggle, .toggle-switchy > input:focus + .toggle,
.toggle-switchy > input:active + .toggle {box-shadow:0 0 5px 3px rgba(0, 119, 200, 0.50);} .toggle-switchy > input:active + .toggle {box-shadow:0 0 5px 3px rgba(0, 119, 200, 0.50);}
@@ -149,7 +149,7 @@ CORE STYLES ABOVE - NO TOUCHY
/* Text: 1/2 */ /* Text: 1/2 */
.toggle-switchy[data-text='12'] > input + .toggle:before {content:'2';} .toggle-switchy[data-text='12'] > input + .toggle:before {content:'2';}
.toggle-switchy[data-text='12'] > input + .toggle:after {content:'1';} .toggle-switchy[data-text='12'] > input + .toggle:after {content:'1';}
.toggle-switchy[data-text='12'] > input:checked + .toggle {background:#3498db;} .toggle-switchy[data-text='12'] > input:checked + .toggle {background:#00c24a;}
.toggle-switchy[data-text='12'] > input:checked + .toggle > .switch {border-color:#3498db;} .toggle-switchy[data-text='12'] > input:checked + .toggle > .switch {border-color:#00c24a;}
.toggle-switchy[data-text='12'] > input:not(:checked) + .toggle > .switch {border-color:#3498db;} .toggle-switchy[data-text='12'] > input:not(:checked) + .toggle > .switch {border-color:#00c24a;}
.toggle-switchy[data-text='12'] > input:not(:checked) + .toggle {background:#3498db;} .toggle-switchy[data-text='12'] > input:not(:checked) + .toggle {background:#00c24a;}

110
tsconfig.json Normal file
View File

@@ -0,0 +1,110 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"jsx": "react", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "esnext", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"*": ["node_modules/*"]
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
"noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
"isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*", "js"]
}