diff --git a/examples/basicProject/basicProject.pde b/examples/basicProject/basicProject.pde index 9f07f5e..dc2a562 100644 --- a/examples/basicProject/basicProject.pde +++ b/examples/basicProject/basicProject.pde @@ -9,10 +9,11 @@ */ -/* In this example, a few files were included in the data folder. - * +/* Befor running this sketch, include all necessary media files + * in the data folder. The library will find them and made + * them available in the left panel. */ - + import paletai.mapping.*; //Luna need this two complementary libraries to work @@ -23,6 +24,11 @@ import processing.video.*; Project project; +// Called every time a new frame is available to read +void movieEvent(Movie m) { + m.read(); +} + void setup() { fullScreen(P2D, SPAN); //This should always be FullScreen, P2D and SPAN project = new Project(this, "NewProject"); //Name your project here diff --git a/examples/basicProject/data/4937374-30Fps.mp4 b/examples/basicProject/data/4937374-30Fps.mp4 new file mode 100644 index 0000000..674c601 Binary files /dev/null and b/examples/basicProject/data/4937374-30Fps.mp4 differ diff --git a/examples/basicProject/data/6289176-30Fps.mp4 b/examples/basicProject/data/6289176-30Fps.mp4 new file mode 100644 index 0000000..5962054 Binary files /dev/null and b/examples/basicProject/data/6289176-30Fps.mp4 differ diff --git a/examples/basicProject/data/9720247-30Fps.mp4 b/examples/basicProject/data/9720247-30Fps.mp4 new file mode 100644 index 0000000..968bd21 Binary files /dev/null and b/examples/basicProject/data/9720247-30Fps.mp4 differ diff --git a/examples/basicProject/data/NewProject.xml b/examples/basicProject/data/NewProject.xml index daaca6e..5404c50 100644 --- a/examples/basicProject/data/NewProject.xml +++ b/examples/basicProject/data/NewProject.xml @@ -4,8 +4,9 @@ + - + @@ -13,15 +14,15 @@ - - - - + + + + - + @@ -29,15 +30,13 @@ - - - - + + + + - - - + @@ -45,28 +44,14 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + diff --git a/examples/basicProject/data/Pexels credits.txt b/examples/basicProject/data/Pexels credits.txt deleted file mode 100644 index 58e2c79..0000000 --- a/examples/basicProject/data/Pexels credits.txt +++ /dev/null @@ -1,18 +0,0 @@ -Pexels content - - -legsDance_640_360_25fps -- original file name: 1590860-sd_960_540_25fps -- by Ronald Hayward - -emptyRoad -- original file name: pexels-sebastian-palomino-933481-1955134 -- by Sebastian Palomino - -painting1 -- original file name: pexels-steve-1585325 -- by Steve Johnson - -painting2 -- original file name: pexels-mccutcheon-1149019 -- by Alexander Grey \ No newline at end of file diff --git a/examples/basicProject/data/legsDance_640_360_25fps.mp4 b/examples/basicProject/data/legsDance_640_360_25fps.mp4 deleted file mode 100644 index c2ca935..0000000 Binary files a/examples/basicProject/data/legsDance_640_360_25fps.mp4 and /dev/null differ diff --git a/examples/cameraFeed/cameraFeed.pde b/examples/cameraFeed/cameraFeed.pde index 3261f30..9aebbe6 100644 --- a/examples/cameraFeed/cameraFeed.pde +++ b/examples/cameraFeed/cameraFeed.pde @@ -30,6 +30,11 @@ int camWidth, camHeight; PGraphics2D pgCamA; +// Called every time a new frame is available to read +void movieEvent(Movie m) { + m.read(); +} + void setup() { fullScreen(P2D, SPAN); //Always FullScreen, P2D and SPAN initCam(); diff --git a/examples/cameraFeed/data/generators.xml b/examples/cameraFeed/data/generators.xml new file mode 100644 index 0000000..e349f7e --- /dev/null +++ b/examples/cameraFeed/data/generators.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/emptyProject/emptyProject.pde b/examples/emptyProject/emptyProject.pde index e3dd868..8261b17 100644 --- a/examples/emptyProject/emptyProject.pde +++ b/examples/emptyProject/emptyProject.pde @@ -24,6 +24,11 @@ import processing.video.*; Project project; +// Called every time a new frame is available to read +void movieEvent(Movie m) { + m.read(); +} + void setup() { fullScreen(P2D, SPAN); //This should always be FullScreen, P2D and SPAN project = new Project(this, "NewProject"); //Name your project here diff --git a/examples/generator/generator.pde b/examples/generator/generator.pde index d098be0..f336a3f 100644 --- a/examples/generator/generator.pde +++ b/examples/generator/generator.pde @@ -24,7 +24,10 @@ Project project; import processing.opengl.PGraphics2D; - +// Called every time a new frame is available to read +void movieEvent(Movie m) { + m.read(); +} void setup() { fullScreen(P2D, SPAN); //Always FullScreen, P2D and SPAN diff --git a/library.properties b/library.properties index 677959b..d40b7a5 100644 --- a/library.properties +++ b/library.properties @@ -4,7 +4,7 @@ maxRevision=0 minRevision=0 name=Luna Video Mapping paragraph= -prettyVersion=0.1.0-alpha +prettyVersion=1.0.0-beta sentence=A video mapping library with a user interface. url=https\://luna.art.br/ version=0 diff --git a/src/main/java/paletai/mapping/MediaItem.java b/src/main/java/paletai/mapping/MediaItem.java index 47cc00d..4aae17f 100644 --- a/src/main/java/paletai/mapping/MediaItem.java +++ b/src/main/java/paletai/mapping/MediaItem.java @@ -73,7 +73,7 @@ public class MediaItem { /** * Video looping status */ - private boolean isLooping = false; + private volatile boolean isLooping = false; /** * Video object (for movies) @@ -158,6 +158,7 @@ public class MediaItem { */ private PFont mediaFont; + /** * Constructs a new MediaItem from a file path. * @@ -490,23 +491,13 @@ public void setThumbnailPosition(int x, int y) { * For generative content, prepares the generator system. */ void initVariables() { - // PApplet.println("initVariables"); mediaFont = p.createFont("NeueMachina-Regular.otf", 10, true); if (isVideo) { - // PApplet.println("is video"); - this.movie = new Movie(p, filePath); - movie.play(); // Preload the movie (optional) - mediaWidth = movie.width; - mediaHeight = movie.height; + initMovie(); thumbnail = p.createImage(150, 100, PConstants.RGB); - } else if (isGenerative) { - // PApplet.println("MediaItem is picture"); -// mediaWidth = 0; -// mediaHeight = 0; thumbnail = p.createImage(150, 100, PConstants.RGB); } else { - // PApplet.println("MediaItem is picture"); img = p.loadImage(filePath); mediaWidth = img.width; mediaHeight = img.height; @@ -517,6 +508,49 @@ void initVariables() { createControlGroup(); } + /** + * Initializes the movie object once without starting playback. + * Called during initVariables() instead of creating Movie inline. + */ + private void initMovie() { + if (isVideo) { + movie = new Movie(p, filePath); + //movie.play(); // must call play() once so GStreamer pipeline initializes + //movie.pause(); // immediately pause — we just needed the pipeline up + } + } + + // ------------------------------------------------------------------------- + // Movie event handling (called from Project.movieEvent on GStreamer thread) + // ------------------------------------------------------------------------- + +// /** +// * Returns true if this MediaItem owns the given Movie instance. +// * Used by Project.movieEvent() to route callbacks to the correct item. +// */ +// public boolean ownsMovie(Movie m) { +// return isVideo && this.movie == m; +// } + +// /** +// * Called by Project.movieEvent() when a new frame is available. +// * Runs on the GStreamer thread — only sets flags, never draws. +// */ +// public void handleMovieEvent() { +// newFrameAvailable = true; +// +// // Check end-of-video here, on the GStreamer thread, +// // but only set a flag — don't call jump() directly +//// if (movie != null && movie.duration() > 0) { +//// float timeLeft = movie.duration() - movie.time(); +//// if (timeLeft <= (1.0f / 30.0f)) { +//// if (isLooping) { +//// rewindRequested = true; +//// } +//// } +//// } +// } + /** * Updates media item configuration from XML data. * Restores homography transformation points from saved project data. @@ -571,6 +605,10 @@ void updateArrayXML(XML arrayXML, PVector[] arr) { } } + // ------------------------------------------------------------------------- + // Display assignment + // ------------------------------------------------------------------------- + /** * Assigns this media item to a display with specific dimensions. * Creates the media canvas, sets resolution, and configures the homography @@ -584,23 +622,16 @@ void updateArrayXML(XML arrayXML, PVector[] arr) { * @see VidMap#assignToDisplay(int, int) */ public void assignToDisplay(int w, int h, int screenIndex) { - // PApplet.println("MediaItem Assigned to Display: " + screenIndex); - //PApplet.println("=== assignToDisplay ==="); - //PApplet.println("Received dimensions: " + w + "x" + h); - //PApplet.println("Screen index: " + screenIndex); this.resolutionX = w; this.resolutionY = h; this.mediaCanvas = (PGraphics2D) p.createGraphics(resolutionX, resolutionY, PConstants.P2D); - //PApplet.println("mediaCanvas created: " + mediaCanvas.width + "x" + mediaCanvas.height); - this.mediaCanvas.beginDraw(); this.mediaCanvas.clear(); this.mediaCanvas.endDraw(); this.assignedScreen = screenIndex; if (isGenerative) { - //PApplet.println("Calling generator.setup(" + resolutionX + ", " + resolutionY + ")"); this.generator.setup(resolutionX, resolutionY); mediaWidth = resolutionX; mediaHeight = resolutionY; @@ -662,6 +693,10 @@ public void applyAspectRatioCorrection(int mediaWidth, int mediaHeight) { } // **🔹 vm Wrapper Methods** + // ------------------------------------------------------------------------- + // Homography wrappers + // ------------------------------------------------------------------------- + /** * Updates the homography transformation with new coordinate points. * Wrapper method for VidMap's updateHomography functionality. @@ -787,6 +822,10 @@ private boolean isVideoFile(String filename) { return filename.endsWith(".mp4") || filename.endsWith(".avi") || filename.endsWith(".mov"); } + // ------------------------------------------------------------------------- + // Thumbnail + // ------------------------------------------------------------------------- + /** * Generates a thumbnail image for this media item. * For generative content: captures the current generator output. @@ -800,20 +839,24 @@ private boolean isVideoFile(String filename) { public void generateThumbnail() { if (isGenerative) { thumbnail = p.createImage(mediaCanvas.width, mediaCanvas.height, PConstants.RGB); - thumbnail.copy(this.generator.getGraphics(), 0, 0, this.generator.getGraphics().width, this.generator.getGraphics().height, 0, 0, thumbnail.width, thumbnail.height); + thumbnail.copy(this.generator.getGraphics(), 0, 0, + this.generator.getGraphics().width, this.generator.getGraphics().height, + 0, 0, thumbnail.width, thumbnail.height); thumbnail.loadPixels(); thumbnail.resize(150, 100); } else if (!isVideo) { img.loadPixels(); thumbnail = p.createImage(img.width, img.height, PConstants.RGB); - thumbnail.copy(img, 0, 0, img.width, img.height, 0, 0, thumbnail.width, thumbnail.height); + thumbnail.copy(img, 0, 0, img.width, img.height, + 0, 0, thumbnail.width, thumbnail.height); thumbnail.loadPixels(); thumbnail.resize(150, 100); } else if (isVideo && movie != null) { // Check if video has pixels available movie.loadPixels(); thumbnail = p.createImage(movie.width, movie.height, PConstants.RGB); - thumbnail.copy(movie, 0, 0, movie.width, movie.height, 0, 0, thumbnail.width, thumbnail.height); + thumbnail.copy(movie, 0, 0, movie.width, movie.height, + 0, 0, thumbnail.width, thumbnail.height); thumbnail.loadPixels(); thumbnail.resize(150, 100); // PApplet.println(thumbnail.width); @@ -835,12 +878,15 @@ public void setPreviewArea(float px, float py, float pw, float ph) { vm.setPreviewArea(px, py, pw, ph); } + // ------------------------------------------------------------------------- + // Rendering + // ------------------------------------------------------------------------- + /** * Renders the media with homography transformation. - * Handles static images, video playback, and generative content. - * For videos: reads new frames and manages thumbnail generation. - * For generative content: updates and draws the generator output. - * Applies homography transformation to the final output. + * Videos: consumes thread-safe flags set by handleMovieEvent(). + * Generative: updates and draws generator output. + * Images: draws the static image. * * @see Movie#available() * @see Movie#read() @@ -849,33 +895,90 @@ public void setPreviewArea(float px, float py, float pw, float ph) { * @see VidMap#render(PGraphics2D) */ public void render() { +// if (isVideo) PApplet.println("render: movie=" + movie + +// " loaded=" + loaded + " newFrame=" + newFrameAvailable + +// " isPlaying=" + (movie != null ? movie.isPlaying() : "null")); + mediaCanvas.beginDraw(); mediaCanvas.background(0); // Clear previous frame if (isVideo && movie != null) { // Check for null FIRST - if (movie.available()) { - movie.read(); - if (mediaHeight == 0) { - mediaWidth = movie.width; - mediaHeight = movie.height; - if (!loaded) - applyAspectRatioCorrection(mediaWidth, mediaHeight); - loaded = true; - } - if (movie.time() > 0.5f && !thumbnailGenerated) { - generateThumbnail(); - //PApplet.println("Video thumbnail generated"); - } +// if (movie.available()) { +// movie.read(); +// if (mediaHeight == 0) { +// mediaWidth = movie.width; +// mediaHeight = movie.height; +// if (!loaded) +// applyAspectRatioCorrection(mediaWidth, mediaHeight); +// loaded = true; +// } +// if (movie.time() > 0.5f && !thumbnailGenerated) { +// generateThumbnail(); +// //PApplet.println("Video thumbnail generated"); +// } +// +// if (movie.time() > (movie.duration() - 0.1) && !isLooping) { +// stopMedia(); +// //PApplet.println("Video thumbnail generated"); +// } +// } +// +// // Only try to draw if movie is not null +// mediaCanvas.image(movie, 0, 0, mediaCanvas.width, mediaCanvas.height); + + // Dimensions and thumbnail — check once after loaded +// if (!loaded && movie.width > 0) { +// mediaWidth = movie.width; +// mediaHeight = movie.height; +// applyAspectRatioCorrection(mediaWidth, mediaHeight); +// loaded = true; +// } +// if (loaded && !thumbnailGenerated && movie.time() > 0.5f) { +// generateThumbnail(); +// } +// +// // End-of-video handling +// if (loaded && movie.duration() > 0 && +// movie.time() >= movie.duration() - (1.0f / 30.0f)) { +// if (isLooping) { +// movie.jump(0); +// movie.play(); +// } else { +// movie.pause(); // freeze on last frame +// } +// } +// +// // Always draw current frame (movieEvent keeps it fresh) +// mediaCanvas.image(movie, 0, 0, mediaCanvas.width, mediaCanvas.height); + + // Handle rewind request set by movieEvent thread +// if (rewindRequested) { +// rewindRequested = false; +// movie.jump(0); +// movie.play(); +// } + + // Capture dimensions once the first frame arrives + if (!loaded && movie.width > 0) { + mediaWidth = movie.width; + mediaHeight = movie.height; + //applyAspectRatioCorrection(mediaWidth, mediaHeight); + loaded = true; + } - if (movie.time() > (movie.duration() - 0.1) && !isLooping) { - stopMedia(); - //PApplet.println("Video thumbnail generated"); - } + // Thumbnail — once only + if (loaded && !thumbnailGenerated && movie.time() > 0.5f) { + generateThumbnail(); } - // Only try to draw if movie is not null - mediaCanvas.image(movie, 0, 0, mediaCanvas.width, mediaCanvas.height); + if (isLooping && movie.time()>movie.duration()-0.2){ + disposeMedia(); + initMovie(); + loopMedia(); + } + // Always draw — movieEvent keeps frame fresh, fallback keeps last frame visible + mediaCanvas.image(movie, 0, 0, mediaCanvas.width, mediaCanvas.height); } else if (isGenerative) { //PApplet.println("Generative"); this.generator.update(); @@ -888,7 +991,6 @@ public void render() { } } } else if (!isVideo) { - // Handle non-video or stopped video case if (!thumbnailGenerated) { generateThumbnail(); } @@ -905,23 +1007,26 @@ public void render() { } } + // ------------------------------------------------------------------------- + // Playback control + // ------------------------------------------------------------------------- + /** * Toggles video playback state. * Plays if paused, pauses if playing. No effect on static images or generative content. */ public void togglePlayback() { - if (isVideo) { + if (isVideo && movie != null) { if (movie.isPlaying()) { movie.pause(); } else { - playMedia(); + movie.play(); } } } /** - * Toggles video loop mode. - * Switches between single playback and continuous looping for video content. + * Toggles loop mode without restarting playback. */ public void toggleLoop() { isLooping = !isLooping; @@ -929,53 +1034,95 @@ public void toggleLoop() { } /** - * Starts media playback. - * For videos: begins playback from the start in single-play mode. - * Stops any existing playback before starting new playback. - * No effect on static images or generative content. + * Starts playback from the beginning in single-play mode. + * Rebuilds the GStreamer pipeline if it was previously disposed. */ public void playMedia() { +// if (isVideo && movie != null) { +// movie.jump(0); // rewind to start +// movie.play(); +// isLooping = false; +// } if (isVideo) { - stopMedia(); // clean up old one first - movie = new Movie(p, filePath); + if (movie == null) initMovie(); // rebuild if was disposed + //if(loaded) movie.jump(0); // only rewind if already played before movie.play(); + movie.noLoop(); isLooping = false; } } +// public void playMedia() { +// if (isVideo) { +// stopMedia(); // clean up old one first +// movie = new Movie(p, filePath); +// movie.play(); +// isLooping = false; +// } +// } + /** - * Starts media playback in loop mode. - * For videos: begins continuous looping playback. - * Stops any existing playback before starting new looped playback. - * No effect on static images or generative content. + * Starts looping playback from the beginning. + * Rebuilds the GStreamer pipeline if it was previously disposed. */ public void loopMedia() { +// if (isVideo && movie != null) { +// movie.jump(0); +// movie.loop(); +// isLooping = true; +// } if (isVideo) { - stopMedia(); // clean up old one first - movie = new Movie(p, filePath); + if (movie == null) initMovie(); + //if (loaded) movie.jump(0); movie.loop(); isLooping = true; } } +// public void loopMedia() { +// if (isVideo) { +// stopMedia(); // clean up old one first +// movie = new Movie(p, filePath); +// movie.loop(); +// isLooping = true; +// } +// } /** - * Stops media playback and cleans up resources. - * For videos: stops playback, disposes native resources, and clears the display. - * Crucial for GStreamer cleanup to release native pipeline resources. - * No effect on static images or generative content. + * Pauses playback, preserving the current frame on screen. + * Used by the Stop button — does NOT dispose the pipeline. */ public void stopMedia() { if (isVideo && movie != null) { - //PApplet.println("Stop Media"); + movie.pause(); // pause instead of stop — preserves last frame in buffer + // Do NOT clear the canvas here — let the last frame persist + } + } +// public void stopMedia() { +// if (isVideo && movie != null) { +// //PApplet.println("Stop Media"); +// movie.stop(); +// movie.dispose(); // force GStreamer cleanup. It is crucial to force GStreamer to release the +// // native pipeline before reusing +// movie = null; +// // movie = new Movie(p, filePath); +// mediaCanvas.beginDraw(); +// mediaCanvas.clear(); +// mediaCanvas.endDraw(); +// //PApplet.println("finished Stop Media"); +// } +// } + + /** + * Fully disposes the GStreamer pipeline and releases all native resources. + * Called by Scene.deactivate() to prevent pipeline accumulation across scene switches. + * playMedia() / loopMedia() will call initMovie() to rebuild when needed. + */ + public void disposeMedia() { + if (isVideo && movie != null) { movie.stop(); - movie.dispose(); // force GStreamer cleanup. It is crucial to force GStreamer to release the - // native pipeline before reusing + movie.dispose(); movie = null; - // movie = new Movie(p, filePath); - mediaCanvas.beginDraw(); - mediaCanvas.clear(); - mediaCanvas.endDraw(); - //PApplet.println("finished Stop Media"); + loaded = false; } } diff --git a/src/main/java/paletai/mapping/Project.java b/src/main/java/paletai/mapping/Project.java index 9e22d6e..59e84e2 100644 --- a/src/main/java/paletai/mapping/Project.java +++ b/src/main/java/paletai/mapping/Project.java @@ -11,6 +11,8 @@ import controlP5.*; import paletai.generators.LunaContentGenerator; import processing.data.XML; +import processing.video.Movie; + import java.lang.reflect.*; import java.util.Arrays; import java.util.Objects; @@ -125,6 +127,7 @@ public class Project { * @see #initializeDisplays() * @see #scanMediaFiles() * @see #scanGenerators() + * @see #scanGenerators() * @see #initXMLconfig() * @see #initializeButtons() */ @@ -153,37 +156,67 @@ public Project(PApplet p, String name) { void initializeDisplays() { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice[] devices = ge.getScreenDevices(); + GraphicsDevice primaryDevice = ge.getDefaultScreenDevice(); // true OS primary availableDisplays.clear(); // Clear previous display info - for (int i = 0; i < devices.length; i++) { - Rectangle bounds = devices[i].getDefaultConfiguration().getBounds(); - if (i == 0) { // initialize UI - mainWidth = bounds.width; - mainHeight = bounds.height; - hx1 = mainWidth / 6; - hx2 = mainWidth - hx1; - hy1 = 30; - hy2 = 2 * mainHeight / 4; - r = 10; - previewAreaX = hx1 + 20; - previewAreaY = hy1 + 20; - previewAreaWidth = hx2 - hx1 - 40; - previewAreaHeight = hy2 - hy1 - 40 - screenButtonsArea; // Room for buttons - // Create an offscreen buffer matching main screen size - canvaUI = (PGraphics2D) mainApplet.createGraphics(mainWidth, mainHeight, PConstants.P2D); - canvaUI.beginDraw(); - canvaUI.background(33); - canvaUI.textSize(20); - canvaUI.fill(200); - canvaUI.textAlign(PConstants.CENTER, PConstants.CENTER); - canvaUI.text("Luna Video Mapping", (float) canvaUI.width / 2, (float) canvaUI.height / 2); - canvaUI.endDraw(); - } else { - // Store external display info without creating screens - availableDisplays.add(bounds); - PApplet.println("Found external display #" + i + ": " + bounds.width + "x" + bounds.height); - } - } +// for (int i = 0; i < devices.length; i++) { +// Rectangle bounds = devices[i].getDefaultConfiguration().getBounds(); +// if (i == 0) { // initialize UI +// mainWidth = bounds.width; +// mainHeight = bounds.height; +// hx1 = mainWidth / 6; +// hx2 = mainWidth - hx1; +// hy1 = 30; +// hy2 = 2 * mainHeight / 4; +// r = 10; +// previewAreaX = hx1 + 20; +// previewAreaY = hy1 + 20; +// previewAreaWidth = hx2 - hx1 - 40; +// previewAreaHeight = hy2 - hy1 - 40 - screenButtonsArea; // Room for buttons +// // Create an offscreen buffer matching main screen size +// canvaUI = (PGraphics2D) mainApplet.createGraphics(mainWidth, mainHeight, PConstants.P2D); +// canvaUI.beginDraw(); +// canvaUI.background(33); +// canvaUI.textSize(20); +// canvaUI.fill(200); +// canvaUI.textAlign(PConstants.CENTER, PConstants.CENTER); +// canvaUI.text("Luna Video Mapping", (float) canvaUI.width / 2, (float) canvaUI.height / 2); +// canvaUI.endDraw(); +// } else { +// // Store external display info without creating screens +// availableDisplays.add(bounds); +// PApplet.println("Found external display #" + i + ": " + bounds.width + "x" + bounds.height); +// } +// } + // First pass: initialize UI from the true primary display + for (GraphicsDevice device : devices) { + Rectangle bounds = device.getDefaultConfiguration().getBounds(); + if (device == primaryDevice) { + mainWidth = bounds.width; + mainHeight = bounds.height; + hx1 = mainWidth / 6; + hx2 = mainWidth - hx1; + hy1 = 30; + hy2 = 2 * mainHeight / 4; + r = 10; + previewAreaX = hx1 + 20; + previewAreaY = hy1 + 20; + previewAreaWidth = hx2 - hx1 - 40; + previewAreaHeight = hy2 - hy1 - 40 - screenButtonsArea; + canvaUI = (PGraphics2D) mainApplet.createGraphics(mainWidth, mainHeight, PConstants.P2D); + canvaUI.beginDraw(); + canvaUI.background(33); + canvaUI.textSize(20); + canvaUI.fill(200); + canvaUI.textAlign(PConstants.CENTER, PConstants.CENTER); + canvaUI.text("Luna Video Mapping", (float) canvaUI.width / 2, (float) canvaUI.height / 2); + canvaUI.endDraw(); + } else { + // All non-primary displays are available for mapping output + availableDisplays.add(bounds); + PApplet.println("Found external display: " + bounds.width + "x" + bounds.height + " at x=" + bounds.x); + } + } } /** @@ -487,7 +520,7 @@ void addMedia(String name) { MediaItem newMedia = new MediaItem(mainApplet, name, currentScreen, newMediaId); newMedia.assignToDisplay(screens.get(currentScreen).w, screens.get(currentScreen).h, currentScreen); scenes.get(currentScene).addMedia(newMedia); - scenes.get(currentScreen).setThumbnailPosition(2 * r, hy2); + scenes.get(currentScene).setThumbnailPosition(2 * r, hy2); } /** @@ -785,7 +818,7 @@ void addSelectScreenButton(int index) { Toggle t = screenRadio.getItem(optionName); if (index == 0) { screenRadio.activate(index); // ensures this Toggle is ON - selectScene(index); // call your logic as if it was clicked + selectScreen(index); // call your logic as if it was clicked } t.addCallback(new CallbackListener() { public void controlEvent(CallbackEvent ev) { @@ -1096,6 +1129,23 @@ public void controlEvent(CallbackEvent event) { } } +// /** +// * Routes a movie event to the correct MediaItem. +// * Must be called from movieEvent(Movie m) in the sketch. +// * +// * @param m The Movie instance that has a new frame available +// */ +// public void movieEvent(Movie m) { +// for (Scene scene : scenes) { +// for (MediaItem media : scene.mediaItems) { +// if (media.ownsMovie(m)) { +// media.handleMovieEvent(); +// return; // found it, no need to keep searching +// } +// } +// } +// } + /** * Main rendering method that draws the entire project interface. * Handles both normal rendering and scene transitions, updates UI controls, diff --git a/src/main/java/paletai/mapping/Scene.java b/src/main/java/paletai/mapping/Scene.java index 86fa507..e5997d6 100644 --- a/src/main/java/paletai/mapping/Scene.java +++ b/src/main/java/paletai/mapping/Scene.java @@ -80,7 +80,8 @@ void setThumbnailPosition(int x, int y) { /** * Renders all media items in this scene if the scene is active. - * Only processes rendering when the scene is in active state. + * Deletion requests are collected after rendering to avoid + * modifying the list while iterating over it. * * @see MediaItem#render() * @see #isActive @@ -91,8 +92,16 @@ void render() { media.render(); } - for (int i = 0; i toDelete = new ArrayList(); + for (MediaItem media : mediaItems) { + if (media.toBeDeleted) toDelete.add(media.mediaId); + } + for (int id : toDelete) { + delMedia(id); } } } @@ -111,15 +120,36 @@ void addMedia(MediaItem newMedia) { } /** - * Removes a media item from this scene by index. + * Removes a media item from this scene by mediaId. + * Searches by mediaId (not list index) to handle gaps safely, + * then re-indexes remaining items so mediaId always matches list position. * * @param index The index of the media item to remove */ - void delMedia(int index) { - mediaItems.get(index).stopMedia(); - mediaItems.get(index).deleteControls(); - mediaItems.remove(index); + void delMedia(int mediaId) { + MediaItem toRemove = null; + for (MediaItem m : mediaItems) { + if (m.mediaId == mediaId) { + toRemove = m; + break; + } + } + if (toRemove == null) return; + + toRemove.stopMedia(); + toRemove.deleteControls(); + mediaItems.remove(toRemove); + + // Re-index so mediaId always matches list position + for (int i = 0; i < mediaItems.size(); i++) { + mediaItems.get(i).mediaId = i; + } } +// void delMedia(int index) { +// mediaItems.get(index).stopMedia(); +// mediaItems.get(index).deleteControls(); +// mediaItems.remove(index); +// } /** * Serializes the scene and all its media items to XML for project saving. @@ -147,7 +177,7 @@ XML saveXML() { */ public void deactivate() { for (MediaItem media : mediaItems) { - media.stopMedia(); + media.disposeMedia(); media.offCalibration(); media.hideControls(); } @@ -163,7 +193,7 @@ public void deactivate() { */ public void activate() { for (MediaItem media : mediaItems) { - media.playMedia(); + media.playMedia(); // rebuilds pipeline if disposed, then plays media.showControls(); } isActive = true;