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;