@apocaliss92/nodedreame 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/support/version.ts
2
2
  var LIBRARY_NAME = "nodedreame";
3
- var LIBRARY_VERSION = "1.8.1";
3
+ var LIBRARY_VERSION = "1.10.0";
4
4
 
5
5
  // src/transport/errors.ts
6
6
  var DreameError = class extends Error {
@@ -2552,6 +2552,157 @@ function decodeCleanedAreaPixels(pixels, width, height) {
2552
2552
  return { cleaned, dirty };
2553
2553
  }
2554
2554
 
2555
+ // src/models/vacuum/map/merge.ts
2556
+ var OutOfOrderFrameError = class extends Error {
2557
+ expectedFrameId;
2558
+ actualFrameId;
2559
+ constructor(expectedFrameId, actualFrameId) {
2560
+ super(`map: out-of-order P-frame (expected frame_id=${expectedFrameId}, got ${actualFrameId})`);
2561
+ this.name = "OutOfOrderFrameError";
2562
+ this.expectedFrameId = expectedFrameId;
2563
+ this.actualFrameId = actualFrameId;
2564
+ }
2565
+ };
2566
+ function mergePFrame(prevInflated, pFrameInflated) {
2567
+ const prevHeader = parseMapHeader(prevInflated);
2568
+ const pHeader = parseMapHeader(pFrameInflated);
2569
+ if (pHeader.frameType !== "P") {
2570
+ throw new MapDecodeError(`mergePFrame: expected P-frame, got frame_type=${pHeader.frameType}`);
2571
+ }
2572
+ if (pHeader.mapId !== prevHeader.mapId) {
2573
+ throw new MapDecodeError(
2574
+ `mergePFrame: map_id mismatch (prev=${prevHeader.mapId}, P=${pHeader.mapId}) \u2014 request a fresh I-frame`
2575
+ );
2576
+ }
2577
+ if (pHeader.frameId !== prevHeader.frameId + 1) {
2578
+ throw new OutOfOrderFrameError(prevHeader.frameId + 1, pHeader.frameId);
2579
+ }
2580
+ if (prevHeader.gridSize !== pHeader.gridSize && pHeader.width > 0 && pHeader.height > 0) {
2581
+ throw new MapDecodeError(
2582
+ `mergePFrame: grid_size changed mid-stream (${prevHeader.gridSize} \u2192 ${pHeader.gridSize})`
2583
+ );
2584
+ }
2585
+ const prevTail = parseMapJsonTail(sliceTailText(prevInflated, prevHeader));
2586
+ const pTail = parseMapJsonTail(sliceTailText(pFrameInflated, pHeader));
2587
+ const prevLeft = prevTail.origin?.[0] ?? prevHeader.left;
2588
+ const prevTop = prevTail.origin?.[1] ?? prevHeader.top;
2589
+ const grid = prevHeader.gridSize;
2590
+ const prevRight = prevLeft + prevHeader.width * grid;
2591
+ const prevBottom = prevTop + prevHeader.height * grid;
2592
+ const hasPixelDelta = pHeader.width > 0 && pHeader.height > 0;
2593
+ let unionLeft = prevLeft;
2594
+ let unionTop = prevTop;
2595
+ let unionWidth = prevHeader.width;
2596
+ let unionHeight = prevHeader.height;
2597
+ let pLeft = 0;
2598
+ let pTop = 0;
2599
+ if (hasPixelDelta) {
2600
+ pLeft = pTail.origin?.[0] ?? pHeader.left;
2601
+ pTop = pTail.origin?.[1] ?? pHeader.top;
2602
+ const pRight = pLeft + pHeader.width * grid;
2603
+ const pBottom = pTop + pHeader.height * grid;
2604
+ if ((pLeft - prevLeft) % grid !== 0 || (pTop - prevTop) % grid !== 0) {
2605
+ throw new MapDecodeError(
2606
+ `mergePFrame: P-frame origin not aligned to prev grid (offset=${pLeft - prevLeft},${pTop - prevTop} vs grid=${grid})`
2607
+ );
2608
+ }
2609
+ unionLeft = Math.min(prevLeft, pLeft);
2610
+ unionTop = Math.min(prevTop, pTop);
2611
+ const unionRight = Math.max(prevRight, pRight);
2612
+ const unionBottom = Math.max(prevBottom, pBottom);
2613
+ unionWidth = (unionRight - unionLeft) / grid;
2614
+ unionHeight = (unionBottom - unionTop) / grid;
2615
+ }
2616
+ const newPixels = Buffer.alloc(unionWidth * unionHeight);
2617
+ const prevPixelEnd = HEADER_SIZE + prevHeader.width * prevHeader.height;
2618
+ if (prevInflated.length < prevPixelEnd) {
2619
+ throw new MapDecodeError(
2620
+ `mergePFrame: prev buffer truncated (need ${prevPixelEnd} bytes for header+pixels, got ${prevInflated.length})`
2621
+ );
2622
+ }
2623
+ const prevPixels = prevInflated.subarray(HEADER_SIZE, prevPixelEnd);
2624
+ const prevDxPx = (prevLeft - unionLeft) / grid;
2625
+ const prevDyPx = (prevTop - unionTop) / grid;
2626
+ for (let y = 0; y < prevHeader.height; y++) {
2627
+ const srcOff = y * prevHeader.width;
2628
+ const dstOff = (prevDyPx + y) * unionWidth + prevDxPx;
2629
+ prevPixels.copy(newPixels, dstOff, srcOff, srcOff + prevHeader.width);
2630
+ }
2631
+ if (hasPixelDelta) {
2632
+ const pPixelEnd = HEADER_SIZE + pHeader.width * pHeader.height;
2633
+ if (pFrameInflated.length < pPixelEnd) {
2634
+ throw new MapDecodeError(
2635
+ `mergePFrame: P buffer truncated (need ${pPixelEnd} bytes for header+pixels, got ${pFrameInflated.length})`
2636
+ );
2637
+ }
2638
+ const pPixels = pFrameInflated.subarray(HEADER_SIZE, pPixelEnd);
2639
+ const pDxPx = (pLeft - unionLeft) / grid;
2640
+ const pDyPx = (pTop - unionTop) / grid;
2641
+ for (let y = 0; y < pHeader.height; y++) {
2642
+ const dstRow = (pDyPx + y) * unionWidth + pDxPx;
2643
+ const srcRow = y * pHeader.width;
2644
+ for (let x = 0; x < pHeader.width; x++) {
2645
+ newPixels[dstRow + x] = newPixels[dstRow + x] + pPixels[srcRow + x] & 255;
2646
+ }
2647
+ }
2648
+ }
2649
+ const newHeader = Buffer.alloc(HEADER_SIZE);
2650
+ newHeader.writeInt16LE(prevHeader.mapId, 0);
2651
+ newHeader.writeInt16LE(pHeader.frameId, 2);
2652
+ newHeader[4] = FRAME_TYPE.I;
2653
+ newHeader.writeInt16LE(pHeader.robotX, 5);
2654
+ newHeader.writeInt16LE(pHeader.robotY, 7);
2655
+ newHeader.writeInt16LE(pHeader.robotA, 9);
2656
+ newHeader.writeInt16LE(pHeader.chargerX, 11);
2657
+ newHeader.writeInt16LE(pHeader.chargerY, 13);
2658
+ newHeader.writeInt16LE(pHeader.chargerA, 15);
2659
+ newHeader.writeInt16LE(grid, 17);
2660
+ newHeader.writeInt16LE(unionWidth, 19);
2661
+ newHeader.writeInt16LE(unionHeight, 21);
2662
+ newHeader.writeInt16LE(unionLeft, 23);
2663
+ newHeader.writeInt16LE(unionTop, 25);
2664
+ const mergedTail = mergeTails(prevTail, pTail, unionLeft, unionTop);
2665
+ const tailBytes = Buffer.from(JSON.stringify(mergedTail), "utf8");
2666
+ return Buffer.concat([newHeader, newPixels, tailBytes]);
2667
+ }
2668
+ function mergePFrameEnvelope(prev, pframe, prevOpts, pframeOpts) {
2669
+ const prevBuf = typeof prev === "string" ? unwrapEnvelope(prev, prevOpts) : prev;
2670
+ const pBuf = typeof pframe === "string" ? unwrapEnvelope(pframe, pframeOpts) : pframe;
2671
+ return mergePFrame(prevBuf, pBuf);
2672
+ }
2673
+ function mergeTails(prev, p, unionLeft, unionTop) {
2674
+ const merged = { ...p };
2675
+ merged.origin = [unionLeft, unionTop];
2676
+ const prevTr = typeof prev.tr === "string" ? prev.tr : "";
2677
+ const pTr = typeof p.tr === "string" ? p.tr : "";
2678
+ if (prevTr || pTr) {
2679
+ merged.tr = prevTr + pTr;
2680
+ } else {
2681
+ delete merged.tr;
2682
+ }
2683
+ if (!("seg_inf" in p) && "seg_inf" in prev) {
2684
+ merged.seg_inf = prev.seg_inf;
2685
+ }
2686
+ if (!("sa" in p) && "sa" in prev) {
2687
+ merged.sa = prev.sa;
2688
+ }
2689
+ for (const key2 of PERSISTENT_TAIL_KEYS) {
2690
+ if (!(key2 in p) && key2 in prev) {
2691
+ merged[key2] = prev[key2];
2692
+ }
2693
+ }
2694
+ return merged;
2695
+ }
2696
+ var PERSISTENT_TAIL_KEYS = [
2697
+ "vw",
2698
+ "vws",
2699
+ "sneak_areas",
2700
+ "sneak_areas_end",
2701
+ "walls_info",
2702
+ "rism",
2703
+ "decmap"
2704
+ ];
2705
+
2555
2706
  // src/models/vacuum/map/decode.ts
2556
2707
  function decodeVacuumMap(input, opts = {}) {
2557
2708
  const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
@@ -2596,6 +2747,10 @@ function decodeVacuumMap(input, opts = {}) {
2596
2747
  cleanedArea
2597
2748
  };
2598
2749
  }
2750
+ function applyVacuumPFrame(prev, pframe, opts = {}) {
2751
+ const merged = typeof prev === "string" || typeof pframe === "string" ? mergePFrameEnvelope(prev, pframe, opts.prev, opts.pframe) : mergePFrame(prev, pframe);
2752
+ return { buffer: merged, data: decodeVacuumMap(merged) };
2753
+ }
2599
2754
  function mergeDimensions(header, tail) {
2600
2755
  const left = tail.origin?.[0] ?? header.left;
2601
2756
  const top = tail.origin?.[1] ?? header.top;
@@ -2616,10 +2771,79 @@ function pose(x, y, angle, absent) {
2616
2771
 
2617
2772
  // src/models/vacuum/map/render.ts
2618
2773
  import { PNG } from "pngjs";
2619
- var RGBA = {
2620
- wall: [60, 60, 60, 255],
2621
- floor: [210, 222, 235, 255],
2622
- carpet: [180, 150, 120, 200]
2774
+ var SCHEMES = {
2775
+ "dreame-light": {
2776
+ floor: [210, 222, 235, 255],
2777
+ wall: [60, 60, 60, 255],
2778
+ carpet: [180, 150, 120, 200],
2779
+ segSat: 0.45,
2780
+ segVal: 0.95,
2781
+ path: [60, 110, 180, 235],
2782
+ robotBody: [40, 70, 110, 255],
2783
+ robotHeading: [255, 255, 255, 255],
2784
+ charger: [40, 160, 80, 255],
2785
+ noGoFill: [220, 60, 60, 70],
2786
+ noGoBorder: [200, 40, 40, 220],
2787
+ noMopFill: [60, 120, 220, 60],
2788
+ noMopBorder: [40, 90, 200, 200],
2789
+ virtualWall: [200, 40, 40, 230],
2790
+ obstacle: [230, 150, 30, 255],
2791
+ label: [40, 40, 40, 255]
2792
+ },
2793
+ "dreame-dark": {
2794
+ floor: [40, 46, 56, 255],
2795
+ wall: [15, 15, 18, 255],
2796
+ carpet: [80, 66, 52, 200],
2797
+ segSat: 0.5,
2798
+ segVal: 0.62,
2799
+ path: [120, 170, 230, 235],
2800
+ robotBody: [120, 170, 230, 255],
2801
+ robotHeading: [20, 24, 30, 255],
2802
+ charger: [60, 200, 110, 255],
2803
+ noGoFill: [220, 70, 70, 80],
2804
+ noGoBorder: [230, 70, 70, 220],
2805
+ noMopFill: [70, 130, 230, 70],
2806
+ noMopBorder: [80, 140, 235, 210],
2807
+ virtualWall: [230, 70, 70, 235],
2808
+ obstacle: [240, 180, 60, 255],
2809
+ label: [225, 230, 240, 255]
2810
+ },
2811
+ "mijia-light": {
2812
+ floor: [225, 232, 240, 255],
2813
+ wall: [70, 78, 92, 255],
2814
+ carpet: [188, 162, 132, 200],
2815
+ segSat: 0.35,
2816
+ segVal: 0.98,
2817
+ path: [80, 130, 200, 230],
2818
+ robotBody: [30, 100, 200, 255],
2819
+ robotHeading: [255, 255, 255, 255],
2820
+ charger: [40, 170, 90, 255],
2821
+ noGoFill: [225, 70, 70, 70],
2822
+ noGoBorder: [205, 45, 45, 220],
2823
+ noMopFill: [70, 130, 225, 60],
2824
+ noMopBorder: [50, 100, 205, 200],
2825
+ virtualWall: [205, 45, 45, 230],
2826
+ obstacle: [235, 160, 35, 255],
2827
+ label: [45, 52, 64, 255]
2828
+ },
2829
+ tasshack: {
2830
+ floor: [200, 210, 225, 255],
2831
+ wall: [80, 80, 95, 255],
2832
+ carpet: [176, 148, 118, 205],
2833
+ segSat: 0.5,
2834
+ segVal: 0.9,
2835
+ path: [50, 100, 170, 235],
2836
+ robotBody: [35, 60, 100, 255],
2837
+ robotHeading: [255, 255, 255, 255],
2838
+ charger: [35, 150, 75, 255],
2839
+ noGoFill: [215, 55, 55, 75],
2840
+ noGoBorder: [195, 35, 35, 220],
2841
+ noMopFill: [55, 115, 215, 65],
2842
+ noMopBorder: [35, 85, 195, 205],
2843
+ virtualWall: [195, 35, 35, 230],
2844
+ obstacle: [225, 145, 25, 255],
2845
+ label: [35, 42, 54, 255]
2846
+ }
2623
2847
  };
2624
2848
  function hsvToRgba(h, s, v) {
2625
2849
  const c = v * s;
@@ -2650,49 +2874,230 @@ function hsvToRgba(h, s, v) {
2650
2874
  const m = v - c;
2651
2875
  return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), 255];
2652
2876
  }
2653
- function segmentColor(id) {
2877
+ function segmentColor(id, pal) {
2654
2878
  const hue = id * 137.508 % 360;
2655
- return hsvToRgba(hue, 0.45, 0.95);
2879
+ return hsvToRgba(hue, pal.segSat, pal.segVal);
2656
2880
  }
2657
2881
  function renderVacuumPng(map, opts = {}) {
2658
2882
  const scale = Math.max(1, Math.trunc(opts.scale ?? 1));
2659
- const w = map.dimensions.width * scale;
2660
- const h = map.dimensions.height * scale;
2661
- const png = new PNG({ width: Math.max(1, w), height: Math.max(1, h) });
2883
+ const pal = SCHEMES[opts.colorScheme ?? "dreame-light"];
2884
+ const w = Math.max(1, map.dimensions.width * scale);
2885
+ const h = Math.max(1, map.dimensions.height * scale);
2886
+ const png = new PNG({ width: w, height: h });
2662
2887
  png.data.fill(0);
2663
2888
  for (const layer of map.layers) {
2664
- paintLayer(png, layer, scale);
2889
+ paintLayer(png, layer, scale, pal);
2890
+ }
2891
+ const dim = map.dimensions;
2892
+ if (opts.showNoGo !== false) {
2893
+ for (const area of map.restrictedAreas) {
2894
+ const fill = area.kind === "noMop" ? pal.noMopFill : pal.noGoFill;
2895
+ const border = area.kind === "noMop" ? pal.noMopBorder : pal.noGoBorder;
2896
+ const a = worldToPx(area.bbox.xMin, area.bbox.yMin, dim, scale);
2897
+ const b = worldToPx(area.bbox.xMax, area.bbox.yMax, dim, scale);
2898
+ fillRect(png, a.x, a.y, b.x, b.y, fill);
2899
+ strokeRect(png, a.x, a.y, b.x, b.y, border);
2900
+ }
2901
+ }
2902
+ if (opts.showVirtualWalls !== false) {
2903
+ for (const vw of map.virtualWalls) {
2904
+ const a = worldToPx(vw.from.x, vw.from.y, dim, scale);
2905
+ const b = worldToPx(vw.to.x, vw.to.y, dim, scale);
2906
+ drawLine(png, a.x, a.y, b.x, b.y, pal.virtualWall, Math.max(1, scale));
2907
+ }
2908
+ }
2909
+ if (opts.showPath !== false) {
2910
+ for (const path of map.paths) {
2911
+ drawPolyline(png, path.points, dim, scale, pal.path, Math.max(1, scale));
2912
+ }
2913
+ }
2914
+ if (opts.showObstacles !== false) {
2915
+ for (const ob of map.obstacles) {
2916
+ const p = worldToPx(ob.x, ob.y, dim, scale);
2917
+ drawDisk(png, p.x, p.y, Math.max(2, scale * 2), pal.obstacle);
2918
+ }
2919
+ }
2920
+ if (opts.showCharger !== false && map.dock !== null) {
2921
+ drawCharger(png, map.dock, dim, scale, pal);
2922
+ }
2923
+ if (opts.showRobot !== false && map.robot !== null) {
2924
+ drawRobot(png, map.robot, dim, scale, pal);
2925
+ }
2926
+ if (opts.showSegmentLabels === true) {
2927
+ for (const seg of map.segments) {
2928
+ const p = worldToPx(seg.centroid.x, seg.centroid.y, dim, scale);
2929
+ drawLabel(png, p.x, p.y, String(seg.id), pal.label, Math.max(1, scale));
2930
+ }
2665
2931
  }
2666
2932
  return PNG.sync.write(png);
2667
2933
  }
2668
- function paintLayer(png, layer, scale) {
2669
- const color = layer.type === "segment" ? segmentColor(layer.segmentId ?? 0) : RGBA[layer.type];
2934
+ function worldToPx(worldX, worldY, dim, scale) {
2935
+ return {
2936
+ x: Math.round((worldX - dim.left) / dim.gridSize * scale),
2937
+ y: Math.round((worldY - dim.top) / dim.gridSize * scale)
2938
+ };
2939
+ }
2940
+ function paintLayer(png, layer, scale, pal) {
2941
+ const color = layer.type === "segment" ? segmentColor(layer.segmentId ?? 0, pal) : layer.type === "wall" ? pal.wall : layer.type === "carpet" ? pal.carpet : pal.floor;
2670
2942
  for (const run of layer.runs) {
2671
- paintRun(png, run, color, scale, png.width, png.height);
2943
+ paintRun(png, run, color, scale);
2672
2944
  }
2673
2945
  }
2674
- function paintRun(png, run, color, scale, width, height) {
2946
+ function paintRun(png, run, color, scale) {
2675
2947
  const [xPx, yPx, len] = run;
2676
2948
  for (let i = 0; i < len; i += 1) {
2677
2949
  const baseX = (xPx + i) * scale;
2678
2950
  const baseY = yPx * scale;
2679
2951
  for (let dy = 0; dy < scale; dy += 1) {
2680
- const py = baseY + dy;
2681
- if (py < 0 || py >= height) {
2682
- continue;
2683
- }
2684
2952
  for (let dx = 0; dx < scale; dx += 1) {
2685
- const px = baseX + dx;
2686
- if (px < 0 || px >= width) {
2687
- continue;
2953
+ setPixel(png, baseX + dx, baseY + dy, color);
2954
+ }
2955
+ }
2956
+ }
2957
+ }
2958
+ function setPixel(png, x, y, color) {
2959
+ if (x < 0 || y < 0 || x >= png.width || y >= png.height) return;
2960
+ const off = (y * png.width + x) * 4;
2961
+ const a = color[3];
2962
+ if (a >= 255) {
2963
+ png.data[off] = color[0];
2964
+ png.data[off + 1] = color[1];
2965
+ png.data[off + 2] = color[2];
2966
+ png.data[off + 3] = 255;
2967
+ return;
2968
+ }
2969
+ const sa = a / 255;
2970
+ const da = (png.data[off + 3] ?? 0) / 255;
2971
+ const outA = sa + da * (1 - sa);
2972
+ if (outA <= 0) return;
2973
+ for (let k = 0; k < 3; k += 1) {
2974
+ const src = color[k] ?? 0;
2975
+ const dst = png.data[off + k] ?? 0;
2976
+ png.data[off + k] = Math.round((src * sa + dst * da * (1 - sa)) / outA);
2977
+ }
2978
+ png.data[off + 3] = Math.round(outA * 255);
2979
+ }
2980
+ function fillRect(png, x0, y0, x1, y1, color) {
2981
+ const xa = Math.min(x0, x1);
2982
+ const xb = Math.max(x0, x1);
2983
+ const ya = Math.min(y0, y1);
2984
+ const yb = Math.max(y0, y1);
2985
+ for (let y = ya; y <= yb; y += 1) {
2986
+ for (let x = xa; x <= xb; x += 1) {
2987
+ setPixel(png, x, y, color);
2988
+ }
2989
+ }
2990
+ }
2991
+ function strokeRect(png, x0, y0, x1, y1, color) {
2992
+ const xa = Math.min(x0, x1);
2993
+ const xb = Math.max(x0, x1);
2994
+ const ya = Math.min(y0, y1);
2995
+ const yb = Math.max(y0, y1);
2996
+ for (let x = xa; x <= xb; x += 1) {
2997
+ setPixel(png, x, ya, color);
2998
+ setPixel(png, x, yb, color);
2999
+ }
3000
+ for (let y = ya; y <= yb; y += 1) {
3001
+ setPixel(png, xa, y, color);
3002
+ setPixel(png, xb, y, color);
3003
+ }
3004
+ }
3005
+ function drawLine(png, x0, y0, x1, y1, color, thickness) {
3006
+ let x = x0;
3007
+ let y = y0;
3008
+ const dx = Math.abs(x1 - x0);
3009
+ const dy = -Math.abs(y1 - y0);
3010
+ const sx = x0 < x1 ? 1 : -1;
3011
+ const sy = y0 < y1 ? 1 : -1;
3012
+ let err = dx + dy;
3013
+ const r = Math.max(0, Math.trunc((thickness - 1) / 2));
3014
+ for (; ; ) {
3015
+ for (let oy = -r; oy <= r; oy += 1) {
3016
+ for (let ox = -r; ox <= r; ox += 1) {
3017
+ setPixel(png, x + ox, y + oy, color);
3018
+ }
3019
+ }
3020
+ if (x === x1 && y === y1) break;
3021
+ const e2 = 2 * err;
3022
+ if (e2 >= dy) {
3023
+ err += dy;
3024
+ x += sx;
3025
+ }
3026
+ if (e2 <= dx) {
3027
+ err += dx;
3028
+ y += sy;
3029
+ }
3030
+ }
3031
+ }
3032
+ function drawPolyline(png, points, dim, scale, color, thickness) {
3033
+ for (let i = 1; i < points.length; i += 1) {
3034
+ const p0 = points[i - 1];
3035
+ const p1 = points[i];
3036
+ if (p0 === void 0 || p1 === void 0) continue;
3037
+ const a = worldToPx(p0.x, p0.y, dim, scale);
3038
+ const b = worldToPx(p1.x, p1.y, dim, scale);
3039
+ drawLine(png, a.x, a.y, b.x, b.y, color, thickness);
3040
+ }
3041
+ }
3042
+ function drawDisk(png, cx, cy, r, color) {
3043
+ const rr = r * r;
3044
+ for (let dy = -r; dy <= r; dy += 1) {
3045
+ for (let dx = -r; dx <= r; dx += 1) {
3046
+ if (dx * dx + dy * dy <= rr) setPixel(png, cx + dx, cy + dy, color);
3047
+ }
3048
+ }
3049
+ }
3050
+ function drawRobot(png, robot, dim, scale, pal) {
3051
+ const p = worldToPx(robot.x, robot.y, dim, scale);
3052
+ const r = Math.max(3, scale * 3);
3053
+ drawDisk(png, p.x, p.y, r, pal.robotBody);
3054
+ const rad = robot.angle * Math.PI / 180;
3055
+ const hx = Math.round(p.x + Math.cos(rad) * r);
3056
+ const hy = Math.round(p.y + Math.sin(rad) * r);
3057
+ drawLine(png, p.x, p.y, hx, hy, pal.robotHeading, Math.max(1, Math.trunc(scale / 2) + 1));
3058
+ }
3059
+ function drawCharger(png, dock, dim, scale, pal) {
3060
+ const p = worldToPx(dock.x, dock.y, dim, scale);
3061
+ const r = Math.max(2, scale * 2);
3062
+ fillRect(png, p.x - r, p.y - r, p.x + r, p.y + r, pal.charger);
3063
+ }
3064
+ var GLYPHS = {
3065
+ "0": [7, 5, 5, 5, 7],
3066
+ "1": [2, 6, 2, 2, 7],
3067
+ "2": [7, 1, 7, 4, 7],
3068
+ "3": [7, 1, 7, 1, 7],
3069
+ "4": [5, 5, 7, 1, 1],
3070
+ "5": [7, 4, 7, 1, 7],
3071
+ "6": [7, 4, 7, 5, 7],
3072
+ "7": [7, 1, 2, 2, 2],
3073
+ "8": [7, 5, 7, 5, 7],
3074
+ "9": [7, 5, 7, 1, 7],
3075
+ "-": [0, 0, 7, 0, 0]
3076
+ };
3077
+ function drawLabel(png, cx, cy, text, color, px) {
3078
+ const glyphW = 3 * px;
3079
+ const glyphH = 5 * px;
3080
+ const gap = px;
3081
+ const totalW = text.length * glyphW + Math.max(0, text.length - 1) * gap;
3082
+ let originX = Math.round(cx - totalW / 2);
3083
+ const originY = Math.round(cy - glyphH / 2);
3084
+ for (const ch of text) {
3085
+ const glyph = GLYPHS[ch];
3086
+ if (glyph !== void 0) {
3087
+ for (let row = 0; row < 5; row += 1) {
3088
+ const bits = glyph[row];
3089
+ if (bits === void 0) continue;
3090
+ for (let col = 0; col < 3; col += 1) {
3091
+ if ((bits & 1 << 2 - col) === 0) continue;
3092
+ for (let sy = 0; sy < px; sy += 1) {
3093
+ for (let sx = 0; sx < px; sx += 1) {
3094
+ setPixel(png, originX + col * px + sx, originY + row * px + sy, color);
3095
+ }
3096
+ }
2688
3097
  }
2689
- const off = (py * width + px) * 4;
2690
- png.data[off] = color[0];
2691
- png.data[off + 1] = color[1];
2692
- png.data[off + 2] = color[2];
2693
- png.data[off + 3] = color[3];
2694
3098
  }
2695
3099
  }
3100
+ originX += glyphW + gap;
2696
3101
  }
2697
3102
  }
2698
3103
 
@@ -3283,6 +3688,14 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
3283
3688
  */
3284
3689
  async getMap(input) {
3285
3690
  this.#requireCap(this.#caps.canMap, "getMap", "map decoding");
3691
+ const blob = await this.#fetchMapBlob(input);
3692
+ const map = decodeVacuumMap(blob, this.#decodeOpts(input));
3693
+ this.#lastMap = map;
3694
+ this.emit("map", map);
3695
+ return map;
3696
+ }
3697
+ /** Fetch the raw OSS blob (still the base64+zlib envelope) for a map frame. */
3698
+ async #fetchMapBlob(input) {
3286
3699
  const session = this.currentSession();
3287
3700
  const region = this.region;
3288
3701
  const fetcher = input.fetcher ?? new OssFetcher();
@@ -3296,14 +3709,77 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
3296
3709
  ...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
3297
3710
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3298
3711
  };
3299
- const blob = await fetcher.fetchBlob(fetchInput);
3300
- const map = decodeVacuumMap(blob, {
3712
+ return fetcher.fetchBlob(fetchInput);
3713
+ }
3714
+ /** The AES key/iv decode options, threaded from a {@link VacuumGetMapInput}. */
3715
+ #decodeOpts(input) {
3716
+ return {
3301
3717
  ...input.key !== void 0 ? { key: input.key } : {},
3302
3718
  ...input.iv !== void 0 ? { iv: input.iv } : {}
3303
- });
3304
- this.#lastMap = map;
3305
- this.emit("map", map);
3306
- return map;
3719
+ };
3720
+ }
3721
+ /**
3722
+ * The merge base for live P-frame streaming — always an INFLATED frame buffer.
3723
+ * An I-frame (re)seeds it; each P-frame merge replaces it with the merged
3724
+ * inflated buffer (so further P-frames stack). `null` until the first I-frame
3725
+ * (or after an out-of-order / map-id reset).
3726
+ */
3727
+ #mapStreamBase = null;
3728
+ /** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
3729
+ resetMapStream() {
3730
+ this.#mapStreamBase = null;
3731
+ }
3732
+ /** Inflate an OSS blob to a raw frame buffer (live envelope → zlib; verbatim if already inflated). */
3733
+ #inflateFrame(blob, opts) {
3734
+ return looksLikeBase64Zlib(blob) ? unwrapEnvelope(blob.toString("latin1"), opts) : blob;
3735
+ }
3736
+ /**
3737
+ * Fetch the latest advertised map frame and fold it into a continuously
3738
+ * updating map. An I-frame (re)seeds the merge base and renders standalone; a
3739
+ * P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
3740
+ * grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
3741
+ * out-of-order P-frame or a map-id change the base is dropped and `null`
3742
+ * returned — the next I-frame re-seeds. Returns `null` when no frame is
3743
+ * advertised yet, or a P-frame arrives before any I-frame.
3744
+ *
3745
+ * Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
3746
+ * map-watching consumer (e.g. the camstack map child) gets a fresh complete
3747
+ * frame on every push during a live clean.
3748
+ */
3749
+ async fetchLatestMapStreaming(opts = {}) {
3750
+ this.#requireCap(this.#caps.canMap, "fetchLatestMapStreaming", "map decoding");
3751
+ const filename = this.mapFilename;
3752
+ if (filename === null) return null;
3753
+ const input = { filename, ...opts };
3754
+ const decodeOpts = this.#decodeOpts(input);
3755
+ const blob = await this.#fetchMapBlob(input);
3756
+ const inflated = this.#inflateFrame(blob, decodeOpts);
3757
+ const frame = decodeVacuumMap(inflated, decodeOpts);
3758
+ if (frame.frameType === "I") {
3759
+ this.#mapStreamBase = inflated;
3760
+ this.#lastMap = frame;
3761
+ this.emit("map", frame);
3762
+ return frame;
3763
+ }
3764
+ if (frame.frameType === "P") {
3765
+ if (this.#mapStreamBase === null) return null;
3766
+ try {
3767
+ const { buffer, data } = applyVacuumPFrame(this.#mapStreamBase, inflated);
3768
+ this.#mapStreamBase = buffer;
3769
+ this.#lastMap = data;
3770
+ this.emit("map", data);
3771
+ return data;
3772
+ } catch (err) {
3773
+ if (err instanceof OutOfOrderFrameError || err instanceof MapDecodeError) {
3774
+ this.#mapStreamBase = null;
3775
+ return null;
3776
+ }
3777
+ throw err;
3778
+ }
3779
+ }
3780
+ this.#lastMap = frame;
3781
+ this.emit("map", frame);
3782
+ return frame;
3307
3783
  }
3308
3784
  /** Props worth seeding on start() / polling — exported for the facade. */
3309
3785
  static DEFAULT_PROPS = [