diff --git a/open-robotics/configs/..valid.json b/open-robotics/configs/..valid.json index ca7b53f7..c7d43a20 100644 --- a/open-robotics/configs/..valid.json +++ b/open-robotics/configs/..valid.json @@ -1,6 +1,6 @@ { "config" : { - "runId" : "ebc72ee6-96dc-4bb3-a878-6f8e7df4cf02", + "runId" : "e0255128-8a9e-4f70-8141-13275f6f698a", "runName" : "default_run", "tickMs" : 100, "maxTicks" : 5000, @@ -15,14 +15,14 @@ "manualTaskAssignment" : false }, "map" : { - "mapId" : "4ef3d4b7-a31b-4aab-8064-b16b7de126b6", + "mapId" : "7796b51c-148b-4555-a2cd-c3081fd033cc", "width" : 8, "height" : 8, "tiles" : [ ] }, "entities" : { "robots" : [ { - "id" : "bb6e0a02-c19d-4200-9c79-8c2f0feb95e3", + "id" : "944fb9eb-0d4a-4551-89f3-55051269d0d0", "name" : "R1", "position" : { "x" : 1, diff --git a/open-robotics/configs/observalble_deadlock.json b/open-robotics/configs/observalble_deadlock.json index 1f08674e..cc13dfeb 100644 --- a/open-robotics/configs/observalble_deadlock.json +++ b/open-robotics/configs/observalble_deadlock.json @@ -122,7 +122,7 @@ } ], "obstacles" : [ { "id" : "f0000000-0000-4000-a000-000000000001", - "name" : "Shelf-L1", + "name" : "Wall-L1", "position" : { "x" : 4, "y" : 3 @@ -130,7 +130,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000002", - "name" : "Shelf-L2", + "name" : "Wall-L2", "position" : { "x" : 4, "y" : 4 @@ -138,7 +138,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000003", - "name" : "Shelf-L3", + "name" : "Wall-L3", "position" : { "x" : 4, "y" : 5 @@ -146,7 +146,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000004", - "name" : "Shelf-L4", + "name" : "Wall-L4", "position" : { "x" : 4, "y" : 6 @@ -154,7 +154,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000005", - "name" : "Shelf-R1", + "name" : "Wall-R1", "position" : { "x" : 7, "y" : 3 @@ -162,7 +162,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000006", - "name" : "Shelf-R2", + "name" : "Wall-R2", "position" : { "x" : 7, "y" : 4 @@ -170,7 +170,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000007", - "name" : "Shelf-R3", + "name" : "Wall-R3", "position" : { "x" : 7, "y" : 5 @@ -178,7 +178,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000008", - "name" : "Shelf-R4", + "name" : "Wall-R4", "position" : { "x" : 7, "y" : 6 diff --git a/open-robotics/configs/test3.json b/open-robotics/configs/test3.json index ba057ffe..ea64d57e 100644 --- a/open-robotics/configs/test3.json +++ b/open-robotics/configs/test3.json @@ -117,7 +117,7 @@ } ], "obstacles" : [ { "id" : "f0000000-0000-4000-a000-000000000001", - "name" : "Shelf-L1", + "name" : "Wall-L1", "position" : { "x" : 4, "y" : 3 @@ -125,7 +125,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000002", - "name" : "Shelf-L2", + "name" : "Wall-L2", "position" : { "x" : 4, "y" : 4 @@ -133,7 +133,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000003", - "name" : "Shelf-L3", + "name" : "Wall-L3", "position" : { "x" : 4, "y" : 5 @@ -141,7 +141,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000004", - "name" : "Shelf-L4", + "name" : "Wall-L4", "position" : { "x" : 4, "y" : 6 @@ -149,7 +149,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000005", - "name" : "Shelf-R1", + "name" : "Wall-R1", "position" : { "x" : 7, "y" : 3 @@ -157,7 +157,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000006", - "name" : "Shelf-R2", + "name" : "Wall-R2", "position" : { "x" : 7, "y" : 4 @@ -165,7 +165,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000007", - "name" : "Shelf-R3", + "name" : "Wall-R3", "position" : { "x" : 7, "y" : 5 @@ -173,7 +173,7 @@ "type" : null }, { "id" : "f0000000-0000-4000-a000-000000000008", - "name" : "Shelf-R4", + "name" : "Wall-R4", "position" : { "x" : 7, "y" : 6 diff --git a/open-robotics/configs/test_scenario.json b/open-robotics/configs/test_scenario.json index 39f3494d..f2ae3c8a 100644 --- a/open-robotics/configs/test_scenario.json +++ b/open-robotics/configs/test_scenario.json @@ -61,7 +61,7 @@ } ], "stations" : [ { "id" : "00000000-0000-0000-0000-000000000010", - "name" : "Pickup-A", + "name" : "Delivery-A", "position" : { "x" : 0, "y" : 4 diff --git a/open-robotics/configs/warehouse_2.json b/open-robotics/configs/warehouse_2.json index 8302f7b3..c8ec1b8c 100644 --- a/open-robotics/configs/warehouse_2.json +++ b/open-robotics/configs/warehouse_2.json @@ -98,7 +98,7 @@ } ], "obstacles" : [ { "id" : "20000000-0000-0000-0000-000000000001", - "name" : "Obstacle-01", + "name" : "wall_0", "position" : { "x" : 6, "y" : 3 diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java b/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java index 33451d1a..cddfd849 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java @@ -976,7 +976,7 @@ private boolean validate() { } // Integer Bounds - if (!checkIntBound(maxTasksField, "Max Tasks", 1, 500)) return false; + if (!checkIntBound(maxTasksField, "Max Tasks", 1, 50000)) return false; if (!checkIntBound(maxTicksField, "Max Ticks", 1, 1000000)) return false; // Float Bounds diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java b/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java index a2cc3d2b..eb7e90d7 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java @@ -24,6 +24,8 @@ import com.openrobotics.simulationcore.SimulationError; import com.openrobotics.task.Task; import com.openrobotics.util.IconLoader; +import com.openrobotics.util.ConnectedWallRenderer; +import com.openrobotics.util.RobotSpriteAnimator; import com.openrobotics.util.ScreenNavigator; import com.openrobotics.util.ViewportTips; import javafx.application.Platform; @@ -160,6 +162,7 @@ public class SimulationController implements ScreenNavigator.Cleanable { // Engine Binding private SimulationEngine engine; private Timeline simLoop; + private javafx.animation.AnimationTimer renderLoop; // drives continuous sprite animation private double speedFactor = 1.0; private int localTick = 0; private static final double BASE_TICK_MS = 100.0; @@ -368,6 +371,20 @@ private void saveEditorBaseline() { // Initialization @FXML private void initialize() { + // Continuous render loop so sprite animations cycle smoothly (~30 fps target). + long[] lastRender = {0}; + renderLoop = new javafx.animation.AnimationTimer() { + @Override public void handle(long now) { + if (now - lastRender[0] > 33_000_000L) { // ~30 fps + drawViewport(); + lastRender[0] = now; + } + } + }; + renderLoop.start(); + RobotSpriteAnimator.ensureLoaded(); + ConnectedWallRenderer.ensureLoaded(); + // Poll fresh simulation logs once per second while the screen is active. logPollingTimeline = new Timeline( new KeyFrame(Duration.seconds(1), event -> fetchLogsAsync()) @@ -599,9 +616,19 @@ private void drawEntities(GraphicsContext gc) { double tileSize = 32 * zoom; double pad = Math.max(1.0, tileSize * 0.06); - for (MapEntity entity : engine.getMap().getEntities()) { - double ex = entity.getPosition().getX(); - double ey = entity.getPosition().getY(); + // Two-pass: draw non-robots first so robots always appear on top + java.util.List entities = engine.getMap().getEntities(); + java.util.List robots = new java.util.ArrayList<>(); + for (MapEntity entity : entities) { + if (entity instanceof Robot) robots.add(entity); + else drawEntity(gc, entity, tileSize, pad); + } + for (MapEntity entity : robots) drawEntity(gc, entity, tileSize, pad); + } + + private void drawEntity(GraphicsContext gc, MapEntity entity, double tileSize, double pad) { + double ex = entity.getPosition().getX(); + double ey = entity.getPosition().getY(); // Interpolate robot sprites between ticks so movement appears continuous. if (animating && entity instanceof Robot && prevRobotPositions.containsKey(entity.getId())) { @@ -613,52 +640,66 @@ private void drawEntities(GraphicsContext gc) { double sx = viewOffsetX + (ex + entityOffsetTileX) * tileSize; double sy = viewOffsetY + (ey + entityOffsetTileY) * tileSize; - javafx.scene.image.Image entityIcon = resolveEntityIcon(entity); - boolean useFullTileIcon = isWallLikeEntity(entity); - if (entityIcon != null && !entityIcon.isError()) { - if (useFullTileIcon) { - gc.drawImage(entityIcon, sx, sy, tileSize, tileSize); + // ── 1. Animated robot sprite ───────────────────────────────── + if (entity instanceof Robot robot) { + javafx.scene.image.Image frame = switch (robot.getState()) { + case MOVING -> RobotSpriteAnimator.walkFrame(robot.getHeading(), robot.isCarrying()); + default -> RobotSpriteAnimator.idleFrame(robot.getHeading()); + }; + if (frame != null) { + gc.drawImage(frame, sx + pad, sy + pad, tileSize - 2 * pad, tileSize - 2 * pad); } else { - gc.drawImage(entityIcon, sx + pad, sy + pad, tileSize - 2 * pad, tileSize - 2 * pad); + gc.setFill(ENTITY_ROBOT_COLOR); + gc.fillRoundRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad, 5, 5); + Color dot = switch (robot.getState()) { + case MOVING -> ENTITY_STATE_MOVING_COLOR; + case CHARGING -> ENTITY_STATE_CHARGING_COLOR; + case LOADING, UNLOADING -> ENTITY_STATE_LOADING_COLOR; + default -> ENTITY_STATE_IDLE_COLOR; + }; + gc.setFill(dot); + double r = Math.max(3.0, tileSize * 0.15); + gc.fillOval(sx + tileSize - r * 2 - pad, sy + pad, r * 2, r * 2); } - } else if (entity instanceof Robot robot) { - gc.setFill(ENTITY_ROBOT_COLOR); - gc.fillRoundRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad, 5, 5); - Color dot = switch (robot.getState()) { - case MOVING -> ENTITY_STATE_MOVING_COLOR; - case CHARGING -> ENTITY_STATE_CHARGING_COLOR; - case LOADING, UNLOADING -> ENTITY_STATE_LOADING_COLOR; - default -> ENTITY_STATE_IDLE_COLOR; - }; - gc.setFill(dot); - double r = Math.max(3.0, tileSize * 0.15); - gc.fillOval(sx + tileSize - r * 2 - pad, sy + pad, r * 2, r * 2); - } else if (entity instanceof Rack) { - gc.setFill(ENTITY_RACK_COLOR); - gc.fillRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad); - } else if (entity instanceof Station) { - gc.setFill(ENTITY_STATION_COLOR); - gc.fillRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad); - } else if (entity instanceof Obstacle) { - gc.setFill(ENTITY_OBSTACLE_COLOR); - gc.fillRect(sx, sy, tileSize, tileSize); + + // ── 2. Connected wall / obstacle ───────────────────────────── + } else if (entity instanceof Obstacle || ConnectedWallRenderer.isWallLike(entity)) { + ConnectedWallRenderer.draw(gc, sx, sy, tileSize, entity, engine.getMap()); + + // ── 3. Animated charger sprite ─────────────────────────────── + } else if (entity instanceof com.openrobotics.map.entities.station.ChargingStation) { + javafx.scene.image.Image frame = RobotSpriteAnimator.chargerFrame(); + if (frame != null) { + gc.drawImage(frame, sx + pad, sy + pad, tileSize - 2 * pad, tileSize - 2 * pad); + } else { + javafx.scene.image.Image icon = resolveEntityIcon(entity); + if (icon != null && !icon.isError()) + gc.drawImage(icon, sx + pad, sy + pad, tileSize - 2 * pad, tileSize - 2 * pad); + } + + // ── 4. Everything else: static icon then colour fallback ────── } else { - // Engine stores all non-robot entities as plain MapEntity; - // infer visual type from name so items are colour-coded. - String n = entity.getName().toLowerCase(); - boolean isWall = n.contains("wall") || n.contains("obstacle"); - if (n.contains("rack") || n.contains("shelf")) { + javafx.scene.image.Image icon = resolveEntityIcon(entity); + if (icon != null && !icon.isError()) { + gc.drawImage(icon, sx + pad, sy + pad, tileSize - 2 * pad, tileSize - 2 * pad); + } else if (entity instanceof Rack) { gc.setFill(ENTITY_RACK_COLOR); - } else if (n.contains("station") || n.contains("charge") || n.contains("depot") - || n.contains("pickup") || n.contains("delivery")) { + gc.fillRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad); + } else if (entity instanceof Station) { gc.setFill(ENTITY_STATION_COLOR); - } else if (isWall) { - gc.setFill(ENTITY_OBSTACLE_COLOR); + gc.fillRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad); } else { - gc.setFill(ENTITY_FALLBACK_COLOR); + String n = entity.getName() == null ? "" : entity.getName().toLowerCase(); + if (n.contains("rack") || n.contains("shelf")) + gc.setFill(ENTITY_RACK_COLOR); + else if (n.contains("station") || n.contains("charge") || + n.contains("depot") || n.contains("pickup") || + n.contains("delivery")) + gc.setFill(ENTITY_STATION_COLOR); + else + gc.setFill(ENTITY_FALLBACK_COLOR); + gc.fillRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad); } - if (isWall) gc.fillRect(sx, sy, tileSize, tileSize); - else gc.fillRect(sx + pad, sy + pad, tileSize - 2*pad, tileSize - 2*pad); } if (zoom >= 0.8 && tileSize >= 18) { @@ -668,9 +709,13 @@ private void drawEntities(GraphicsContext gc) { String lbl = entity.getName().length() > maxChars ? entity.getName().substring(0, maxChars - 1) + "\u2026" : entity.getName(); - gc.setFill(ENTITY_LABEL_COLOR); - gc.setFont(Font.font(fontSize)); + // Draw label using DIFFERENCE blend mode with white — inverts whatever is beneath + gc.save(); + gc.setGlobalBlendMode(javafx.scene.effect.BlendMode.DIFFERENCE); + gc.setFill(Color.WHITE); + gc.setFont(Font.font("System Bold", fontSize)); gc.fillText(lbl, sx + pad + 1, sy + tileSize - pad - 2, maxLabelWidth); + gc.restore(); } // Green outline for the currently selected entity @@ -686,7 +731,6 @@ private void drawEntities(GraphicsContext gc) { gc.setLineWidth(Math.max(2.0, tileSize * 0.1)); gc.strokeRect(sx + 1, sy + 1, tileSize - 2, tileSize - 2); } - } } private void drawTrafficRuleIntersections(GraphicsContext gc) { @@ -1118,10 +1162,12 @@ private void onViewportMousePressed(MouseEvent e) { MapEntity entityHit = entityAtScreenPos(e.getX(), e.getY()); if (entityHit != null) { selectEntity(entityHit); - draggingOnCanvas = entityHit; - dragStartPosition = new com.openrobotics.map.Vector2D( - (int) entityHit.getPosition().getX(), - (int) entityHit.getPosition().getY()); + if (!isEditorLocked()) { + draggingOnCanvas = entityHit; + dragStartPosition = new com.openrobotics.map.Vector2D( + (int) entityHit.getPosition().getX(), + (int) entityHit.getPosition().getY()); + } } else { clearSelection(); draggingOnCanvas = null; diff --git a/open-robotics/src/main/java/com/openrobotics/robot/Robot.java b/open-robotics/src/main/java/com/openrobotics/robot/Robot.java index 3f092974..7a115ab4 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/Robot.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/Robot.java @@ -18,6 +18,7 @@ import com.openrobotics.task.Task; import com.openrobotics.task.TaskStatus; import com.openrobotics.map.Vector2D; +import com.openrobotics.common.Direction; import java.util.HashMap; import java.util.UUID; @@ -42,6 +43,7 @@ public class Robot extends MapEntity { private boolean rerouteAttemptedForCurrentTask; // one reroute budget per task private Vector2D rerouteAvoidTile; // temporary avoid hint used by the next planning attempt private Vector2D lastRequestedNextTile; // raw pre-coordination next tile requested this tick + private Direction heading = Direction.DOWN; // last movement direction, default south // lifetime stats — accumulated during update(), read by results screen private int totalDistanceMoved; @@ -106,6 +108,9 @@ private void initMovementFields() { public SensorStrategy getSensor() { return sensor; } public Sensor getLastScan() { return lastScan; } public RobotState getState() { return state; } + public Direction getHeading() { return heading; } + /** True when the robot has picked up a box and is carrying it to the dropoff. */ + public boolean isCarrying() { return hasPickedUp && currentTask != null; } public Task getCurrentTask() { return currentTask; } public int getStuckTicks() { return stuckTicks; } public Vector2D getPreviousPosition() { return previousPosition; } @@ -396,6 +401,13 @@ public void update(Map map) { totalEnergyConsumed += config.energyPerMove; totalDistanceMoved++; stuckTicks = 0; + // update facing direction from movement delta + int hdx = getPosition().getX() - previousPosition.getX(); + int hdy = getPosition().getY() - previousPosition.getY(); + if (hdx > 0) heading = Direction.RIGHT; + else if (hdx < 0) heading = Direction.LEFT; + else if (hdy > 0) heading = Direction.DOWN; + else if (hdy < 0) heading = Direction.UP; // Logging robot movement execution event SimLogRecordBuilder movementRecordBuilder = new SimLogRecordBuilder(AppState.getEngine().getRunId(), AppState.getEngine().getTickCounter(), getId(), getPosition().getX(), getPosition().getY()); diff --git a/open-robotics/src/main/java/com/openrobotics/util/ConnectedWallRenderer.java b/open-robotics/src/main/java/com/openrobotics/util/ConnectedWallRenderer.java new file mode 100644 index 00000000..a0da97d1 --- /dev/null +++ b/open-robotics/src/main/java/com/openrobotics/util/ConnectedWallRenderer.java @@ -0,0 +1,88 @@ +package com.openrobotics.util; + +import com.openrobotics.map.Map; +import com.openrobotics.map.MapEntity; +import com.openrobotics.map.Vector2D; +import com.openrobotics.map.entities.environment.Obstacle; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.Image; + +import java.util.EnumMap; + +/** + * Draws wall tiles that visually connect to their neighbours using pre-rendered + * Blender sprites for every combination. + * + *

Bitmask encoding (4 bits → 16 sprites): + *

+ *   bit 0 (1) = N   (y-1 on grid = up   on screen)
+ *   bit 1 (2) = S   (y+1 on grid = down on screen)
+ *   bit 2 (4) = E   (x+1 on grid = right)
+ *   bit 3 (8) = W   (x-1 on grid = left)
+ * 
+ * Sprites are named {@code wall_0.png} … {@code wall_15.png} in the img resource folder. + */ +public class ConnectedWallRenderer { + + private static final int VARIANT_COUNT = 16; + private static final Image[] VARIANTS = new Image[VARIANT_COUNT]; + private static boolean loaded = false; + + /** Pre-load all 16 wall variant images (call once on init). */ + public static void ensureLoaded() { + if (loaded) return; + for (int i = 0; i < VARIANT_COUNT; i++) { + String path = "/com/openrobotics/img/wall_" + i + ".png"; + try (var s = ConnectedWallRenderer.class.getResourceAsStream(path)) { + VARIANTS[i] = (s != null) ? new Image(s) : null; + } catch (Exception ignored) {} + } + loaded = true; + } + + /** + * Draw the correct wall sprite for {@code entity} at canvas position + * ({@code sx}, {@code sy}) with size {@code tileSize × tileSize}. + */ + public static void draw(GraphicsContext gc, + double sx, double sy, double tileSize, + MapEntity entity, Map map) { + ensureLoaded(); + + int ex = entity.getPosition().getX(); + int ey = entity.getPosition().getY(); + + int mask = 0; + if (isWall(map, ex, ey - 1)) mask |= 1; // N + if (isWall(map, ex, ey + 1)) mask |= 2; // S + if (isWall(map, ex + 1, ey )) mask |= 4; // E + if (isWall(map, ex - 1, ey )) mask |= 8; // W + + Image img = VARIANTS[mask]; + if (img != null && !img.isError()) { + gc.drawImage(img, sx, sy, tileSize, tileSize); + } else { + // Fallback: solid dark rect + gc.setFill(javafx.scene.paint.Color.web("#6b5b4e")); + gc.fillRect(sx, sy, tileSize, tileSize); + } + } + + // ── helpers ────────────────────────────────────────────────────────── + + private static boolean isWall(Map map, int x, int y) { + if (map == null) return false; + try { + for (MapEntity e : map.getEntitiesAt(new Vector2D(x, y))) { + if (isWallLike(e)) return true; + } + } catch (Exception ignored) {} + return false; + } + + public static boolean isWallLike(MapEntity e) { + if (e instanceof Obstacle) return true; + String n = e.getName() == null ? "" : e.getName().toLowerCase(); + return n.contains("wall") || n.contains("obstacle"); + } +} diff --git a/open-robotics/src/main/java/com/openrobotics/util/RobotSpriteAnimator.java b/open-robotics/src/main/java/com/openrobotics/util/RobotSpriteAnimator.java new file mode 100644 index 00000000..578ecf06 --- /dev/null +++ b/open-robotics/src/main/java/com/openrobotics/util/RobotSpriteAnimator.java @@ -0,0 +1,112 @@ +package com.openrobotics.util; + +import com.openrobotics.common.Direction; +import javafx.scene.image.Image; +import javafx.scene.image.WritableImage; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +/** + * Loads robot and charger sprite sheets and slices the correct frame at runtime. + * + *

Sheet layout (robot sheets): + *

+ *   row 0 = South (DOWN)   row 1 = North (UP)
+ *   row 2 = East  (RIGHT)  row 3 = West  (LEFT)
+ *   columns = animation frames (0-based)
+ * 
+ * + *

Charger sheet: single row, columns = frames. + */ +public class RobotSpriteAnimator { + + private static final int FRAME_PX = 256; // each cell in the PNG is 256×256 + private static final int WALK_FRAMES = 12; + private static final int IDLE_FRAMES = 12; + private static final int CHARGE_FRAMES = 12; + + /** ms per frame for each animation type */ + private static final long WALK_FRAME_MS = 80; // 12 fps-ish walk + private static final long IDLE_FRAME_MS = 160; // slow idle sway + private static final long CHARGE_FRAME_MS = 400; // slow charge fill + + // Sheet images — loaded lazily once + private static Image walkSheet; + private static Image walkBoxSheet; + private static Image idleSheet; + private static Image chargerSheet; + private static boolean loaded = false; + + // Frame cache: "sheetKey_row_col" → WritableImage crop + private static final Map frameCache = new HashMap<>(); + + private static final EnumMap DIR_ROW = new EnumMap<>(Direction.class); + static { + DIR_ROW.put(Direction.DOWN, 0); // South + DIR_ROW.put(Direction.UP, 1); // North + DIR_ROW.put(Direction.RIGHT, 2); // East + DIR_ROW.put(Direction.LEFT, 3); // West + } + + public static void ensureLoaded() { + if (loaded) return; + walkSheet = loadSheet("robot_walk.png"); + walkBoxSheet = loadSheet("robot_walk_box.png"); + idleSheet = loadSheet("robot_idle.png"); + chargerSheet = loadSheet("charger_anim.png"); + loaded = true; + } + + private static Image loadSheet(String filename) { + try (var s = RobotSpriteAnimator.class.getResourceAsStream( + "/com/openrobotics/img/" + filename)) { + if (s == null) return null; + return new Image(s); + } catch (Exception e) { + return null; + } + } + + /** Robot walk frame (or walk-with-box). */ + public static Image walkFrame(Direction dir, boolean carrying) { + ensureLoaded(); + Image sheet = carrying ? walkBoxSheet : walkSheet; + if (sheet == null) return null; + int row = DIR_ROW.getOrDefault(dir, 0); + int col = (int) ((System.currentTimeMillis() / WALK_FRAME_MS) % WALK_FRAMES); + return crop(sheet, carrying ? "wb" : "w", row, col); + } + + /** Robot idle frame. */ + public static Image idleFrame(Direction dir) { + ensureLoaded(); + if (idleSheet == null) return null; + int row = DIR_ROW.getOrDefault(dir, 0); + int col = (int) ((System.currentTimeMillis() / IDLE_FRAME_MS) % IDLE_FRAMES); + return crop(idleSheet, "i", row, col); + } + + /** Charging-station animation frame (independent of direction). */ + public static Image chargerFrame() { + ensureLoaded(); + if (chargerSheet == null) return null; + int col = (int) ((System.currentTimeMillis() / CHARGE_FRAME_MS) % CHARGE_FRAMES); + return crop(chargerSheet, "c", 0, col); + } + + private static WritableImage crop(Image src, String key, int row, int col) { + String cacheKey = key + "_" + row + "_" + col; + return frameCache.computeIfAbsent(cacheKey, k -> { + int x = col * FRAME_PX; + int y = row * FRAME_PX; + int maxW = (int) src.getWidth() - x; + int maxH = (int) src.getHeight() - y; + if (maxW <= 0 || maxH <= 0) return null; + int w = Math.min(FRAME_PX, maxW); + int h = Math.min(FRAME_PX, maxH); + return new WritableImage(src.getPixelReader(), x, y, w, h); + }); + } +} diff --git a/open-robotics/src/main/resources/com/openrobotics/img/box.png b/open-robotics/src/main/resources/com/openrobotics/img/box.png new file mode 100644 index 00000000..c340e7ca Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/box.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/charger.png b/open-robotics/src/main/resources/com/openrobotics/img/charger.png index 01a7d33d..3b269f96 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/charger.png and b/open-robotics/src/main/resources/com/openrobotics/img/charger.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/charger_anim.png b/open-robotics/src/main/resources/com/openrobotics/img/charger_anim.png new file mode 100644 index 00000000..1e29357d Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/charger_anim.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/dock.png b/open-robotics/src/main/resources/com/openrobotics/img/dock.png index 0d090536..9636de38 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/dock.png and b/open-robotics/src/main/resources/com/openrobotics/img/dock.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/rack.png b/open-robotics/src/main/resources/com/openrobotics/img/rack.png new file mode 100644 index 00000000..e4d545d8 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/rack.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/rack_test_20.png b/open-robotics/src/main/resources/com/openrobotics/img/rack_test_20.png new file mode 100644 index 00000000..d65fbc40 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/rack_test_20.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/rack_test_35.png b/open-robotics/src/main/resources/com/openrobotics/img/rack_test_35.png new file mode 100644 index 00000000..9636de38 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/rack_test_35.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/rack_test_55.png b/open-robotics/src/main/resources/com/openrobotics/img/rack_test_55.png new file mode 100644 index 00000000..3bddbbc9 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/rack_test_55.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/robot.png b/open-robotics/src/main/resources/com/openrobotics/img/robot.png index 190e075b..e2e5c250 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/robot.png and b/open-robotics/src/main/resources/com/openrobotics/img/robot.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/robot_idle.png b/open-robotics/src/main/resources/com/openrobotics/img/robot_idle.png index 1c946fbe..e296a8c6 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/robot_idle.png and b/open-robotics/src/main/resources/com/openrobotics/img/robot_idle.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/robot_turn.png b/open-robotics/src/main/resources/com/openrobotics/img/robot_turn.png deleted file mode 100644 index 8dea6da3..00000000 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/robot_turn.png and /dev/null differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/robot_walk.png b/open-robotics/src/main/resources/com/openrobotics/img/robot_walk.png index 1c4c29f6..7601088e 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/robot_walk.png and b/open-robotics/src/main/resources/com/openrobotics/img/robot_walk.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/robot_walk_box.png b/open-robotics/src/main/resources/com/openrobotics/img/robot_walk_box.png new file mode 100644 index 00000000..f7a96865 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/robot_walk_box.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/shelf.png b/open-robotics/src/main/resources/com/openrobotics/img/shelf.png index d513d385..e4d545d8 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/shelf.png and b/open-robotics/src/main/resources/com/openrobotics/img/shelf.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/station.png b/open-robotics/src/main/resources/com/openrobotics/img/station.png index 14f5bd2a..9636de38 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/station.png and b/open-robotics/src/main/resources/com/openrobotics/img/station.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall.png b/open-robotics/src/main/resources/com/openrobotics/img/wall.png index 1aad023e..2b1ca0bf 100644 Binary files a/open-robotics/src/main/resources/com/openrobotics/img/wall.png and b/open-robotics/src/main/resources/com/openrobotics/img/wall.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_0.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_0.png new file mode 100644 index 00000000..cad5b2e6 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_0.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_1.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_1.png new file mode 100644 index 00000000..95b9b8c4 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_1.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_10.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_10.png new file mode 100644 index 00000000..d5f0f711 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_10.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_11.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_11.png new file mode 100644 index 00000000..8c3c0c94 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_11.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_12.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_12.png new file mode 100644 index 00000000..2b1ca0bf Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_12.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_13.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_13.png new file mode 100644 index 00000000..0a38f855 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_13.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_14.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_14.png new file mode 100644 index 00000000..82a3d391 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_14.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_15.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_15.png new file mode 100644 index 00000000..7a9eff4e Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_15.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_2.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_2.png new file mode 100644 index 00000000..42cf5977 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_2.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_3.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_3.png new file mode 100644 index 00000000..b61802d2 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_3.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_4.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_4.png new file mode 100644 index 00000000..f85ea860 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_4.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_5.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_5.png new file mode 100644 index 00000000..863b6aef Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_5.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_6.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_6.png new file mode 100644 index 00000000..6574138e Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_6.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_7.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_7.png new file mode 100644 index 00000000..8de7b9fb Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_7.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_8.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_8.png new file mode 100644 index 00000000..83630a39 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_8.png differ diff --git a/open-robotics/src/main/resources/com/openrobotics/img/wall_9.png b/open-robotics/src/main/resources/com/openrobotics/img/wall_9.png new file mode 100644 index 00000000..ba9d02f0 Binary files /dev/null and b/open-robotics/src/main/resources/com/openrobotics/img/wall_9.png differ