diff --git a/Changelog.md b/Changelog.md index bfb00c9..58497b8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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: 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-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-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6905ad8..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" @@ -52,7 +52,7 @@ "ext": [ "beam" ], - "mimeType": "text/plain" + "mimeType": "application/lightningbeam" } ] } 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 }; diff --git a/src/main.js b/src/main.js index f4678c4..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 @@ -350,7 +350,6 @@ let context = { let config = { shortcuts: { playAnimation: " ", - // undo: "+z" undo: "z", redo: "Z", new: "n", @@ -379,6 +378,7 @@ let config = { recentFiles: [], scrollSpeed: 1, debug: false, + reopenLastSession: false }; function getShortcut(shortcut) { @@ -421,13 +421,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 @@ -1327,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; @@ -1448,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) { @@ -1457,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 = []; @@ -1472,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, @@ -2637,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 + } } } } @@ -2965,20 +2965,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; @@ -4114,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); @@ -5069,6 +5073,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 @@ -5971,7 +5988,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 += @@ -6729,8 +6746,12 @@ function outliner(object = undefined) { async function startup() { await loadConfig(); createNewFileDialog(_newFile, _open, config); - if (!window.openedFiles) { - showNewFileDialog(config); + if (!window.openedFiles?.length) { + if (config.reopenLastSession && config.recentFiles?.length) { + _open(config.recentFiles[0]) + } else { + showNewFileDialog(config); + } } } @@ -7896,6 +7917,11 @@ async function renderMenu() { enabled: false, action: () => {}, }, + { + text: "Close Window", + enabled: true, + action: quit, + }, { text: "Quit Lightningbeam", enabled: true, @@ -8349,4 +8375,4 @@ if (window.openedFiles?.length>0) { for (let i=1; i