From 7b6dbf21c2da9384be48dadd64cc8d11fe596e7b Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 18 Jan 2025 03:13:15 -0500 Subject: [PATCH 01/10] fix opening files on macOS --- src-tauri/src/lib.rs | 163 ++++++++++++++++++++++--------------------- src/main.js | 5 ++ 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eeceb59..e2b1282 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -126,87 +126,94 @@ fn handle_file_associations(app: AppHandle, files: Vec) { pub fn run() { let pkg_name = env!("CARGO_PKG_NAME").to_string(); tauri::Builder::default() - .setup(|app| { - { - app.manage(Mutex::new(AppState::default())); - } - #[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS - { - let mut files = Vec::new(); + .manage(Mutex::new(AppState::default())) + .setup(|app| { + #[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS + { + let mut files = Vec::new(); - // NOTICE: `args` may include URL protocol (`your-app-protocol://`) - // or arguments (`--`) if your app supports them. - // files may aslo be passed as `file://path/to/file` - for maybe_file in std::env::args().skip(1) { - // skip flags like -f or --flag - if maybe_file.starts_with('-') { - 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)) - } + // NOTICE: `args` may include URL protocol (`your-app-protocol://`) + // or arguments (`--`) if your app supports them. + // files may also be passed as `file://path/to/file` + for maybe_file in std::env::args().skip(1) { + // skip flags like -f or --flag + if maybe_file.starts_with('-') { + continue; } - 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 files = urls - .into_iter() - .filter_map(|url| url.to_file_path().ok()) - .collect::>(); - - handle_file_associations(app.clone(), files); + // 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); + } + #[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::>(); + + 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(); } diff --git a/src/main.js b/src/main.js index f4678c4..3c20f13 100644 --- a/src/main.js +++ b/src/main.js @@ -7896,6 +7896,11 @@ async function renderMenu() { enabled: false, action: () => {}, }, + { + text: "Close Window", + enabled: true, + action: quit, + }, { text: "Quit Lightningbeam", enabled: true, From 666db06b781e9a02992d7d4ed9f3501a2ebdb149 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 18 Jan 2025 03:17:54 -0500 Subject: [PATCH 02/10] Fix new file dialog --- src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 3c20f13..3723fa2 100644 --- a/src/main.js +++ b/src/main.js @@ -6729,7 +6729,7 @@ function outliner(object = undefined) { async function startup() { await loadConfig(); createNewFileDialog(_newFile, _open, config); - if (!window.openedFiles) { + if (!window.openedFiles?.length) { showNewFileDialog(config); } } From 268790943f1a3680ac2e87303b26e12294169b07 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 18 Jan 2025 03:32:53 -0500 Subject: [PATCH 03/10] Add option to reopen file from last session --- src/main.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main.js b/src/main.js index 3723fa2..ee65f89 100644 --- a/src/main.js +++ b/src/main.js @@ -379,6 +379,7 @@ let config = { recentFiles: [], scrollSpeed: 1, debug: false, + reopenLastSession: false }; function getShortcut(shortcut) { @@ -421,13 +422,8 @@ async function saveConfig() { } async function addRecentFile(filePath) { - if (!config.recentFiles.includes(filePath)) { - config.recentFiles.unshift(filePath); - if (config.recentFiles.length > 10) { - config.recentFiles = config.recentFiles.slice(0, 10); - } - await saveConfig(config); - } + config.recentFiles = [filePath, ...config.recentFiles.filter(file => file !== filePath)].slice(0, 10); + await saveConfig(config); } // Pointers to all objects @@ -6730,7 +6726,11 @@ async function startup() { await loadConfig(); createNewFileDialog(_newFile, _open, config); if (!window.openedFiles?.length) { - showNewFileDialog(config); + if (config.reopenLastSession && config.recentFiles?.length) { + _open(config.recentFiles[0]) + } else { + showNewFileDialog(config); + } } } From babd2ebbdcb1ee047ef2c53144fedb15aa4ea62e Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 18 Jan 2025 03:44:00 -0500 Subject: [PATCH 04/10] remove text/plain MIME type --- src-tauri/tauri.conf.json | 3 +-- src/main.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6905ad8..022b1f8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -51,8 +51,7 @@ { "ext": [ "beam" - ], - "mimeType": "text/plain" + ] } ] } diff --git a/src/main.js b/src/main.js index ee65f89..9d14ab6 100644 --- a/src/main.js +++ b/src/main.js @@ -350,7 +350,6 @@ let context = { let config = { shortcuts: { playAnimation: " ", - // undo: "+z" undo: "z", redo: "Z", new: "n", From ad1da5b349b4041ef7ced028b94822392e5922e7 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 18 Jan 2025 03:53:36 -0500 Subject: [PATCH 05/10] replace it with application/lightningbeam --- src-tauri/tauri.conf.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 022b1f8..6294688 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -51,7 +51,8 @@ { "ext": [ "beam" - ] + ], + "mimeType": "application/lightningbeam" } ] } From c79476e7a9ab66ce8da82a867bce0d6fac0d264d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 23 Jan 2025 04:55:18 -0500 Subject: [PATCH 06/10] Cache icon rendering to improve performance --- src/icon.js | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/icon.js b/src/icon.js index 6f49db5..1dd5890 100644 --- a/src/icon.js +++ b/src/icon.js @@ -3,28 +3,43 @@ class Icon { this.url = url; this.svgContent = ''; this.svgLoaded = false; - this.load() + this.cacheCanvas = null; // Offscreen canvas for caching + this.cacheContext = null; + this.cacheColor = null; + this.load(); } async load() { - return fetch(this.url) - .then(response => response.text()) - .then(svgContent => { - this.svgContent = svgContent; - this.svgLoaded = true; - }) - .catch(error => { - console.error('Error loading SVG:', error); - this.svgLoaded = false; - }); + try { + const response = await fetch(this.url); + this.svgContent = await response.text(); + this.svgLoaded = true; + } catch (error) { + console.error('Error loading SVG:', error); + this.svgLoaded = false; + } } render(ctx, x, y, w, h, color) { - if (this.svgLoaded) { - ctx.fillStyle = color - ctx.drawSvg(this.svgContent, x, y, w, h) + if (!this.svgLoaded) return; // Do nothing if SVG is not loaded + + 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 }; \ No newline at end of file +export { Icon }; From 2de03ff7f74631253e920aef275d9243bc47e721 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 23 Jan 2025 04:55:46 -0500 Subject: [PATCH 07/10] restore finishEncoding function --- src/main.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 9d14ab6..fdc5b64 100644 --- a/src/main.js +++ b/src/main.js @@ -5064,6 +5064,19 @@ async function setupVideoExport(ext, path, canvas, exportContext) { // 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) => { if (currentFrame < root.maxFrame) { // Update progress bar @@ -5966,7 +5979,7 @@ function stage() { for (let child of context.selection) { if (!context.activeObject.currentFrame) 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 += mouse.x - context.lastMouse.x; context.activeObject.currentFrame.keys[child.idx].y += From f88c1f1408c010a3287d3f798d1ffdb8fc18900f Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 23 Jan 2025 05:09:49 -0500 Subject: [PATCH 08/10] Cache line highlight pattern to improve performance --- Changelog.md | 1 + src/main.js | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Changelog.md b/Changelog.md index bfb00c9..8e36f35 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,7 @@ New features: - Add "New Window" command - Enable files to be opened with Lightningbeam + Bugfixes: - Fix error when an object is deleted from a frame - Fix parent references being lost diff --git a/src/main.js b/src/main.js index fdc5b64..ba3e38c 100644 --- a/src/main.js +++ b/src/main.js @@ -2960,20 +2960,22 @@ class BaseShape { ctx.lineCap = "round"; // Create a repeating pattern for indicating selected shapes - let patternCanvas = document.createElement('canvas'); - patternCanvas.width = 2; - patternCanvas.height = 2; - let patternCtx = patternCanvas.getContext('2d'); - // Draw the pattern: - // black, transparent, - // transparent, white - patternCtx.fillStyle = 'black'; - patternCtx.fillRect(0, 0, 1, 1); - patternCtx.clearRect(1, 0, 1, 1); - patternCtx.clearRect(0, 1, 1, 1); - patternCtx.fillStyle = 'white'; - patternCtx.fillRect(1, 1, 1, 1); - let pattern = ctx.createPattern(patternCanvas, 'repeat'); // repeat the pattern across the canvas + if (!this.patternCanvas) { + this.patternCanvas = document.createElement('canvas'); + this.patternCanvas.width = 2; + this.patternCanvas.height = 2; + let patternCtx = this.patternCanvas.getContext('2d'); + // Draw the pattern: + // black, transparent, + // transparent, white + patternCtx.fillStyle = 'black'; + patternCtx.fillRect(0, 0, 1, 1); + patternCtx.clearRect(1, 0, 1, 1); + patternCtx.clearRect(0, 1, 1, 1); + patternCtx.fillStyle = 'white'; + 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) { // // if (region.filled) continue; From 139aedd5de7289f57ba21ecf8d01bbb63cc5e083 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 23 Jan 2025 05:23:26 -0500 Subject: [PATCH 09/10] fix a few errors --- src/main.js | 53 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/main.js b/src/main.js index ba3e38c..ef475c0 100644 --- a/src/main.js +++ b/src/main.js @@ -93,7 +93,7 @@ function forwardConsole(fnName, dest) { const stackLines = error.stack.split("\n"); 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") { // Send the full stack trace for errors @@ -1322,12 +1322,14 @@ let actions = { for (let frameObj of action.selectedFrames) { let layer = object.layers[frameObj.layer]; let frame = layer.frames[frameObj.frameNum]; - frameBuffer.push({ - frame: frame, - frameNum: frameObj.frameNum, - layer: frameObj.layer, - }); - layer.deleteFrame(frame.idx, undefined, frameObj.replacementUuid); + if (frameObj) { + frameBuffer.push({ + frame: frame, + frameNum: frameObj.frameNum, + layer: frameObj.layer, + }); + layer.deleteFrame(frame.idx, undefined, frameObj.replacementUuid); + } } for (let frameObj of frameBuffer) { const layer_idx = frameObj.layer + action.offset.layers; @@ -1443,6 +1445,7 @@ let actions = { let serializableShapes = []; let serializableObjects = []; let bbox; + const frame = context.activeObject.currentFrame for (let shape of context.shapeselection) { serializableShapes.push(shape.idx); if (bbox == undefined) { @@ -1452,12 +1455,14 @@ let actions = { } } for (let object of context.selection) { - serializableObjects.push(object.idx); - // TODO: rotated bbox - if (bbox == undefined) { - bbox = object.bbox(); - } else { - growBoundingBox(bbox, object.bbox()); + if (object.idx in frame.keys) { + serializableObjects.push(object.idx); + // TODO: rotated bbox + if (bbox == undefined) { + bbox = object.bbox(); + } else { + growBoundingBox(bbox, object.bbox()); + } } } context.shapeselection = []; @@ -1467,7 +1472,7 @@ let actions = { objects: serializableObjects, groupUuid: uuidv4(), parent: context.activeObject.idx, - frame: context.activeObject.currentFrame.idx, + frame: frame.idx, layer: context.activeObject.activeLayer.idx, position: { x: (bbox.x.min + bbox.x.max) / 2, @@ -2632,14 +2637,14 @@ class Layer extends Widget { ctx.setTransform(transform) } } - } - if (this.activeShape) { - this.activeShape.draw(cxt) - } - if (context.activeCurve) { - if (frame.shapes.indexOf(context.activeCurve.shape) != -1) { - cxt.selected = true + if (this.activeShape) { + this.activeShape.draw(cxt) + } + if (context.activeCurve) { + if (frame.shapes.indexOf(context.activeCurve.shape) != -1) { + cxt.selected = true + } } } } @@ -4111,7 +4116,9 @@ class GraphicsObject extends Widget { for (let layer of this.layers) { layer.children = layer.children.filter(child => child.idx !== idx); for (let frame of layer.frames) { - delete frame[idx]; + if (frame) { + delete frame[idx]; + } } } // this.children.splice(this.children.indexOf(childObject), 1); @@ -8368,4 +8375,4 @@ if (window.openedFiles?.length>0) { for (let i=1; i Date: Thu, 23 Jan 2025 05:28:04 -0500 Subject: [PATCH 10/10] Bump version to 0.7.13-alpha --- Changelog.md | 9 +++++++++ src-tauri/tauri.conf.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 8e36f35..58497b8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,12 @@ +# 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: New features: - Add "New Window" command diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6294688..e564eb6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Lightningbeam", - "version": "0.7.12-alpha", + "version": "0.7.13-alpha", "identifier": "org.lightningbeam.core", "build": { "frontendDist": "../src"