Release 0.7.13-alpha

This commit is contained in:
Skyler Lehmkuhl 2025-01-23 05:28:04 -05:00
commit 42bdab988d
5 changed files with 201 additions and 143 deletions

View File

@ -1,7 +1,17 @@
# 0.7.13-alpha:
Changes:
- changed file MIME type from text/plain to application/lightningbeam to prevent editor woes on Linux
Bugfixes:
- Port several live fixes to version control
- Fix opening files on macOS
- Improve rendering speed by 10x or more when multiple layers are present
# 0.7.12-alpha: # 0.7.12-alpha:
New features: New features:
- Add "New Window" command - Add "New Window" command
- Enable files to be opened with Lightningbeam - Enable files to be opened with Lightningbeam
Bugfixes: Bugfixes:
- Fix error when an object is deleted from a frame - Fix error when an object is deleted from a frame
- Fix parent references being lost - Fix parent references being lost

View File

@ -126,87 +126,94 @@ fn handle_file_associations(app: AppHandle, files: Vec<PathBuf>) {
pub fn run() { pub fn run() {
let pkg_name = env!("CARGO_PKG_NAME").to_string(); let pkg_name = env!("CARGO_PKG_NAME").to_string();
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .manage(Mutex::new(AppState::default()))
{ .setup(|app| {
app.manage(Mutex::new(AppState::default())); #[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS
} {
#[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS let mut files = Vec::new();
{
let mut files = Vec::new();
// NOTICE: `args` may include URL protocol (`your-app-protocol://`) // NOTICE: `args` may include URL protocol (`your-app-protocol://`)
// or arguments (`--`) if your app supports them. // or arguments (`--`) if your app supports them.
// files may aslo be passed as `file://path/to/file` // files may also be passed as `file://path/to/file`
for maybe_file in std::env::args().skip(1) { for maybe_file in std::env::args().skip(1) {
// skip flags like -f or --flag // skip flags like -f or --flag
if maybe_file.starts_with('-') { if maybe_file.starts_with('-') {
continue; continue;
}
// handle `file://` path urls and skip other urls
if let Ok(url) = Url::parse(&maybe_file) {
// if let Ok(url) = url::Url::parse(&maybe_file) {
if let Ok(path) = url.to_file_path() {
files.push(path);
}
} else {
files.push(PathBuf::from(maybe_file))
}
} }
handle_file_associations(app.handle().clone(), files); // handle `file://` path urls and skip other urls
} if let Ok(url) = Url::parse(&maybe_file) {
#[cfg(debug_assertions)] // only include this code on debug builds // if let Ok(url) = url::Url::parse(&maybe_file) {
{ if let Ok(path) = url.to_file_path() {
let window = app.get_webview_window("main").unwrap(); files.push(path);
window.open_devtools(); }
window.close_devtools(); } else {
} files.push(PathBuf::from(maybe_file))
Ok(())
})
.plugin(
tauri_plugin_log::Builder::new()
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
.format(|out, message, record| {
let date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
out.finish(format_args!(
"{}[{}] {}",
date,
record.level(),
message
))
})
.targets([
Target::new(TargetKind::Stdout),
// LogDir locations:
// Linux: /home/user/.local/share/org.lightningbeam.core/logs
// macOS: /Users/user/Library/Logs/org.lightningbeam.core/logs
// Windows: C:\Users\user\AppData\Local\org.lightningbeam.core\logs
Target::new(TargetKind::LogDir { file_name: Some("logs".to_string()) }),
Target::new(TargetKind::Webview),
])
.build()
)
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet, trace, debug, info, warn, error, create_window])
// .manage(window_counter)
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(
#[allow(unused_variables)]
|app, event| {
#[cfg(any(target_os = "macos", target_os = "ios"))]
if let tauri::RunEvent::Opened { urls } = event {
let files = urls
.into_iter()
.filter_map(|url| url.to_file_path().ok())
.collect::<Vec<_>>();
handle_file_associations(app.clone(), files);
} }
}, }
);
handle_file_associations(app.handle().clone(), files);
}
#[cfg(debug_assertions)] // only include this code on debug builds
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
window.close_devtools();
}
Ok(())
})
.plugin(
tauri_plugin_log::Builder::new()
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
.format(|out, message, record| {
let date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
out.finish(format_args!(
"{}[{}] {}",
date,
record.level(),
message
))
})
.targets([
Target::new(TargetKind::Stdout),
// LogDir locations:
// Linux: /home/user/.local/share/org.lightningbeam.core/logs
// macOS: /Users/user/Library/Logs/org.lightningbeam.core/logs
// Windows: C:\Users\user\AppData\Local\org.lightningbeam.core\logs
Target::new(TargetKind::LogDir { file_name: Some("logs".to_string()) }),
Target::new(TargetKind::Webview),
])
.build()
)
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet, trace, debug, info, warn, error, create_window])
// .manage(window_counter)
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(
#[allow(unused_variables)]
|app, event| {
#[cfg(any(target_os = "macos", target_os = "ios"))]
if let tauri::RunEvent::Opened { urls } = event {
let app = app.clone();
let files = urls
.into_iter()
.filter_map(|url| url.to_file_path().ok())
.map(|f| {
let file = f.to_string_lossy().replace('\\', "\\\\"); // escape backslash
format!("\"{file}\"",) // wrap in quotes for JS array
})
.collect::<Vec<_>>();
tauri::async_runtime::spawn(async move {
for path in files {
create_window(app.clone(), Some(path)).await;
}
});
}
},
);
tracing_subscriber::fmt().with_env_filter(EnvFilter::new(format!("{}=trace", pkg_name))).init(); tracing_subscriber::fmt().with_env_filter(EnvFilter::new(format!("{}=trace", pkg_name))).init();
} }

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Lightningbeam", "productName": "Lightningbeam",
"version": "0.7.12-alpha", "version": "0.7.13-alpha",
"identifier": "org.lightningbeam.core", "identifier": "org.lightningbeam.core",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
@ -52,7 +52,7 @@
"ext": [ "ext": [
"beam" "beam"
], ],
"mimeType": "text/plain" "mimeType": "application/lightningbeam"
} }
] ]
} }

View File

@ -3,28 +3,43 @@ class Icon {
this.url = url; this.url = url;
this.svgContent = ''; this.svgContent = '';
this.svgLoaded = false; this.svgLoaded = false;
this.load() this.cacheCanvas = null; // Offscreen canvas for caching
this.cacheContext = null;
this.cacheColor = null;
this.load();
} }
async load() { async load() {
return fetch(this.url) try {
.then(response => response.text()) const response = await fetch(this.url);
.then(svgContent => { this.svgContent = await response.text();
this.svgContent = svgContent; this.svgLoaded = true;
this.svgLoaded = true; } catch (error) {
}) console.error('Error loading SVG:', error);
.catch(error => { this.svgLoaded = false;
console.error('Error loading SVG:', error); }
this.svgLoaded = false;
});
} }
render(ctx, x, y, w, h, color) { render(ctx, x, y, w, h, color) {
if (this.svgLoaded) { if (!this.svgLoaded) return; // Do nothing if SVG is not loaded
ctx.fillStyle = color
ctx.drawSvg(this.svgContent, x, y, w, h) if (!this.cacheCanvas || this.cacheCanvas.width !== w || this.cacheCanvas.height !== h || this.cacheColor !== color) {
// Create or update the offscreen canvas for caching
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = w;
this.cacheCanvas.height = h;
this.cacheContext = this.cacheCanvas.getContext('2d');
// Draw the SVG onto the offscreen canvas
this.cacheContext.clearRect(0, 0, w, h); // Clear previous contents
this.cacheContext.fillStyle = color;
this.cacheColor = color
this.cacheContext.drawSvg(this.svgContent, 0, 0, w, h);
} }
// Draw the cached result onto the main canvas
ctx.drawImage(this.cacheCanvas, x, y);
} }
} }
export { Icon }; export { Icon };

View File

@ -93,7 +93,7 @@ function forwardConsole(fnName, dest) {
const stackLines = error.stack.split("\n"); const stackLines = error.stack.split("\n");
let message = args.join(" "); // Join all arguments into a single string let message = args.join(" "); // Join all arguments into a single string
const location = stackLines[1].match(/([a-zA-Z0-9_-]+\.js:\d+)/); const location = stackLines.length>1 ? stackLines[1].match(/([a-zA-Z0-9_-]+\.js:\d+)/) : stackLines.toString();
if (fnName === "error") { if (fnName === "error") {
// Send the full stack trace for errors // Send the full stack trace for errors
@ -350,7 +350,6 @@ let context = {
let config = { let config = {
shortcuts: { shortcuts: {
playAnimation: " ", playAnimation: " ",
// undo: "<ctrl>+z"
undo: "<mod>z", undo: "<mod>z",
redo: "<mod>Z", redo: "<mod>Z",
new: "<mod>n", new: "<mod>n",
@ -379,6 +378,7 @@ let config = {
recentFiles: [], recentFiles: [],
scrollSpeed: 1, scrollSpeed: 1,
debug: false, debug: false,
reopenLastSession: false
}; };
function getShortcut(shortcut) { function getShortcut(shortcut) {
@ -421,13 +421,8 @@ async function saveConfig() {
} }
async function addRecentFile(filePath) { async function addRecentFile(filePath) {
if (!config.recentFiles.includes(filePath)) { config.recentFiles = [filePath, ...config.recentFiles.filter(file => file !== filePath)].slice(0, 10);
config.recentFiles.unshift(filePath); await saveConfig(config);
if (config.recentFiles.length > 10) {
config.recentFiles = config.recentFiles.slice(0, 10);
}
await saveConfig(config);
}
} }
// Pointers to all objects // Pointers to all objects
@ -1327,12 +1322,14 @@ let actions = {
for (let frameObj of action.selectedFrames) { for (let frameObj of action.selectedFrames) {
let layer = object.layers[frameObj.layer]; let layer = object.layers[frameObj.layer];
let frame = layer.frames[frameObj.frameNum]; let frame = layer.frames[frameObj.frameNum];
frameBuffer.push({ if (frameObj) {
frame: frame, frameBuffer.push({
frameNum: frameObj.frameNum, frame: frame,
layer: frameObj.layer, frameNum: frameObj.frameNum,
}); layer: frameObj.layer,
layer.deleteFrame(frame.idx, undefined, frameObj.replacementUuid); });
layer.deleteFrame(frame.idx, undefined, frameObj.replacementUuid);
}
} }
for (let frameObj of frameBuffer) { for (let frameObj of frameBuffer) {
const layer_idx = frameObj.layer + action.offset.layers; const layer_idx = frameObj.layer + action.offset.layers;
@ -1448,6 +1445,7 @@ let actions = {
let serializableShapes = []; let serializableShapes = [];
let serializableObjects = []; let serializableObjects = [];
let bbox; let bbox;
const frame = context.activeObject.currentFrame
for (let shape of context.shapeselection) { for (let shape of context.shapeselection) {
serializableShapes.push(shape.idx); serializableShapes.push(shape.idx);
if (bbox == undefined) { if (bbox == undefined) {
@ -1457,12 +1455,14 @@ let actions = {
} }
} }
for (let object of context.selection) { for (let object of context.selection) {
serializableObjects.push(object.idx); if (object.idx in frame.keys) {
// TODO: rotated bbox serializableObjects.push(object.idx);
if (bbox == undefined) { // TODO: rotated bbox
bbox = object.bbox(); if (bbox == undefined) {
} else { bbox = object.bbox();
growBoundingBox(bbox, object.bbox()); } else {
growBoundingBox(bbox, object.bbox());
}
} }
} }
context.shapeselection = []; context.shapeselection = [];
@ -1472,7 +1472,7 @@ let actions = {
objects: serializableObjects, objects: serializableObjects,
groupUuid: uuidv4(), groupUuid: uuidv4(),
parent: context.activeObject.idx, parent: context.activeObject.idx,
frame: context.activeObject.currentFrame.idx, frame: frame.idx,
layer: context.activeObject.activeLayer.idx, layer: context.activeObject.activeLayer.idx,
position: { position: {
x: (bbox.x.min + bbox.x.max) / 2, x: (bbox.x.min + bbox.x.max) / 2,
@ -2637,14 +2637,14 @@ class Layer extends Widget {
ctx.setTransform(transform) ctx.setTransform(transform)
} }
} }
} if (this.activeShape) {
if (this.activeShape) { this.activeShape.draw(cxt)
this.activeShape.draw(cxt) }
} if (context.activeCurve) {
if (context.activeCurve) { if (frame.shapes.indexOf(context.activeCurve.shape) != -1) {
if (frame.shapes.indexOf(context.activeCurve.shape) != -1) { cxt.selected = true
cxt.selected = true
}
} }
} }
} }
@ -2965,20 +2965,22 @@ class BaseShape {
ctx.lineCap = "round"; ctx.lineCap = "round";
// Create a repeating pattern for indicating selected shapes // Create a repeating pattern for indicating selected shapes
let patternCanvas = document.createElement('canvas'); if (!this.patternCanvas) {
patternCanvas.width = 2; this.patternCanvas = document.createElement('canvas');
patternCanvas.height = 2; this.patternCanvas.width = 2;
let patternCtx = patternCanvas.getContext('2d'); this.patternCanvas.height = 2;
// Draw the pattern: let patternCtx = this.patternCanvas.getContext('2d');
// black, transparent, // Draw the pattern:
// transparent, white // black, transparent,
patternCtx.fillStyle = 'black'; // transparent, white
patternCtx.fillRect(0, 0, 1, 1); patternCtx.fillStyle = 'black';
patternCtx.clearRect(1, 0, 1, 1); patternCtx.fillRect(0, 0, 1, 1);
patternCtx.clearRect(0, 1, 1, 1); patternCtx.clearRect(1, 0, 1, 1);
patternCtx.fillStyle = 'white'; patternCtx.clearRect(0, 1, 1, 1);
patternCtx.fillRect(1, 1, 1, 1); patternCtx.fillStyle = 'white';
let pattern = ctx.createPattern(patternCanvas, 'repeat'); // repeat the pattern across the canvas patternCtx.fillRect(1, 1, 1, 1);
}
let pattern = ctx.createPattern(this.patternCanvas, 'repeat'); // repeat the pattern across the canvas
// for (let region of this.regions) { // for (let region of this.regions) {
// // if (region.filled) continue; // // if (region.filled) continue;
@ -4114,7 +4116,9 @@ class GraphicsObject extends Widget {
for (let layer of this.layers) { for (let layer of this.layers) {
layer.children = layer.children.filter(child => child.idx !== idx); layer.children = layer.children.filter(child => child.idx !== idx);
for (let frame of layer.frames) { for (let frame of layer.frames) {
delete frame[idx]; if (frame) {
delete frame[idx];
}
} }
} }
// this.children.splice(this.children.indexOf(childObject), 1); // this.children.splice(this.children.indexOf(childObject), 1);
@ -5069,6 +5073,19 @@ async function setupVideoExport(ext, path, canvas, exportContext) {
// audioEncoder.configure(audioConfig) // audioEncoder.configure(audioConfig)
async function finishEncoding() {
const progressText = document.getElementById('progressText');
progressText.innerText = 'Finalizing...';
const progressBar = document.getElementById('progressBar');
progressBar.value = 100;
await videoEncoder.flush();
muxer.finalize();
await writeFile(path, new Uint8Array(target.buffer));
const modal = document.getElementById('progressModal');
modal.style.display = 'none';
document.querySelector("body").style.cursor = "default";
}
const processFrame = async (currentFrame) => { const processFrame = async (currentFrame) => {
if (currentFrame < root.maxFrame) { if (currentFrame < root.maxFrame) {
// Update progress bar // Update progress bar
@ -5971,7 +5988,7 @@ function stage() {
for (let child of context.selection) { for (let child of context.selection) {
if (!context.activeObject.currentFrame) continue; if (!context.activeObject.currentFrame) continue;
if (!context.activeObject.currentFrame.keys) continue; if (!context.activeObject.currentFrame.keys) continue;
if (!child.idx in context.activeObject.currentFrame.keys) continue; if (!(child.idx in context.activeObject.currentFrame.keys)) continue;
context.activeObject.currentFrame.keys[child.idx].x += context.activeObject.currentFrame.keys[child.idx].x +=
mouse.x - context.lastMouse.x; mouse.x - context.lastMouse.x;
context.activeObject.currentFrame.keys[child.idx].y += context.activeObject.currentFrame.keys[child.idx].y +=
@ -6729,8 +6746,12 @@ function outliner(object = undefined) {
async function startup() { async function startup() {
await loadConfig(); await loadConfig();
createNewFileDialog(_newFile, _open, config); createNewFileDialog(_newFile, _open, config);
if (!window.openedFiles) { if (!window.openedFiles?.length) {
showNewFileDialog(config); if (config.reopenLastSession && config.recentFiles?.length) {
_open(config.recentFiles[0])
} else {
showNewFileDialog(config);
}
} }
} }
@ -7896,6 +7917,11 @@ async function renderMenu() {
enabled: false, enabled: false,
action: () => {}, action: () => {},
}, },
{
text: "Close Window",
enabled: true,
action: quit,
},
{ {
text: "Quit Lightningbeam", text: "Quit Lightningbeam",
enabled: true, enabled: true,
@ -8349,4 +8375,4 @@ if (window.openedFiles?.length>0) {
for (let i=1; i<window.openedFiles.length; i++) { for (let i=1; i<window.openedFiles.length; i++) {
newWindow(window.openedFiles[i]) newWindow(window.openedFiles[i])
} }
} }