added webpack dev server, slightly improved ui and render preview

This commit is contained in:
2022-06-13 01:15:17 +02:00
parent 7ea3b514ce
commit 6f6c84dc46
19 changed files with 722 additions and 869 deletions

3
.gitignore vendored
View File

@@ -265,3 +265,6 @@ output/
blender-win/ blender-win/
blender-linux/ blender-linux/
blender/
index.build.js

View File

@@ -1,16 +1,15 @@
from ast import Str
import csv import csv
from importlib.resources import path
import logging import logging
import math import math
import sys import sys
import time
import bpy import bpy
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
argv = sys.argv argv = sys.argv
argv = argv[argv.index("--") + 1:] argv = argv[argv.index("--") + 1:]
settings = ET.parse(argv[0])
logger = logging.getLogger('simple_example') logger = logging.getLogger('simple_example')
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(message)s') formatter = logging.Formatter('%(message)s')
@@ -19,19 +18,15 @@ console_handler.setFormatter(formatter)
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
logger.addHandler(console_handler) logger.addHandler(console_handler)
def _map(x, in_min, in_max, out_min, out_max): GimbalL = bpy.data.objects["GimbalL"]
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min StickL = bpy.data.objects["StickL"]
GimbalR = bpy.data.objects["GimbalR"]
settingsRoot = settings.getroot() StickR = bpy.data.objects["StickR"]
GimbalCoverR = bpy.data.objects["GimbalCoverR"]
FPS = int(settingsRoot[0].text) TrailR = bpy.data.objects["TrailR"]
Width = int(settingsRoot[1].text) Camera = bpy.data.objects["Camera"]
StickDistance = _map(int(settingsRoot[2].text), 0, 100, 5, 105) Plane = bpy.data.objects["Plane.001"]
StickMode = settingsRoot[3].text scn = bpy.context.scene
if(StickMode == "true"):
StickMode = 2
else:
StickMode = 1
lyMax = 0.436 lyMax = 0.436
lyMin = -0.436 lyMin = -0.436
@@ -42,127 +37,174 @@ ryMin = 0.436
rxMax = 0.436 rxMax = 0.436
rxMin = -0.436 rxMin = -0.436
logs = settingsRoot[4].text[1:][:-1].split("\"\"") 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
logCount = len(logs) logger.info("Blender started successfully!")
logNumber = 1
for log in logs: while True:
logger.info("Lognr:" + ((str)(logNumber)) + ":") command = input("Waiting for command: ")
logTime = [] time.sleep(0.5)
rud = []
ele = []
thr = []
ail = []
try: settingsRoot = ET.parse(argv[0]+"/settings.xml").getroot()
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 = [] StickMode = settingsRoot[3].text
i = 0 if(StickMode == "true"):
while i < len(logTime)-1: StickMode = 2
if int(logTime[i]) > int(logTime[i+1]): else:
meanTime.append(60000 - int(logTime[i]) + int(logTime[i+1])) StickMode = 1
else:
meanTime.append(int(logTime[i+1]) - int(logTime[i]))
i+=1
totalTime = 0 fps = int(settingsRoot[0].text)
for e in meanTime: width = int(settingsRoot[1].text)
totalTime+=e StickDistance = _map(int(settingsRoot[2].text), 0, 100, 5, 105)
logs = settingsRoot[4].text[1:][:-1].split("\"\"")
output = settingsRoot[5].text
frameCount = math.floor(totalTime/1000*FPS-1) if(command == "startRendering"):
FPSxxx = 1000/FPS
except Exception as e:
print("Can't read Log!")
exit()
GimbalL = bpy.data.objects["GimbalL"] logCount = len(logs)
StickL = bpy.data.objects["StickL"] logNumber = 1
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 for log in logs:
GimbalCoverR.location[0] = StickDistance logger.info("Lognr:" + ((str)(logNumber)) + ":")
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 logTime = []
scn.render.fps_base = FPSxxx rud = []
ele = []
thr = []
ail = []
scn.frame_start = 0 try:
scn.frame_end = frameCount+1 with open(log, newline='') as csvFile:
logger.info("Frames:" + str(frameCount+1) + ":") 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']))
frame = 0 meanTime = []
log = 0 i = 0
pastTime = 0 while i < len(logTime)-1:
while frame <= frameCount: if int(logTime[i]) > int(logTime[i+1]):
currentTime = math.floor(FPSxxx*frame) meanTime.append(60000 - int(logTime[i]) + int(logTime[i+1]))
while currentTime >= pastTime+meanTime[log]: else:
pastTime+=meanTime[log] meanTime.append(int(logTime[i+1]) - int(logTime[i]))
log+=1 i+=1
multiplier = (currentTime-pastTime)/meanTime[log] totalTime = 0
for e in meanTime:
totalTime+=e
ailP = _map(ail[log]+(ail[log+1]-ail[log])*multiplier, -1024, 1024, rxMin, rxMax) frameCount = math.floor(totalTime/1000*fps-1)
eleP = _map(ele[log]+(ele[log+1]-ele[log])*multiplier, -1024, 1024, ryMin, ryMax) FPSxxx = 1000/fps
rudP = _map(rud[log]+(rud[log+1]-rud[log])*multiplier, -1024, 1024, lyMin, lyMax) except Exception as e:
thrP = _map(thr[log]+(thr[log+1]-thr[log])*multiplier, -1024, 1024, lxMin, lxMax) logger.error("Can't read Log: " + (Str)(e))
bpy.context.scene.frame_set(frame) bpy.context.scene.render.image_settings.file_format = 'FFMPEG'
bpy.context.scene.render.ffmpeg.format = "QUICKTIME"
bpy.context.scene.render.ffmpeg.codec = "QTRLE"
if StickMode == "1": scn.render.resolution_x = width
StickL.rotation_euler=[0,0,0] GimbalCoverR.location[0] = StickDistance
StickL.rotation_euler.rotate_axis("Y", ailP) GimbalR.location[0] = StickDistance
StickL.keyframe_insert(data_path="rotation_euler", index=-1) TrailR.location[0] = StickDistance
GimbalL.rotation_euler=[0,0,0] Plane.location[0] = StickDistance
GimbalL.rotation_euler.rotate_axis("X", eleP) Camera.location[0] = StickDistance/2
GimbalL.keyframe_insert(data_path="rotation_euler", index=-1) Camera.data.ortho_scale = StickDistance+5
StickR.rotation_euler=[0,0,0] scn.render.resolution_y = int(width/_map(StickDistance, 5, 105, 2, 21.6))
StickR.rotation_euler.rotate_axis("Y", rudP) bpy.context.scene.render.filepath = output + "\\" + log.split("/")[-1].split("\\")[-1].replace(".csv", ".mov")
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)) + ":") scn.render.fps = 1000
frame+=1 scn.render.fps_base = FPSxxx
bpy.ops.render.render(animation=True) scn.frame_start = 0
scn.frame_end = frameCount
logger.info("Frames:" + str(frameCount) + ":")
if(logCount <= logNumber): frame = 0
logger.info("Finished") log = 0
pastTime = 0
while frame <= frameCount:
currentTime = math.floor(FPSxxx*frame)
while currentTime >= pastTime+meanTime[log]:
pastTime+=meanTime[log]
log+=1
logNumber+=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
elif(command == "getRender"):
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.context.scene.frame_set(0)
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))
StickL.rotation_euler=[0,0,0]
StickL.rotation_euler.rotate_axis("Y", 0.436)
StickL.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalL.rotation_euler=[0,0,0]
GimbalL.rotation_euler.rotate_axis("X", 0.236)
GimbalL.keyframe_insert(data_path="rotation_euler", index=-1)
StickR.rotation_euler=[0,0,0]
StickR.rotation_euler.rotate_axis("Y", -0.056)
StickR.keyframe_insert(data_path="rotation_euler", index=-1)
GimbalR.rotation_euler=[0,0,0]
GimbalR.rotation_euler.rotate_axis("X", -0.436)
GimbalR.keyframe_insert(data_path="rotation_euler", index=-1)
bpy.ops.render.render(write_still=True)

View File

@@ -3,7 +3,6 @@ import ESLintPlugin from "eslint-webpack-plugin";
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
export default { export default {
mode: 'production',
module: { module: {
rules: [ rules: [
{ {

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

@@ -2,9 +2,10 @@ import baseConfig from "./webpack.base.config.js";
import {merge} from 'webpack-merge'; import {merge} from 'webpack-merge';
export default merge(baseConfig, { export default merge(baseConfig, {
mode: 'production',
target: 'electron-main', target: 'electron-main',
entry: './src/index.ts', entry: './src/index.ts',
output: { output: {
filename: 'index.js' 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

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

706
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,15 @@
"name": "stickexportertx", "name": "stickexportertx",
"productName": "StickExporterTX", "productName": "StickExporterTX",
"description": "3D stick exporter for EdgeTX/OpenTX logs", "description": "3D stick exporter for EdgeTX/OpenTX logs",
"main": "src/.webpack/index.js", "main": "src/index.build.js",
"scripts": { "scripts": {
"build:main": "webpack --config configs/webpack.main.config.babel.js", "build:main": "cross-env NODE_ENV=production webpack --config configs/webpack.main.prod.config.babel.js",
"build:renderer": "webpack --config configs/webpack.renderer.config.babel.js", "build:renderer": "cross-env NODE_ENV=production webpack --config configs/webpack.renderer.prod.config.babel.js",
"build": "npm run build:main && npm run build:renderer", "build": "cross-env npm run build:main && cross-env npm run build:renderer",
"start": "npm run build && electron .", "bundle": "cross-env npm run build && electron-builder build",
"build:app": "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": {
@@ -18,33 +20,6 @@
"url": "https://link.lino3d.de" "url": "https://link.lino3d.de"
}, },
"license": "MIT", "license": "MIT",
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "stickexportertx"
}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
},
"build": { "build": {
"appId": "de.lino3d.stickexportertx", "appId": "de.lino3d.stickexportertx",
"productName": "StickExporterTX", "productName": "StickExporterTX",
@@ -54,7 +29,8 @@
"package.json" "package.json"
], ],
"extraFiles": [ "extraFiles": [
"assets/template.blend" "assets/template.blend",
"assets/blenderScript.py"
], ],
"win": { "win": {
"icon": "icon.png", "icon": "icon.png",
@@ -63,7 +39,7 @@
], ],
"extraFiles": [ "extraFiles": [
{ {
"from": "assets/blender-win/", "from": "assets/blender/",
"to": "assets/blender/", "to": "assets/blender/",
"filter": [ "filter": [
"**/*" "**/*"
@@ -86,7 +62,7 @@
], ],
"extraFiles": [ "extraFiles": [
{ {
"from": "assets/blender-linux/", "from": "assets/blender/",
"to": "assets/blender/", "to": "assets/blender/",
"filter": [ "filter": [
"**/*" "**/*"
@@ -102,7 +78,7 @@
], ],
"extraFiles": [ "extraFiles": [
{ {
"from": "assets/blender-mac/", "from": "assets/blender/",
"to": "assets/blender/", "to": "assets/blender/",
"filter": [ "filter": [
"**/*" "**/*"
@@ -124,6 +100,7 @@
"@electron/remote": "^2.0.8", "@electron/remote": "^2.0.8",
"electron-log": "^4.4.7", "electron-log": "^4.4.7",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"electron-updater": "^5.0.1",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"xml-formatter": "^2.6.1" "xml-formatter": "^2.6.1"
@@ -144,6 +121,7 @@
"@typescript-eslint/parser": "^5.27.0", "@typescript-eslint/parser": "^5.27.0",
"@vercel/webpack-asset-relocator-loader": "^1.7.2", "@vercel/webpack-asset-relocator-loader": "^1.7.2",
"babel-loader": "^8.2.5", "babel-loader": "^8.2.5",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
"electron": "18.1.0", "electron": "18.1.0",
"electron-builder": "^23.0.3", "electron-builder": "^23.0.3",
@@ -153,7 +131,6 @@
"eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-react-hooks": "^4.5.0",
"eslint-webpack-plugin": "^3.1.1", "eslint-webpack-plugin": "^3.1.1",
"fork-ts-checker-webpack-plugin": "^7.2.11", "fork-ts-checker-webpack-plugin": "^7.2.11",
"html-webpack-plugin": "^5.5.0",
"node-loader": "^2.0.0", "node-loader": "^2.0.0",
"sass": "^1.52.1", "sass": "^1.52.1",
"sass-loader": "^13.0.0", "sass-loader": "^13.0.0",

View File

@@ -0,0 +1,145 @@
import { blenderPath, blenderScriptPath, dataPath, templatePath } from "./paths";
import {spawn} from "child_process";
import logger from "./logger";
import { setBlenderLoading, setBlenderStatus } from "./ui/menu";
import { setLogNumber, setStatus } from "./ui/mainSide";
import {imageLoading, imageLoaded} from "./ui/settingsSide";
const blenderStartString = [
templatePath,
"--background",
"--python",
blenderScriptPath,
"--",
dataPath.replaceAll("\\", "/")
]
let blenderConsole = spawn(blenderPath, blenderStartString);
let readyToAcceptCommand = false;
let renderingPicture = false;
let renderingVideo = false;
let waitingForRender = false;
function startBlender() {
let frames = "0";
let lastFrame = "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);
restartBlender();
}
if(dataStr.includes("Frames:")) {
frames = dataStr.split(":")[1];
renderingVideo = true;
readyToAcceptCommand = false;
setBlenderStatus("Rendering");
setBlenderLoading(true);
}
if(dataStr.includes("Fra:") && renderingVideo) {
lastFrame = dataStr.split(":")[1].split(" ")[0];
setStatus("Rendering Frame " + lastFrame + "/" + frames);
}
if(dataStr.includes("Finished") && renderingVideo) {
if(lastFrame == frames) {
setStatus("Finished Render Successfully!");
} else {
logger.errorMSG("Render Failed!");
}
}
if(dataStr.includes("Init:") && renderingVideo) {
setStatus("Initialize Frame " + dataStr.split(":")[1] + "/" + frames);
}
if(dataStr.includes("Lognr:") && renderingVideo) {
setLogNumber(dataStr.split(":")[1]);
}
if(dataStr.includes("Waiting for command")) {
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() {
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();
blenderConsole.stdin.write("getRender\n");
} else {
waitingForRender = true;
}
} else if(command === blenderCmd.startRendering) {
if(readyToAcceptCommand) {
readyToAcceptCommand = false;
renderingVideo = true;
blenderConsole.stdin.write("startRendering\n");
}
} else if(command === blenderCmd.stopRendering) {
restartBlender();
readyToAcceptCommand = false;
renderingPicture = false;
renderingVideo = false;
}
}
export {
blender,
blenderCmd,
startBlender,
renderingPicture
}

View File

@@ -4,6 +4,6 @@ import {app} from '@electron/remote';
export const dataPath = app.getPath('userData'); export const dataPath = app.getPath('userData');
export const SettingPath = path.join(dataPath, "settings.xml"); export const SettingPath = path.join(dataPath, "settings.xml");
export const blenderPath = path.join("assets", "blender-win", "blender"); export const blenderPath = path.join("assets", "blender", "blender");
export const templatePath = path.join("assets", "template.blend"); export const templatePath = path.join("assets", "template.blend");
export const blenderScriptPath = path.join("assets", "blenderScript.py"); export const blenderScriptPath = path.join("assets", "blenderScript.py");

View File

@@ -1,43 +0,0 @@
import {blenderPath, blenderScriptPath, SettingPath, templatePath} from "./paths";
import {exec} from "child_process";
import logger from "./logger";
import React from "react";
function Render(setStatusDisplay:React.Dispatch<React.SetStateAction<string>>, setLogNumber:React.Dispatch<React.SetStateAction<string>>) {
const blenderCons = exec('"' + blenderPath + '" "' + templatePath + '" --background --python "' + blenderScriptPath + '" -- "' + SettingPath.replaceAll('\\', '/') + '"', {maxBuffer: Infinity});
let frames = "0";
let lastFrame = "0";
let lastData = "Initializing...";
setStatusDisplay(lastData);
blenderCons.stdout?.on("data", (data:string) => {
logger.info(data);
if(data.startsWith("Frames:")) {
frames = data.split(":")[1];
} else if(data.startsWith("Fra:")) {
lastFrame = data.split(":")[1].split(" ")[0]
lastData = "Render Frame " + lastFrame + "/" + frames;
} else if(data.startsWith("Finished")) {
if(lastFrame == frames) {
lastData = "Finished Render Successfully!"
} else {
logger.errorMSG("Render Failed!");
}
} else if(data.includes("Blender quit")) {
if(lastData != "Finished Render Successfully!") {
logger.errorMSG("Render Failed!");
}
} else if(data.startsWith("Init:")) {
lastData = "Initialize Frame " + data.split(":")[1] + "/" + frames;
} else if(data.startsWith("Lognr:")) {
setLogNumber(data.split(":")[1]);
}
setStatusDisplay(lastData);
});
}
export default Render;

View File

@@ -3,11 +3,16 @@ import { dialog } from "@electron/remote";
import { settingList, updateSettings } from "../settings"; import { settingList, updateSettings } from "../settings";
import logger from "../logger"; import logger from "../logger";
import {exec} from "child_process"; import {exec} from "child_process";
import Render from "../render"; import {blender, blenderCmd} from "../blender-controller";
let setStatus:React.Dispatch<React.SetStateAction<string>>;
let setLogNumber:React.Dispatch<React.SetStateAction<string>>;
function MainSide() { function MainSide() {
const [status, setStatus] = useState("Idle"); const [status, setStatusInner] = useState("Idle");
const [logNumber, setLogNumber] = useState("0"); setStatus = setStatusInner;
const [logNumber, setLogNumberInner] = useState("0");
setLogNumber = setLogNumberInner;
const [logs, setLogs] = useState(settingList.log); const [logs, setLogs] = useState(settingList.log);
const [output, setOutput] = useState(settingList.output); const [output, setOutput] = useState(settingList.output);
const [logTable, setLogTable] = useState(logs.substring(1).slice(0, -1).split('""').map((log, index) => { const [logTable, setLogTable] = useState(logs.substring(1).slice(0, -1).split('""').map((log, index) => {
@@ -25,7 +30,12 @@ function MainSide() {
return ( return (
<div id="content"> <div id="content">
<button onClick={() => Render(setStatus, setLogNumber)}>Start Render</button> <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>
<p>{"Log " + logNumber + "/" + String(settingList.log.split("\"\"").length)}</p> <p>{"Log " + logNumber + "/" + String(settingList.log.split("\"\"").length)}</p>
<div className="dataDiv"> <div className="dataDiv">
<p>{status}</p> <p>{status}</p>
@@ -96,3 +106,7 @@ function openOutputFolder() {
} }
export default MainSide; export default MainSide;
export {
setStatus,
setLogNumber
}

View File

@@ -1,4 +1,4 @@
import React from "react"; import React, {useState} from "react";
import { openSide, Side } from "../../renderer"; import { openSide, Side } from "../../renderer";
const UpdateButton = () => ( const UpdateButton = () => (
@@ -25,12 +25,46 @@ const OtherSideButtons = () => (
<button id="settings-back" onClick={() => openSide(Side.Main)}>Back</button> <button id="settings-back" onClick={() => openSide(Side.Main)}>Back</button>
) )
const Menu = ({updateAvailable, side}:{updateAvailable:boolean, side:Side}) => ( const BlenderLoadingSVG = () => (
<header> <svg id="blender-loading-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<h1 id="main-headline">{(side == Side.Main)? "StickExporterTX" : "Settings"}</h1> {/* Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. */}
{updateAvailable? <UpdateButton/> : null} <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"/>
{(side == Side.Main)? <MainSideButtons/> : <OtherSideButtons/>} </svg>
</header> )
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>
) )
let setBlenderLoading:React.Dispatch<React.SetStateAction<boolean>>;
let setBlenderStatus:React.Dispatch<React.SetStateAction<string>>;
function Menu({updateAvailable, side}:{updateAvailable:boolean, side:Side}) {
const [blenderLoading, setBlenderLoadingInner] = useState(true);
setBlenderLoading = setBlenderLoadingInner;
const [blenderStatus, setBlenderStatusInner] = useState("Starting");
setBlenderStatus = setBlenderStatusInner;
return (
<header>
<h1 id="main-headline">{(side == Side.Main)? "StickExporterTX" : "Settings"}</h1>
<div id="blender-info">
<div id="blender-icon">
{blenderLoading? <BlenderLoadingSVG/> : <BlenderReadySVG/>}
</div>
<p>{blenderStatus}</p>
</div>
{updateAvailable? <UpdateButton/> : null}
{(side == Side.Main)? <MainSideButtons/> : <OtherSideButtons/>}
</header>
)
}
export default Menu; export default Menu;
export {
setBlenderLoading,
setBlenderStatus
}

View File

@@ -1,5 +1,25 @@
import React, {useState} from "react"; import React, {useState, useEffect} from "react";
import { settingList, updateSettings, settingListLoadDefault } from "../settings"; import { settingList, updateSettings, settingListLoadDefault } from "../settings";
import {blender, blenderCmd, renderingPicture} from "../blender-controller";
import {dataPath} from "../paths";
import path from "path";
let setRenderImg:React.Dispatch<React.SetStateAction<string>>;
let setRenderLoading:React.Dispatch<React.SetStateAction<boolean>>;
let sideLoaded = 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 SettingsSide() { function SettingsSide() {
@@ -7,36 +27,47 @@ function SettingsSide() {
const [width, setWidth] = useState(settingList.width); const [width, setWidth] = useState(settingList.width);
const [stickDistance, setStickDistance] = useState(settingList.stickDistance); const [stickDistance, setStickDistance] = useState(settingList.stickDistance);
const [stickMode2, setStickMode2] = useState(settingList.stickMode2); const [stickMode2, setStickMode2] = useState(settingList.stickMode2);
const [renderImg, setRenderImgInner] = useState(picturePath());
setRenderImg = setRenderImgInner;
const [renderLoading, setRenderLoadingInner] = useState(renderingPicture);
setRenderLoading = setRenderLoadingInner;
useEffect(() => {
const timer = setTimeout(() => {
updateSettings({fps, width, stickDistance, stickMode2});
blender(blenderCmd.getRender);
}, 500);
return () => clearTimeout(timer);
}, [fps, width, stickDistance, stickMode2]);
sideLoaded = true;
return ( return (
<div id="content"> <div id="content">
<div className="dataDiv"> <div className="dataDiv">
<p>FPS: </p> <p>FPS: </p>
<input id="fpsInput" type="number" value={fps.toString()} min="1" step="1" onChange={e => { <input id="fpsInput" type="number" defaultValue={fps.toString()} min="1" step="1" onChange={e => {
updateSettings({fps:parseInt(e.target.value)}); if(e.target.value.trim().length !== 0) setFps(parseInt(e.target.value));
setFps(settingList.fps);
}}/> }}/>
</div> </div>
<div className="dataDiv"> <div className="dataDiv">
<p>Width: </p> <p>Width: </p>
<input id="widthInput" type="number" value={width.toString()} min="1" step="1" onChange={e => { <input id="widthInput" type="number" defaultValue={width.toString()} min="1" step="1" onChange={e => {
updateSettings({width:parseInt(e.target.value)}); if(e.target.value.trim().length !== 0) setWidth(parseInt(e.target.value));
setWidth(settingList.width);
}}/> }}/>
</div> </div>
<div className="dataDiv"> <div className="dataDiv">
<p>Stick Distance: </p> <p>Stick Distance: </p>
<input id="stickDistanceInput" type="number" value={stickDistance.toString()} min="0" step="1" onChange={e => { <input id="stickDistanceInput" type="number" defaultValue={stickDistance.toString()} min="0" step="1" onChange={e => {
updateSettings({stickDistance:parseInt(e.target.value)}); if(e.target.value.trim().length !== 0) setStickDistance(parseInt(e.target.value));
setStickDistance(settingList.stickDistance);
}}/> }}/>
</div> </div>
<div className="dataDiv"> <div className="dataDiv">
<p>Stick Mode:</p> <p>Stick Mode:</p>
<label htmlFor="stickMode" className="toggle-switchy" data-style="rounded" data-text="12"> <label htmlFor="stickMode" className="toggle-switchy" data-style="rounded" data-text="12">
<input checked={stickMode2} type="checkbox" id="stickMode" onChange={e => { <input defaultChecked={stickMode2} type="checkbox" id="stickMode" onChange={e => {
updateSettings({stickMode2:e.target.checked}); setStickMode2(e.target.checked);
setStickMode2(settingList.stickMode2);
}}/> }}/>
<span className="toggle"> <span className="toggle">
<span className="switch"></span> <span className="switch"></span>
@@ -51,8 +82,26 @@ function SettingsSide() {
setStickDistance(settingList.stickDistance); setStickDistance(settingList.stickDistance);
setStickMode2(settingList.stickMode2); setStickMode2(settingList.stickMode2);
}}>Reset Settings</button> }}>Reset Settings</button>
<div id="renderImgDiv">
<img id="render-ex" src={renderImg}></img>
{renderLoading? <RenderLoadingSpinner/> : null}
</div>
</div> </div>
) )
} }
function imageLoading() {
if(sideLoaded) setRenderLoading(true);
}
function imageLoaded() {
if(sideLoaded) {
setRenderImg(picturePath());
setRenderLoading(false);
}
}
export default SettingsSide; export default SettingsSide;
export {
imageLoading,
imageLoaded
};

View File

@@ -36,6 +36,8 @@ header {
} }
header h1 { header h1 {
margin-right: auto; margin-right: auto;
display: flex;
width: auto;
} }
#settings-back { #settings-back {
@@ -90,7 +92,6 @@ header h1 {
border: none; border: none;
outline: none; outline: none;
margin-right: 15px; margin-right: 15px;
display: none;
} }
#update-available svg { #update-available svg {
width: 35px; width: 35px;
@@ -102,3 +103,90 @@ header h1 {
#update-available:hover { #update-available:hover {
transform: scale(1.05); transform: scale(1.05);
} }
#blender-info {
margin-right: auto;
}
#blender-info p {
margin-top: 5px;
margin-bottom: 0;
}
#blender-icon {
width: 25px;
height: 25px;
align-items: center;
margin-left: auto;
margin-right: auto;
}
#blender-icon 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%;
}

View File

@@ -7,6 +7,13 @@
<body style="background-color: #172336;margin:0;padding:0;color:white;font-family:sans-serif;"> <body style="background-color: #172336;margin:0;padding:0;color:white;font-family:sans-serif;">
<div id="root"></div> <div id="root"></div>
<script src="./.webpack/renderer.js"></script> <script>
if(process.env.NODE_ENV === 'development') {
const port = process.env.PORT || 3000;
document.write('<script src="http://localhost:'+port+'/renderer.js"><\/script>');
} else {
document.write('<script src="./.webpack/renderer.js"><\/script>');
}
</script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
import {app, BrowserWindow} from 'electron'; import {app, BrowserWindow} from 'electron';
import {initialize as remoteInitialize, enable as remoteEnable} from '@electron/remote/main'; import {initialize as remoteInitialize, enable as remoteEnable} from '@electron/remote/main';
import path from 'path';
// Handle creating/removing shortcuts on Windows when installing/uninstalling. // Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { if (require('electron-squirrel-startup')) {
@@ -14,20 +15,21 @@ const createWindow = () => {
height: 600, height: 600,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false contextIsolation: false,
} }
}); });
if(app.isPackaged) mainWindow.setMenu(null); // remove the menu bar when in production.
if(process.env.NODE_ENV === 'production') mainWindow.setMenu(null);
remoteInitialize(); remoteInitialize();
remoteEnable(mainWindow.webContents); remoteEnable(mainWindow.webContents);
// and load the index.html of the app. // load the index.html of the app.
mainWindow.loadFile("src/index.html"); mainWindow.loadFile(path.join(__dirname, 'index.html'));
// Open the DevTools. // Open the DevTools when in development mode.
if(!app.isPackaged) mainWindow.webContents.openDevTools(); if(process.env.NODE_ENV === 'development') mainWindow.webContents.openDevTools();
}; };
// This method will be called when Electron has finished // This method will be called when Electron has finished

View File

@@ -1,30 +1,32 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom/client";
import Menu from "./components/ui/menu"; import Menu from "./components/ui/menu";
import MainSide from "./components/ui/mainSide"; import MainSide from "./components/ui/mainSide";
import SettingsSite from "./components/ui/settingsSide"; import SettingsSite from "./components/ui/settingsSide";
import "./index.css"; import "./index.css";
import "./toggle-switchy.css"; import "./toggle-switchy.css";
import { startBlender } from "./components/blender-controller";
enum Side { enum Side {
Main, Main,
Settings Settings
} }
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
function openSide(side:Side) { function openSide(side:Side) {
ReactDOM.render( root.render(
<React.StrictMode> <React.StrictMode>
<Menu updateAvailable={true} side={side}/> <Menu updateAvailable={false} side={side}/>
{(side == Side.Main)? <MainSide/> : <SettingsSite/>} {(side == Side.Main)? <MainSide/> : <SettingsSite/>}
</React.StrictMode>, </React.StrictMode>
document.getElementById('root')); );
} }
openSide(Side.Main); openSide(Side.Main);
startBlender();
export { export {
openSide, openSide,
Side, Side
} }