diff --git a/effect/utils.py b/effect/utils.py index 624c17c..f6dbc06 100644 --- a/effect/utils.py +++ b/effect/utils.py @@ -1,13 +1,26 @@ import numpy as np -from qiskit import generate_preset_pass_manager -from qiskit.primitives import BackendEstimatorV2 as Estimator import os -from qiskit_aer import AerSimulator import colorsys from itertools import product -import matplotlib.pyplot as plt -from matplotlib.path import Path as plt_path -from scipy.ndimage import distance_transform_edt + +try: + from matplotlib.path import Path as plt_path +except ImportError: + plt_path = None + +try: + from scipy.ndimage import distance_transform_edt +except ImportError: + distance_transform_edt = None + +try: + from qiskit import generate_preset_pass_manager + from qiskit.primitives import BackendEstimatorV2 as Estimator + from qiskit_aer import AerSimulator +except ImportError: + generate_preset_pass_manager = None + Estimator = None + AerSimulator = None def svd(matrix=None,U=None,S=None,Vt=None): if U is not None: @@ -89,26 +102,47 @@ def points_within_radius(points, radius=10, border = None, return_distance = Fal # Create binary mask mask = np.zeros((height, width), dtype=bool) mask[shifted[:, 0], shifted[:, 1]] = True # y, x - - # Distance transform - dist = distance_transform_edt(~mask) - # Find pixels within offset - region_mask = dist <= radius - ys, xs = np.nonzero(region_mask) - - # Shift back to original coordinates - coords = np.stack([ys, xs], axis=1) + min_yx - #print(coords) + + if distance_transform_edt is not None: + # Distance transform + dist = distance_transform_edt(~mask) + # Find pixels within offset + region_mask = dist <= radius + ys, xs = np.nonzero(region_mask) + + # Shift back to original coordinates + coords = np.stack([ys, xs], axis=1) + min_yx + distances = dist[ys, xs] / radius + else: + line_points = np.argwhere(mask) + grid_y, grid_x = np.indices((height, width)) + grid = np.stack([grid_y.ravel(), grid_x.ravel()], axis=1) + + min_dist_sq = np.empty(grid.shape[0], dtype=np.float64) + chunk_size = 2048 + for start in range(0, grid.shape[0], chunk_size): + end = start + chunk_size + chunk = grid[start:end] + diff = chunk[:, None, :] - line_points[None, :, :] + dist_sq = np.sum(diff * diff, axis=2) + min_dist_sq[start:end] = dist_sq.min(axis=1) + + region_mask = min_dist_sq <= radius ** 2 + coords = grid[region_mask] + min_yx + distances = np.sqrt(min_dist_sq[region_mask]) / radius + if border is not None: coords = np.clip(coords, [0, 0], [border[0] - 1, border[1] - 1]) if return_distance: - distances = dist[ys, xs] / radius return coords, distances return coords def points_within_lasso(points,border = None): + if plt_path is None: + raise ImportError("matplotlib is required to use lasso paths.") + min_x = np.min(points[:,1]) max_x = np.max(points[:,1])+1 min_y = np.min(points[:,0]) @@ -152,6 +186,8 @@ def run_estimator(circuits, operators, backend=None, options = None): It can receive a single circuit or a list of circuits. It can receive a single operator or a list of operators or a list of list of operators (one for each circuit). ''' + if generate_preset_pass_manager is None or Estimator is None or AerSimulator is None: + raise ImportError("Qiskit and qiskit-aer are required to run estimator-based effects.") #iqm_server_url = "https://cocos.resonance.meetiqm.com/garnet:mock" # Replace this with the correct URL #provider = IQMProvider(iqm_server_url) @@ -352,4 +388,3 @@ def apply_patch_to_image(original_image: np.ndarray, new_patch: np.ndarray, blur new_patch[..., :3] = (1 - alpha) * original_float[..., :3] + alpha * new_patch[..., :3] return (new_patch * 255).astype(np.uint8) - \ No newline at end of file diff --git a/src/EffectManager.java b/src/EffectManager.java index 886286c..11ff09c 100644 --- a/src/EffectManager.java +++ b/src/EffectManager.java @@ -17,7 +17,7 @@ public void loadEffects() { // Clear existing effects to prevent contamination effects.clear(); - File effectsDir = new File("effect"); + File effectsDir = QuantumBrush.appFile("effect"); if (!effectsDir.exists() || !effectsDir.isDirectory()) { System.err.println("Error: effect directory not found or not a directory"); return; diff --git a/src/FileManager.java b/src/FileManager.java index b48be3f..58480b3 100644 --- a/src/FileManager.java +++ b/src/FileManager.java @@ -18,22 +18,23 @@ public FileManager(QuantumBrush app) { public void saveProject(String projectId, PImage originalImage) { String projectPath = "project/" + projectId; ensureDirectoryExists(projectPath); + File projectDir = QuantumBrush.appFile(projectPath); // Save original image if (originalImage != null) { - originalImage.save(projectPath + "/original.png"); + originalImage.save(new File(projectDir, "original.png").getAbsolutePath()); } // IMPORTANT: Also save the current state (with all effects applied) PImage currentImage = app.getCurrentImage(); if (currentImage != null) { - currentImage.save(projectPath + "/current.png"); + currentImage.save(new File(projectDir, "current.png").getAbsolutePath()); System.out.println("Saved current state to: " + projectPath + "/current.png"); } } public boolean ensureDirectoryExists(String path) { - File directory = new File(path); + File directory = QuantumBrush.appFile(path); if (!directory.exists()) { return directory.mkdirs(); } @@ -51,17 +52,17 @@ public void saveStroke( // Save instructions String instructionsPath = strokeDirPath + "/" + strokeId + "_instructions.json"; - app.saveJSONObject(instructions, instructionsPath); + app.saveJSONObject(instructions, QuantumBrush.appFile(instructionsPath).getAbsolutePath()); // Save input image if (inputImage != null) { String imagePath = strokeDirPath + "/" + strokeId + "_input.png"; - inputImage.save(imagePath); + inputImage.save(QuantumBrush.appFile(imagePath).getAbsolutePath()); } } public JSONObject loadEffectRequirements(String effectName) { - File requirementsFile = new File( + File requirementsFile = QuantumBrush.appFile( "effect/" + effectName + "/" + effectName + "_requirements.json" ); if (requirementsFile.exists()) { @@ -89,18 +90,18 @@ public void createProjectMetadata(String projectId, String projectName) { metadata.setLong("modified_time", currentTime); // Save metadata - app.saveJSONObject(metadata, "metadata/" + projectId + ".json"); + app.saveJSONObject(metadata, QuantumBrush.appFile("metadata/" + projectId + ".json").getAbsolutePath()); } public void updateProjectMetadata(String projectId) { String metadataPath = "metadata/" + projectId + ".json"; - File metadataFile = new File(metadataPath); + File metadataFile = QuantumBrush.appFile(metadataPath); if (metadataFile.exists()) { try { - JSONObject metadata = app.loadJSONObject(metadataPath); + JSONObject metadata = app.loadJSONObject(metadataFile.getAbsolutePath()); metadata.setLong("modified_time", System.currentTimeMillis()); - app.saveJSONObject(metadata, metadataPath); + app.saveJSONObject(metadata, metadataFile.getAbsolutePath()); } catch (Exception e) { System.err.println("Error updating metadata: " + e.getMessage()); } @@ -110,7 +111,7 @@ public void updateProjectMetadata(String projectId) { public ArrayList getProjectsMetadata() { ArrayList projects = new ArrayList<>(); - File metadataDir = new File("metadata"); + File metadataDir = QuantumBrush.appFile("metadata"); if (!metadataDir.exists()) { ensureDirectoryExists("metadata"); return projects; @@ -126,7 +127,7 @@ public ArrayList getProjectsMetadata() { // Check if the corresponding project directory exists JSONObject metadata = app.loadJSONObject(file.getAbsolutePath()); String projectId = metadata.getString("project_id", ""); - File projectDir = new File("project/" + projectId); + File projectDir = QuantumBrush.appFile("project/" + projectId); // Only add to the list if the project directory exists if (projectDir.exists() && projectDir.isDirectory()) { @@ -174,11 +175,11 @@ public ArrayList getProjectsMetadata() { public JSONObject getProjectMetadata(String projectId) { String metadataPath = "metadata/" + projectId + ".json"; - File metadataFile = new File(metadataPath); + File metadataFile = QuantumBrush.appFile(metadataPath); if (metadataFile.exists()) { try { - return app.loadJSONObject(metadataPath); + return app.loadJSONObject(metadataFile.getAbsolutePath()); } catch (Exception e) { System.err.println("Error loading metadata: " + e.getMessage()); } @@ -190,15 +191,15 @@ public JSONObject getProjectMetadata(String projectId) { // Add a method to load strokes when a project is loaded public boolean loadProject(String projectId) { String projectPath = "project/" + projectId; - File projectDir = new File(projectPath); + File projectDir = QuantumBrush.appFile(projectPath); if (!projectDir.exists()) { return false; } // Try to load current state first (with all effects applied) - File currentImageFile = new File(projectPath + "/current.png"); - File originalImageFile = new File(projectPath + "/original.png"); + File currentImageFile = QuantumBrush.appFile(projectPath + "/current.png"); + File originalImageFile = QuantumBrush.appFile(projectPath + "/original.png"); PImage loadedImage = null; @@ -246,13 +247,13 @@ public boolean deleteProject(String projectId) { try { // Delete project directory and all its contents - File projectDir = new File("project/" + projectId); + File projectDir = QuantumBrush.appFile("project/" + projectId); if (projectDir.exists()) { success &= deleteDirectory(projectDir); } // Delete metadata file - File metadataFile = new File("metadata/" + projectId + ".json"); + File metadataFile = QuantumBrush.appFile("metadata/" + projectId + ".json"); if (metadataFile.exists()) { success &= metadataFile.delete(); } @@ -294,7 +295,7 @@ private boolean deleteDirectory(File directory) { public int cleanupOrphanedMetadata() { int cleanedCount = 0; - File metadataDir = new File("metadata"); + File metadataDir = QuantumBrush.appFile("metadata"); if (!metadataDir.exists()) { return 0; } @@ -308,7 +309,7 @@ public int cleanupOrphanedMetadata() { try { JSONObject metadata = app.loadJSONObject(file.getAbsolutePath()); String projectId = metadata.getString("project_id", ""); - File projectDir = new File("project/" + projectId); + File projectDir = QuantumBrush.appFile("project/" + projectId); // Only delete if project directory truly doesn't exist if (!projectDir.exists()) { diff --git a/src/QuantumBrush.java b/src/QuantumBrush.java index 488dafe..d43cc15 100644 --- a/src/QuantumBrush.java +++ b/src/QuantumBrush.java @@ -6,8 +6,13 @@ import java.awt.event.*; import java.util.*; import java.io.*; +import java.net.URISyntaxException; +import java.nio.file.*; +import java.util.jar.*; public class QuantumBrush extends PApplet { + private static File appRootDirectory = new File(".").getAbsoluteFile(); + // Managers private CanvasManager canvas; private EffectManager effects; @@ -19,6 +24,9 @@ public class QuantumBrush extends PApplet { // UI components private JFrame controlFrame; private JFrame canvasFrame; + private Component processingCanvas; + private JSplitPane workspaceSplit; + private JPanel canvasPanel; private JMenuBar menuBar; private JComboBox effectsDropdown; private JButton createButton; @@ -42,6 +50,8 @@ public class QuantumBrush extends PApplet { private static final int CANVAS_HEIGHT = 600; private static final int MIN_CANVAS_WIDTH = 320; private static final int MIN_CANVAS_HEIGHT = 240; + private static final int CONTROL_PANEL_WIDTH = 420; + private static final int CONTROL_PANEL_MIN_WIDTH = 320; private static final float CANVAS_SCREEN_WIDTH_RATIO = 0.6f; private static final float CANVAS_SCREEN_HEIGHT_RATIO = 0.8f; @@ -55,8 +65,75 @@ public class QuantumBrush extends PApplet { private float strokeWeight = 2.0f; public static void main(String[] args) { + configureAppRoot(); + ensureBundledEffectsAvailable(); PApplet.main("QuantumBrush"); } + + public static File getAppRootDirectory() { + return appRootDirectory; + } + + public static File appFile(String path) { + File file = new File(path); + return file.isAbsolute() ? file : new File(appRootDirectory, path); + } + + private static void configureAppRoot() { + try { + File codeSource = new File( + QuantumBrush.class.getProtectionDomain().getCodeSource().getLocation().toURI() + ); + appRootDirectory = codeSource.isFile() + ? codeSource.getParentFile().getAbsoluteFile() + : new File(".").getAbsoluteFile(); + System.setProperty("user.dir", appRootDirectory.getAbsolutePath()); + } catch (URISyntaxException e) { + appRootDirectory = new File(".").getAbsoluteFile(); + } + } + + private static void ensureBundledEffectsAvailable() { + File effectDir = appFile("effect"); + if (effectDir.isDirectory()) { + return; + } + + try { + File codeSource = new File( + QuantumBrush.class.getProtectionDomain().getCodeSource().getLocation().toURI() + ); + if (!codeSource.isFile()) { + return; + } + + try (JarFile jar = new JarFile(codeSource)) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (!name.startsWith("effect/") || name.contains("__pycache__")) { + continue; + } + + File output = appFile(name); + if (entry.isDirectory()) { + output.mkdirs(); + } else { + File parent = output.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try (InputStream in = jar.getInputStream(entry)) { + Files.copy(in, output.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + } + } + } catch (Exception e) { + System.err.println("Could not extract bundled effects: " + e.getMessage()); + } + } public void settings() { size(CANVAS_WIDTH, CANVAS_HEIGHT); @@ -138,9 +215,10 @@ private void cleanupTempFiles() { private void setupUI() { // Get the JFrame from Processing for canvas PSurfaceAWT.SmoothCanvas smoothCanvas = (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative(); + processingCanvas = smoothCanvas; canvasFrame = (JFrame) smoothCanvas.getFrame(); - canvasFrame.setTitle("Quantum Brush - Canvas"); - canvasFrame.setMinimumSize(new Dimension(MIN_CANVAS_WIDTH, MIN_CANVAS_HEIGHT)); + canvasFrame.setTitle("Quantum Brush"); + canvasFrame.setMinimumSize(new Dimension(CONTROL_PANEL_MIN_WIDTH + MIN_CANVAS_WIDTH, MIN_CANVAS_HEIGHT)); surface.setResizable(true); canvasFrame.addWindowFocusListener(new WindowAdapter() { @Override @@ -149,11 +227,7 @@ public void windowGainedFocus(WindowEvent e) { } }); - // Position the canvas frame on the right side of the screen - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - canvasFrame.setLocation(screenSize.width/2, screenSize.height/4); - - // Create control frame + // Create a single workspace with controls and canvas in the Processing frame. createControlFrame(); // Add keyboard shortcuts for undo/redo and zoom/pan @@ -205,7 +279,7 @@ public boolean dispatchKeyEvent(KeyEvent e) { } }); - canvasFrame.setVisible(true); + fitWorkspaceToCanvasSize(CANVAS_WIDTH, CANVAS_HEIGHT); } // Zoom and Pan methods @@ -273,7 +347,7 @@ public void resizeCanvasToFitImage(PImage image) { zoomLevel = 1.0f; panX = 0; panY = 0; - keepCanvasFrameOnScreen(); + fitWorkspaceToCanvasSize(CANVAS_WIDTH, CANVAS_HEIGHT); return; } @@ -303,7 +377,7 @@ public void resizeCanvasToFitImage(PImage image) { ); surface.setSize(targetWidth, targetHeight); - keepCanvasFrameOnScreen(); + fitWorkspaceToCanvasSize(targetWidth, targetHeight); calculateInitialZoomAndPan(targetWidth, targetHeight); } @@ -318,7 +392,38 @@ private void keepCanvasFrameOnScreen() { int y = Math.max(0, Math.min(frameLocation.y, maxY)); canvasFrame.setLocation(x, y); } - + + private void fitWorkspaceToCanvasSize(int canvasWidth, int canvasHeight) { + if (canvasFrame == null || workspaceSplit == null) { + keepCanvasFrameOnScreen(); + return; + } + + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + Insets insets = canvasFrame.getInsets(); + int frameDecorationsWidth = insets.left + insets.right; + int frameDecorationsHeight = insets.top + insets.bottom; + int maxWidth = Math.max( + CONTROL_PANEL_MIN_WIDTH + MIN_CANVAS_WIDTH, + screenSize.width - 80 + ); + int maxHeight = Math.max(MIN_CANVAS_HEIGHT, screenSize.height - 80); + int targetWidth = Math.min( + maxWidth, + CONTROL_PANEL_WIDTH + Math.max(canvasWidth, MIN_CANVAS_WIDTH) + frameDecorationsWidth + 40 + ); + int targetHeight = Math.min( + maxHeight, + Math.max(canvasHeight, MIN_CANVAS_HEIGHT) + frameDecorationsHeight + 80 + ); + + canvasFrame.setSize(targetWidth, targetHeight); + workspaceSplit.setDividerLocation(Math.min(CONTROL_PANEL_WIDTH, targetWidth / 2)); + keepCanvasFrameOnScreen(); + canvasFrame.revalidate(); + canvasFrame.repaint(); + } + // Coordinate transformation methods private PVector screenToImage(float screenX, float screenY) { if (currentImage == null) return new PVector(screenX, screenY); @@ -348,17 +453,13 @@ private boolean isPointInImage(float screenX, float screenY) { } private void createControlFrame() { - // Create main control window - controlFrame = new JFrame("Quantum Brush - Control Panel"); + controlFrame = canvasFrame; + controlFrame.setTitle("Quantum Brush"); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - int controlWidth = Math.min(500, (int)(screenSize.width * 0.4)); + int controlWidth = Math.min(CONTROL_PANEL_WIDTH, Math.max(CONTROL_PANEL_MIN_WIDTH, (int)(screenSize.width * 0.35))); int controlHeight = Math.min(700, (int)(screenSize.height * 0.8)); - controlFrame.setSize(controlWidth, controlHeight); controlFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - // Position the control frame on the left side of the screen - controlFrame.setLocation(screenSize.width/4, screenSize.height/4); - // Set references for UI manager ui.setMainControlFrame(controlFrame); @@ -542,8 +643,28 @@ private void createControlFrame() { mainPanel.add(tabbedPane, BorderLayout.CENTER); mainPanel.add(zoomInfoPanel, BorderLayout.SOUTH); + mainPanel.setMinimumSize(new Dimension(CONTROL_PANEL_MIN_WIDTH, MIN_CANVAS_HEIGHT)); + mainPanel.setPreferredSize(new Dimension(controlWidth, controlHeight)); + + canvasPanel = new JPanel(new BorderLayout()); + canvasPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(10, 0, 10, 10), + BorderFactory.createTitledBorder("Canvas") + )); + canvasPanel.setMinimumSize(new Dimension(MIN_CANVAS_WIDTH, MIN_CANVAS_HEIGHT)); + canvasPanel.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT)); + canvasPanel.add(processingCanvas, BorderLayout.CENTER); - controlFrame.add(mainPanel); + workspaceSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, mainPanel, canvasPanel); + workspaceSplit.setContinuousLayout(true); + workspaceSplit.setResizeWeight(0.0); + workspaceSplit.setDividerLocation(controlWidth); + workspaceSplit.setBorder(BorderFactory.createEmptyBorder()); + + controlFrame.getContentPane().removeAll(); + controlFrame.getContentPane().setLayout(new BorderLayout()); + controlFrame.getContentPane().add(workspaceSplit, BorderLayout.CENTER); + fitWorkspaceToCanvasSize(CANVAS_WIDTH, CANVAS_HEIGHT); // Belt-and-braces: clear the in-memory IQM token if the user closes the // control window (the shutdown hook covers SIGTERM/exit() paths too). @@ -557,6 +678,11 @@ public void windowClosing(WindowEvent e) { }); controlFrame.setVisible(true); + fitWorkspaceToCanvasSize(CANVAS_WIDTH, CANVAS_HEIGHT); + controlFrame.setLocation( + Math.max(0, (screenSize.width - controlFrame.getWidth()) / 2), + Math.max(0, (screenSize.height - controlFrame.getHeight()) / 2) + ); } private void showLiveDebugViewer() { @@ -1054,9 +1180,9 @@ private void initializeProjectHistoryAfterLoad() { private void saveCurrentImageToDisk() { if (projectId != null && currentImage != null) { String projectPath = "project/" + projectId; - File projectDir = new File(projectPath); + File projectDir = appFile(projectPath); if (projectDir.exists()) { - currentImage.save(projectPath + "/current.png"); + currentImage.save(appFile(projectPath + "/current.png").getAbsolutePath()); System.out.println("Saved current image state to disk: project/" + projectId + "/current.png"); } } @@ -1149,16 +1275,41 @@ public void draw() { canvas.draw(zoomLevel, panX, panY); } else { // Show "no image" message + float placeholderX = getVisibleCanvasWidth() / 2.0f; + float placeholderY = getVisibleCanvasHeight() / 2.0f; + fill(200); textAlign(CENTER, CENTER); textSize(18); - text("Load an image or project to begin", width/2, height/2); + text("Load an image or project to begin", placeholderX, placeholderY); fill(150); textSize(14); - text("Use File > New to load an image", width/2, height/2 + 30); - text("Use keyboard shortcuts to zoom: Ctrl/Cmd + Plus/Minus", width/2, height/2 + 50); + text("Use File > New to load an image", placeholderX, placeholderY + 30); + text("Use keyboard shortcuts to zoom: Ctrl/Cmd + Plus/Minus", placeholderX, placeholderY + 50); + } + } + + private int getVisibleCanvasWidth() { + if (canvasPanel != null && canvasPanel.getWidth() > 0) { + Insets insets = canvasPanel.getInsets(); + return Math.max(1, canvasPanel.getWidth() - insets.left - insets.right); + } + if (processingCanvas != null && processingCanvas.getWidth() > 0) { + return processingCanvas.getWidth(); + } + return width; + } + + private int getVisibleCanvasHeight() { + if (canvasPanel != null && canvasPanel.getHeight() > 0) { + Insets insets = canvasPanel.getInsets(); + return Math.max(1, canvasPanel.getHeight() - insets.top - insets.bottom); + } + if (processingCanvas != null && processingCanvas.getHeight() > 0) { + return processingCanvas.getHeight(); } + return height; } // Mouse event handling with coordinate transformation diff --git a/src/StrokeManager.java b/src/StrokeManager.java index 6eabdab..ea1fc68 100644 --- a/src/StrokeManager.java +++ b/src/StrokeManager.java @@ -402,7 +402,7 @@ public String getDescription() { private String loadCustomPythonPath() { try { - File configFile = new File("config/python_path.txt"); + File configFile = QuantumBrush.appFile("config/python_path.txt"); if (configFile.exists()) { return new String(Files.readAllBytes(configFile.toPath())).trim(); } @@ -414,12 +414,12 @@ private String loadCustomPythonPath() { private void saveCustomPythonPath(String path) { try { - File configDir = new File("config"); + File configDir = QuantumBrush.appFile("config"); if (!configDir.exists()) { configDir.mkdirs(); } - File configFile = new File("config/python_path.txt"); + File configFile = QuantumBrush.appFile("config/python_path.txt"); Files.write(configFile.toPath(), path.getBytes()); System.out.println("Saved custom Python path: " + path); } catch (Exception e) { @@ -468,14 +468,14 @@ public String createStroke(Effect effect, Map parameters) { } // Create project directory structure - File projectDir = new File("project/" + projectId); + File projectDir = QuantumBrush.appFile("project/" + projectId); if (!projectDir.exists()) { projectDir.mkdirs(); // Save original image if (app.getCurrentImage() != null) { - String originalPath = projectDir.getPath() + "/original.png"; - app.getCurrentImage().save(originalPath); + File originalPath = new File(projectDir, "original.png"); + app.getCurrentImage().save(originalPath.getAbsolutePath()); } } @@ -488,7 +488,7 @@ public String createStroke(Effect effect, Map parameters) { // Generate and save JSON instructions String instructionsPath = strokeDir.getPath() + "/" + strokeId + "_instructions.json"; JSONObject instructions = stroke.generateJSON(projectId, app.getHardwareManager().snapshotForStroke()); - app.saveJSONObject(instructions, instructionsPath); + app.saveJSONObject(instructions, new File(instructionsPath).getAbsolutePath()); // DEBUG: Show the final JSON that was saved DebugLogger.log("\n=== FINAL SAVED JSON DEBUG ==="); @@ -509,10 +509,10 @@ public String createStroke(Effect effect, Map parameters) { // Save input image if (app.getCurrentImage() != null) { String inputPath = strokeDir.getPath() + "/" + strokeId + "_input.png"; - app.getCurrentImage().save(inputPath); + app.getCurrentImage().save(new File(inputPath).getAbsolutePath()); // Update JSON with image paths - instructions = app.loadJSONObject(instructionsPath); + instructions = app.loadJSONObject(new File(instructionsPath).getAbsolutePath()); JSONObject strokeInput = instructions.getJSONObject("stroke_input"); strokeInput.setString("input_location", inputPath); strokeInput.setString( @@ -520,7 +520,7 @@ public String createStroke(Effect effect, Map parameters) { strokeDir.getPath() + "/" + strokeId + "_output.png" ); instructions.setJSONObject("stroke_input", strokeInput); - app.saveJSONObject(instructions, instructionsPath); + app.saveJSONObject(instructions, new File(instructionsPath).getAbsolutePath()); } // Set current stroke index @@ -536,7 +536,7 @@ public void loadExistingStrokes() { } String projectId = app.getProjectId(); - File strokeDir = new File("project/" + projectId + "/stroke"); + File strokeDir = QuantumBrush.appFile("project/" + projectId + "/stroke"); if (!strokeDir.exists() || !strokeDir.isDirectory()) { return; @@ -846,11 +846,11 @@ public void runStroke(Stroke stroke) { // Define instructionsPath once here to use throughout the method final String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; - File instructionsFile = new File(instructionsPath); + File instructionsFile = QuantumBrush.appFile(instructionsPath); // Check if the stroke is in a failed state and needs cleanup if (instructionsFile.exists()) { - JSONObject instructions = app.loadJSONObject(instructionsPath); + JSONObject instructions = app.loadJSONObject(instructionsFile.getAbsolutePath()); String processingStatus = instructions.getString("processing_status", ""); // If the stroke is in a "running" state but not in our processing map, @@ -858,15 +858,15 @@ public void runStroke(Stroke stroke) { if ("running".equals(processingStatus) && !processingStrokes.containsKey(strokeId)) { // Reset the status to allow reprocessing instructions.setString("processing_status", "pending"); - app.saveJSONObject(instructions, instructionsPath); + app.saveJSONObject(instructions, instructionsFile.getAbsolutePath()); } } // Check if Python script exists - File pythonScript = new File("effect/" + folderName + "/" + effectId + ".py"); + File pythonScript = QuantumBrush.appFile("effect/" + folderName + "/" + effectId + ".py"); if (!pythonScript.exists()) { // Try with folder name as fallback - pythonScript = new File("effect/" + folderName + "/" + folderName + ".py"); + pythonScript = QuantumBrush.appFile("effect/" + folderName + "/" + folderName + ".py"); if (!pythonScript.exists()) { JOptionPane.showMessageDialog( null, @@ -879,7 +879,7 @@ public void runStroke(Stroke stroke) { } // Ensure directories exist - File projectDir = new File("project/" + projectId); + File projectDir = QuantumBrush.appFile("project/" + projectId); File strokeDir = new File(projectDir, "stroke"); if (!projectDir.exists()) projectDir.mkdirs(); if (!strokeDir.exists()) strokeDir.mkdirs(); @@ -887,17 +887,17 @@ public void runStroke(Stroke stroke) { // Ensure instructions file exists if (!instructionsFile.exists()) { JSONObject instructions = stroke.generateJSON(projectId, app.getHardwareManager().snapshotForStroke()); - app.saveJSONObject(instructions, instructionsPath); + app.saveJSONObject(instructions, instructionsFile.getAbsolutePath()); } // Update status fields to indicate processing has started - JSONObject instructions = app.loadJSONObject(instructionsPath); + JSONObject instructions = app.loadJSONObject(instructionsFile.getAbsolutePath()); instructions.setBoolean("created", true); instructions.setString("effect_received", "null"); instructions.setString("effect_processed", "null"); instructions.setString("effect_success", "null"); instructions.setString("processing_status", "running"); - app.saveJSONObject(instructions, instructionsPath); + app.saveJSONObject(instructions, instructionsFile.getAbsolutePath()); // Create a task to execute the Python script asynchronously Runnable task = () -> { @@ -1163,10 +1163,10 @@ public String getStrokeProcessingStatus(String strokeId) { try { String projectId = app.getProjectId(); String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; - File instructionsFile = new File(instructionsPath); + File instructionsFile = QuantumBrush.appFile(instructionsPath); if (instructionsFile.exists()) { - JSONObject instructions = app.loadJSONObject(instructionsFile); + JSONObject instructions = app.loadJSONObject(instructionsFile.getAbsolutePath()); return instructions.getString("processing_status", "unknown"); } } catch (Exception e) { @@ -1184,14 +1184,14 @@ private boolean executeApplyEffectScript(String instructionsFilePath) throws Int } // Create log directory if it doesn't exist - File logDir = new File("log"); + File logDir = QuantumBrush.appFile("log"); if (!logDir.exists()) { logDir.mkdirs(); } // Create log files for stdout and stderr - File stdoutLog = new File("log/python_stdout.log"); - File stderrLog = new File("log/python_stderr.log"); + File stdoutLog = QuantumBrush.appFile("log/python_stdout.log"); + File stderrLog = QuantumBrush.appFile("log/python_stderr.log"); // Display Python version information ProcessBuilder versionProcessBuilder = new ProcessBuilder(pythonCommand, "--version"); @@ -1231,9 +1231,13 @@ private boolean executeApplyEffectScript(String instructionsFilePath) throws Int versionProcess.waitFor(); // Create command to execute Python script + File instructionsFile = QuantumBrush.appFile(instructionsFilePath); ProcessBuilder processBuilder = new ProcessBuilder( - pythonCommand, "effect/apply_effect.py", instructionsFilePath + pythonCommand, + QuantumBrush.appFile("effect/apply_effect.py").getAbsolutePath(), + instructionsFile.getAbsolutePath() ); + processBuilder.directory(QuantumBrush.getAppRootDirectory()); processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(stdoutLog)); processBuilder.redirectError(ProcessBuilder.Redirect.appendTo(stderrLog)); @@ -1253,11 +1257,11 @@ private boolean executeApplyEffectScript(String instructionsFilePath) throws Int int exitCode = process.exitValue(); // Check the effect_success flag in the JSON file - JSONObject instructions = app.loadJSONObject(instructionsFilePath); + JSONObject instructions = app.loadJSONObject(instructionsFile.getAbsolutePath()); String projectId = instructions.getString("project_id", ""); String strokeId = instructions.getString("stroke_id", ""); String outputPath = "project/" + projectId + "/stroke/" + strokeId + "_output.png"; - File outputFile = new File(outputPath); + File outputFile = QuantumBrush.appFile(outputPath); String effectSuccess = instructions.getString("effect_success", "false"); boolean success = exitCode == 0 && "true".equals(effectSuccess) && outputFile.exists(); @@ -1321,7 +1325,7 @@ public boolean applyEffectToCanvas(String strokeId) { // Check if the effect was successful String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; - File instructionsFile = new File(instructionsPath); + File instructionsFile = QuantumBrush.appFile(instructionsPath); if (!instructionsFile.exists()) { JOptionPane.showMessageDialog( @@ -1333,7 +1337,7 @@ public boolean applyEffectToCanvas(String strokeId) { return false; } - JSONObject instructions = app.loadJSONObject(instructionsPath); + JSONObject instructions = app.loadJSONObject(instructionsFile.getAbsolutePath()); String effectSuccess = instructions.getString("effect_success", "false"); if (!"true".equals(effectSuccess)) { @@ -1348,7 +1352,7 @@ public boolean applyEffectToCanvas(String strokeId) { // Get the effect output image (just the effect, not layered) String outputPath = "project/" + projectId + "/stroke/" + strokeId + "_output.png"; - PImage effectImage = app.loadImage(outputPath); + PImage effectImage = app.loadImage(QuantumBrush.appFile(outputPath).getAbsolutePath()); if (effectImage == null) { System.err.println("Failed to load effect output image: " + outputPath); @@ -1412,9 +1416,9 @@ public boolean applyEffectToCanvas(String strokeId) { // IMPORTANT: Save the current state to disk if (projectId != null) { String projectPath = "project/" + projectId; - File projectDir = new File(projectPath); + File projectDir = QuantumBrush.appFile(projectPath); if (projectDir.exists()) { - resultImage.save(projectPath + "/current.png"); + resultImage.save(QuantumBrush.appFile(projectPath + "/current.png").getAbsolutePath()); System.out.println("Saved current state after applying effect"); } } @@ -1449,9 +1453,9 @@ public boolean deleteStroke(String strokeId) { // Delete stroke files String strokeDir = "project/" + projectId + "/stroke/"; - File instructionsFile = new File(strokeDir + strokeId + "_instructions.json"); - File inputFile = new File(strokeDir + strokeId + "_input.png"); - File outputFile = new File(strokeDir + strokeId + "_output.png"); + File instructionsFile = QuantumBrush.appFile(strokeDir + strokeId + "_instructions.json"); + File inputFile = QuantumBrush.appFile(strokeDir + strokeId + "_input.png"); + File outputFile = QuantumBrush.appFile(strokeDir + strokeId + "_output.png"); boolean success = true; if (instructionsFile.exists()) { @@ -1547,13 +1551,14 @@ public boolean updateStroke(String strokeId, Map newParameters) String projectId = app.getProjectId(); JSONObject instructions = strokeToUpdate.generateJSON(projectId, app.getHardwareManager().snapshotForStroke()); String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; + File instructionsFile = QuantumBrush.appFile(instructionsPath); // Mark the stroke as pending again, as its parameters have changed. // This is important so it can be re-run. instructions.setString("processing_status", "pending"); instructions.setString("effect_success", "null"); // Reset success flag - app.saveJSONObject(instructions, instructionsPath); + app.saveJSONObject(instructions, instructionsFile.getAbsolutePath()); System.out.println("Stroke " + strokeId + " updated successfully. Its status has been reset to 'pending'."); return true; diff --git a/src/UIManager.java b/src/UIManager.java index 6868949..6880b21 100644 --- a/src/UIManager.java +++ b/src/UIManager.java @@ -530,10 +530,10 @@ private void updateStrokeDetails(JPanel detailsPanel, StrokeManager.Stroke strok String projectIdForCost = app.getProjectId(); if (projectIdForCost != null) { String instrPath = "project/" + projectIdForCost + "/stroke/" + stroke.getId() + "_instructions.json"; - java.io.File instrFile = new java.io.File(instrPath); + java.io.File instrFile = QuantumBrush.appFile(instrPath); if (instrFile.exists()) { try { - JSONObject instr = app.loadJSONObject(instrPath); + JSONObject instr = app.loadJSONObject(instrFile.getAbsolutePath()); if (instr.hasKey("cost_estimate_qpu_seconds")) { float cost = instr.getFloat("cost_estimate_qpu_seconds", 0.0f); JSONObject hwBlock = instr.hasKey("hardware") ? instr.getJSONObject("hardware") : null; @@ -573,11 +573,11 @@ private void updateStrokeDetails(JPanel detailsPanel, StrokeManager.Stroke strok if (projectId != null) { // Input Image String inputPath = "project/" + projectId + "/stroke/" + stroke.getId() + "_input.png"; - addImageWithOverlay(imagesPanel, inputPath, "Input", stroke); + addImageWithOverlay(imagesPanel, QuantumBrush.appFile(inputPath).getAbsolutePath(), "Input", stroke); // Output Image String outputPath = "project/" + projectId + "/stroke/" + stroke.getId() + "_output.png"; - addImageWithOverlay(imagesPanel, outputPath, "Output", null); + addImageWithOverlay(imagesPanel, QuantumBrush.appFile(outputPath).getAbsolutePath(), "Output", null); } detailsPanel.add(imagesPanel); detailsPanel.add(Box.createVerticalStrut(10));