@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.cjs +509 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -5
- package/dist/index.d.ts +46 -5
- package/dist/index.js +509 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -81,7 +81,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
81
81
|
|
|
82
82
|
// src/support/version.ts
|
|
83
83
|
var LIBRARY_NAME = "nodedreame";
|
|
84
|
-
var LIBRARY_VERSION = "1.
|
|
84
|
+
var LIBRARY_VERSION = "1.10.0";
|
|
85
85
|
|
|
86
86
|
// src/transport/errors.ts
|
|
87
87
|
var DreameError = class extends Error {
|
|
@@ -2633,6 +2633,157 @@ function decodeCleanedAreaPixels(pixels, width, height) {
|
|
|
2633
2633
|
return { cleaned, dirty };
|
|
2634
2634
|
}
|
|
2635
2635
|
|
|
2636
|
+
// src/models/vacuum/map/merge.ts
|
|
2637
|
+
var OutOfOrderFrameError = class extends Error {
|
|
2638
|
+
expectedFrameId;
|
|
2639
|
+
actualFrameId;
|
|
2640
|
+
constructor(expectedFrameId, actualFrameId) {
|
|
2641
|
+
super(`map: out-of-order P-frame (expected frame_id=${expectedFrameId}, got ${actualFrameId})`);
|
|
2642
|
+
this.name = "OutOfOrderFrameError";
|
|
2643
|
+
this.expectedFrameId = expectedFrameId;
|
|
2644
|
+
this.actualFrameId = actualFrameId;
|
|
2645
|
+
}
|
|
2646
|
+
};
|
|
2647
|
+
function mergePFrame(prevInflated, pFrameInflated) {
|
|
2648
|
+
const prevHeader = parseMapHeader(prevInflated);
|
|
2649
|
+
const pHeader = parseMapHeader(pFrameInflated);
|
|
2650
|
+
if (pHeader.frameType !== "P") {
|
|
2651
|
+
throw new MapDecodeError(`mergePFrame: expected P-frame, got frame_type=${pHeader.frameType}`);
|
|
2652
|
+
}
|
|
2653
|
+
if (pHeader.mapId !== prevHeader.mapId) {
|
|
2654
|
+
throw new MapDecodeError(
|
|
2655
|
+
`mergePFrame: map_id mismatch (prev=${prevHeader.mapId}, P=${pHeader.mapId}) \u2014 request a fresh I-frame`
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
if (pHeader.frameId !== prevHeader.frameId + 1) {
|
|
2659
|
+
throw new OutOfOrderFrameError(prevHeader.frameId + 1, pHeader.frameId);
|
|
2660
|
+
}
|
|
2661
|
+
if (prevHeader.gridSize !== pHeader.gridSize && pHeader.width > 0 && pHeader.height > 0) {
|
|
2662
|
+
throw new MapDecodeError(
|
|
2663
|
+
`mergePFrame: grid_size changed mid-stream (${prevHeader.gridSize} \u2192 ${pHeader.gridSize})`
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
const prevTail = parseMapJsonTail(sliceTailText(prevInflated, prevHeader));
|
|
2667
|
+
const pTail = parseMapJsonTail(sliceTailText(pFrameInflated, pHeader));
|
|
2668
|
+
const prevLeft = prevTail.origin?.[0] ?? prevHeader.left;
|
|
2669
|
+
const prevTop = prevTail.origin?.[1] ?? prevHeader.top;
|
|
2670
|
+
const grid = prevHeader.gridSize;
|
|
2671
|
+
const prevRight = prevLeft + prevHeader.width * grid;
|
|
2672
|
+
const prevBottom = prevTop + prevHeader.height * grid;
|
|
2673
|
+
const hasPixelDelta = pHeader.width > 0 && pHeader.height > 0;
|
|
2674
|
+
let unionLeft = prevLeft;
|
|
2675
|
+
let unionTop = prevTop;
|
|
2676
|
+
let unionWidth = prevHeader.width;
|
|
2677
|
+
let unionHeight = prevHeader.height;
|
|
2678
|
+
let pLeft = 0;
|
|
2679
|
+
let pTop = 0;
|
|
2680
|
+
if (hasPixelDelta) {
|
|
2681
|
+
pLeft = pTail.origin?.[0] ?? pHeader.left;
|
|
2682
|
+
pTop = pTail.origin?.[1] ?? pHeader.top;
|
|
2683
|
+
const pRight = pLeft + pHeader.width * grid;
|
|
2684
|
+
const pBottom = pTop + pHeader.height * grid;
|
|
2685
|
+
if ((pLeft - prevLeft) % grid !== 0 || (pTop - prevTop) % grid !== 0) {
|
|
2686
|
+
throw new MapDecodeError(
|
|
2687
|
+
`mergePFrame: P-frame origin not aligned to prev grid (offset=${pLeft - prevLeft},${pTop - prevTop} vs grid=${grid})`
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
unionLeft = Math.min(prevLeft, pLeft);
|
|
2691
|
+
unionTop = Math.min(prevTop, pTop);
|
|
2692
|
+
const unionRight = Math.max(prevRight, pRight);
|
|
2693
|
+
const unionBottom = Math.max(prevBottom, pBottom);
|
|
2694
|
+
unionWidth = (unionRight - unionLeft) / grid;
|
|
2695
|
+
unionHeight = (unionBottom - unionTop) / grid;
|
|
2696
|
+
}
|
|
2697
|
+
const newPixels = Buffer.alloc(unionWidth * unionHeight);
|
|
2698
|
+
const prevPixelEnd = HEADER_SIZE + prevHeader.width * prevHeader.height;
|
|
2699
|
+
if (prevInflated.length < prevPixelEnd) {
|
|
2700
|
+
throw new MapDecodeError(
|
|
2701
|
+
`mergePFrame: prev buffer truncated (need ${prevPixelEnd} bytes for header+pixels, got ${prevInflated.length})`
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
const prevPixels = prevInflated.subarray(HEADER_SIZE, prevPixelEnd);
|
|
2705
|
+
const prevDxPx = (prevLeft - unionLeft) / grid;
|
|
2706
|
+
const prevDyPx = (prevTop - unionTop) / grid;
|
|
2707
|
+
for (let y = 0; y < prevHeader.height; y++) {
|
|
2708
|
+
const srcOff = y * prevHeader.width;
|
|
2709
|
+
const dstOff = (prevDyPx + y) * unionWidth + prevDxPx;
|
|
2710
|
+
prevPixels.copy(newPixels, dstOff, srcOff, srcOff + prevHeader.width);
|
|
2711
|
+
}
|
|
2712
|
+
if (hasPixelDelta) {
|
|
2713
|
+
const pPixelEnd = HEADER_SIZE + pHeader.width * pHeader.height;
|
|
2714
|
+
if (pFrameInflated.length < pPixelEnd) {
|
|
2715
|
+
throw new MapDecodeError(
|
|
2716
|
+
`mergePFrame: P buffer truncated (need ${pPixelEnd} bytes for header+pixels, got ${pFrameInflated.length})`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
const pPixels = pFrameInflated.subarray(HEADER_SIZE, pPixelEnd);
|
|
2720
|
+
const pDxPx = (pLeft - unionLeft) / grid;
|
|
2721
|
+
const pDyPx = (pTop - unionTop) / grid;
|
|
2722
|
+
for (let y = 0; y < pHeader.height; y++) {
|
|
2723
|
+
const dstRow = (pDyPx + y) * unionWidth + pDxPx;
|
|
2724
|
+
const srcRow = y * pHeader.width;
|
|
2725
|
+
for (let x = 0; x < pHeader.width; x++) {
|
|
2726
|
+
newPixels[dstRow + x] = newPixels[dstRow + x] + pPixels[srcRow + x] & 255;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
const newHeader = Buffer.alloc(HEADER_SIZE);
|
|
2731
|
+
newHeader.writeInt16LE(prevHeader.mapId, 0);
|
|
2732
|
+
newHeader.writeInt16LE(pHeader.frameId, 2);
|
|
2733
|
+
newHeader[4] = FRAME_TYPE.I;
|
|
2734
|
+
newHeader.writeInt16LE(pHeader.robotX, 5);
|
|
2735
|
+
newHeader.writeInt16LE(pHeader.robotY, 7);
|
|
2736
|
+
newHeader.writeInt16LE(pHeader.robotA, 9);
|
|
2737
|
+
newHeader.writeInt16LE(pHeader.chargerX, 11);
|
|
2738
|
+
newHeader.writeInt16LE(pHeader.chargerY, 13);
|
|
2739
|
+
newHeader.writeInt16LE(pHeader.chargerA, 15);
|
|
2740
|
+
newHeader.writeInt16LE(grid, 17);
|
|
2741
|
+
newHeader.writeInt16LE(unionWidth, 19);
|
|
2742
|
+
newHeader.writeInt16LE(unionHeight, 21);
|
|
2743
|
+
newHeader.writeInt16LE(unionLeft, 23);
|
|
2744
|
+
newHeader.writeInt16LE(unionTop, 25);
|
|
2745
|
+
const mergedTail = mergeTails(prevTail, pTail, unionLeft, unionTop);
|
|
2746
|
+
const tailBytes = Buffer.from(JSON.stringify(mergedTail), "utf8");
|
|
2747
|
+
return Buffer.concat([newHeader, newPixels, tailBytes]);
|
|
2748
|
+
}
|
|
2749
|
+
function mergePFrameEnvelope(prev, pframe, prevOpts, pframeOpts) {
|
|
2750
|
+
const prevBuf = typeof prev === "string" ? unwrapEnvelope(prev, prevOpts) : prev;
|
|
2751
|
+
const pBuf = typeof pframe === "string" ? unwrapEnvelope(pframe, pframeOpts) : pframe;
|
|
2752
|
+
return mergePFrame(prevBuf, pBuf);
|
|
2753
|
+
}
|
|
2754
|
+
function mergeTails(prev, p, unionLeft, unionTop) {
|
|
2755
|
+
const merged = { ...p };
|
|
2756
|
+
merged.origin = [unionLeft, unionTop];
|
|
2757
|
+
const prevTr = typeof prev.tr === "string" ? prev.tr : "";
|
|
2758
|
+
const pTr = typeof p.tr === "string" ? p.tr : "";
|
|
2759
|
+
if (prevTr || pTr) {
|
|
2760
|
+
merged.tr = prevTr + pTr;
|
|
2761
|
+
} else {
|
|
2762
|
+
delete merged.tr;
|
|
2763
|
+
}
|
|
2764
|
+
if (!("seg_inf" in p) && "seg_inf" in prev) {
|
|
2765
|
+
merged.seg_inf = prev.seg_inf;
|
|
2766
|
+
}
|
|
2767
|
+
if (!("sa" in p) && "sa" in prev) {
|
|
2768
|
+
merged.sa = prev.sa;
|
|
2769
|
+
}
|
|
2770
|
+
for (const key2 of PERSISTENT_TAIL_KEYS) {
|
|
2771
|
+
if (!(key2 in p) && key2 in prev) {
|
|
2772
|
+
merged[key2] = prev[key2];
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
return merged;
|
|
2776
|
+
}
|
|
2777
|
+
var PERSISTENT_TAIL_KEYS = [
|
|
2778
|
+
"vw",
|
|
2779
|
+
"vws",
|
|
2780
|
+
"sneak_areas",
|
|
2781
|
+
"sneak_areas_end",
|
|
2782
|
+
"walls_info",
|
|
2783
|
+
"rism",
|
|
2784
|
+
"decmap"
|
|
2785
|
+
];
|
|
2786
|
+
|
|
2636
2787
|
// src/models/vacuum/map/decode.ts
|
|
2637
2788
|
function decodeVacuumMap(input, opts = {}) {
|
|
2638
2789
|
const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
|
|
@@ -2677,6 +2828,10 @@ function decodeVacuumMap(input, opts = {}) {
|
|
|
2677
2828
|
cleanedArea
|
|
2678
2829
|
};
|
|
2679
2830
|
}
|
|
2831
|
+
function applyVacuumPFrame(prev, pframe, opts = {}) {
|
|
2832
|
+
const merged = typeof prev === "string" || typeof pframe === "string" ? mergePFrameEnvelope(prev, pframe, opts.prev, opts.pframe) : mergePFrame(prev, pframe);
|
|
2833
|
+
return { buffer: merged, data: decodeVacuumMap(merged) };
|
|
2834
|
+
}
|
|
2680
2835
|
function mergeDimensions(header, tail) {
|
|
2681
2836
|
const left = tail.origin?.[0] ?? header.left;
|
|
2682
2837
|
const top = tail.origin?.[1] ?? header.top;
|
|
@@ -2697,10 +2852,79 @@ function pose(x, y, angle, absent) {
|
|
|
2697
2852
|
|
|
2698
2853
|
// src/models/vacuum/map/render.ts
|
|
2699
2854
|
var import_pngjs = require("pngjs");
|
|
2700
|
-
var
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2855
|
+
var SCHEMES = {
|
|
2856
|
+
"dreame-light": {
|
|
2857
|
+
floor: [210, 222, 235, 255],
|
|
2858
|
+
wall: [60, 60, 60, 255],
|
|
2859
|
+
carpet: [180, 150, 120, 200],
|
|
2860
|
+
segSat: 0.45,
|
|
2861
|
+
segVal: 0.95,
|
|
2862
|
+
path: [60, 110, 180, 235],
|
|
2863
|
+
robotBody: [40, 70, 110, 255],
|
|
2864
|
+
robotHeading: [255, 255, 255, 255],
|
|
2865
|
+
charger: [40, 160, 80, 255],
|
|
2866
|
+
noGoFill: [220, 60, 60, 70],
|
|
2867
|
+
noGoBorder: [200, 40, 40, 220],
|
|
2868
|
+
noMopFill: [60, 120, 220, 60],
|
|
2869
|
+
noMopBorder: [40, 90, 200, 200],
|
|
2870
|
+
virtualWall: [200, 40, 40, 230],
|
|
2871
|
+
obstacle: [230, 150, 30, 255],
|
|
2872
|
+
label: [40, 40, 40, 255]
|
|
2873
|
+
},
|
|
2874
|
+
"dreame-dark": {
|
|
2875
|
+
floor: [40, 46, 56, 255],
|
|
2876
|
+
wall: [15, 15, 18, 255],
|
|
2877
|
+
carpet: [80, 66, 52, 200],
|
|
2878
|
+
segSat: 0.5,
|
|
2879
|
+
segVal: 0.62,
|
|
2880
|
+
path: [120, 170, 230, 235],
|
|
2881
|
+
robotBody: [120, 170, 230, 255],
|
|
2882
|
+
robotHeading: [20, 24, 30, 255],
|
|
2883
|
+
charger: [60, 200, 110, 255],
|
|
2884
|
+
noGoFill: [220, 70, 70, 80],
|
|
2885
|
+
noGoBorder: [230, 70, 70, 220],
|
|
2886
|
+
noMopFill: [70, 130, 230, 70],
|
|
2887
|
+
noMopBorder: [80, 140, 235, 210],
|
|
2888
|
+
virtualWall: [230, 70, 70, 235],
|
|
2889
|
+
obstacle: [240, 180, 60, 255],
|
|
2890
|
+
label: [225, 230, 240, 255]
|
|
2891
|
+
},
|
|
2892
|
+
"mijia-light": {
|
|
2893
|
+
floor: [225, 232, 240, 255],
|
|
2894
|
+
wall: [70, 78, 92, 255],
|
|
2895
|
+
carpet: [188, 162, 132, 200],
|
|
2896
|
+
segSat: 0.35,
|
|
2897
|
+
segVal: 0.98,
|
|
2898
|
+
path: [80, 130, 200, 230],
|
|
2899
|
+
robotBody: [30, 100, 200, 255],
|
|
2900
|
+
robotHeading: [255, 255, 255, 255],
|
|
2901
|
+
charger: [40, 170, 90, 255],
|
|
2902
|
+
noGoFill: [225, 70, 70, 70],
|
|
2903
|
+
noGoBorder: [205, 45, 45, 220],
|
|
2904
|
+
noMopFill: [70, 130, 225, 60],
|
|
2905
|
+
noMopBorder: [50, 100, 205, 200],
|
|
2906
|
+
virtualWall: [205, 45, 45, 230],
|
|
2907
|
+
obstacle: [235, 160, 35, 255],
|
|
2908
|
+
label: [45, 52, 64, 255]
|
|
2909
|
+
},
|
|
2910
|
+
tasshack: {
|
|
2911
|
+
floor: [200, 210, 225, 255],
|
|
2912
|
+
wall: [80, 80, 95, 255],
|
|
2913
|
+
carpet: [176, 148, 118, 205],
|
|
2914
|
+
segSat: 0.5,
|
|
2915
|
+
segVal: 0.9,
|
|
2916
|
+
path: [50, 100, 170, 235],
|
|
2917
|
+
robotBody: [35, 60, 100, 255],
|
|
2918
|
+
robotHeading: [255, 255, 255, 255],
|
|
2919
|
+
charger: [35, 150, 75, 255],
|
|
2920
|
+
noGoFill: [215, 55, 55, 75],
|
|
2921
|
+
noGoBorder: [195, 35, 35, 220],
|
|
2922
|
+
noMopFill: [55, 115, 215, 65],
|
|
2923
|
+
noMopBorder: [35, 85, 195, 205],
|
|
2924
|
+
virtualWall: [195, 35, 35, 230],
|
|
2925
|
+
obstacle: [225, 145, 25, 255],
|
|
2926
|
+
label: [35, 42, 54, 255]
|
|
2927
|
+
}
|
|
2704
2928
|
};
|
|
2705
2929
|
function hsvToRgba(h, s, v) {
|
|
2706
2930
|
const c = v * s;
|
|
@@ -2731,49 +2955,230 @@ function hsvToRgba(h, s, v) {
|
|
|
2731
2955
|
const m = v - c;
|
|
2732
2956
|
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), 255];
|
|
2733
2957
|
}
|
|
2734
|
-
function segmentColor(id) {
|
|
2958
|
+
function segmentColor(id, pal) {
|
|
2735
2959
|
const hue = id * 137.508 % 360;
|
|
2736
|
-
return hsvToRgba(hue,
|
|
2960
|
+
return hsvToRgba(hue, pal.segSat, pal.segVal);
|
|
2737
2961
|
}
|
|
2738
2962
|
function renderVacuumPng(map, opts = {}) {
|
|
2739
2963
|
const scale = Math.max(1, Math.trunc(opts.scale ?? 1));
|
|
2740
|
-
const
|
|
2741
|
-
const
|
|
2742
|
-
const
|
|
2964
|
+
const pal = SCHEMES[opts.colorScheme ?? "dreame-light"];
|
|
2965
|
+
const w = Math.max(1, map.dimensions.width * scale);
|
|
2966
|
+
const h = Math.max(1, map.dimensions.height * scale);
|
|
2967
|
+
const png = new import_pngjs.PNG({ width: w, height: h });
|
|
2743
2968
|
png.data.fill(0);
|
|
2744
2969
|
for (const layer of map.layers) {
|
|
2745
|
-
paintLayer(png, layer, scale);
|
|
2970
|
+
paintLayer(png, layer, scale, pal);
|
|
2971
|
+
}
|
|
2972
|
+
const dim = map.dimensions;
|
|
2973
|
+
if (opts.showNoGo !== false) {
|
|
2974
|
+
for (const area of map.restrictedAreas) {
|
|
2975
|
+
const fill = area.kind === "noMop" ? pal.noMopFill : pal.noGoFill;
|
|
2976
|
+
const border = area.kind === "noMop" ? pal.noMopBorder : pal.noGoBorder;
|
|
2977
|
+
const a = worldToPx(area.bbox.xMin, area.bbox.yMin, dim, scale);
|
|
2978
|
+
const b = worldToPx(area.bbox.xMax, area.bbox.yMax, dim, scale);
|
|
2979
|
+
fillRect(png, a.x, a.y, b.x, b.y, fill);
|
|
2980
|
+
strokeRect(png, a.x, a.y, b.x, b.y, border);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
if (opts.showVirtualWalls !== false) {
|
|
2984
|
+
for (const vw of map.virtualWalls) {
|
|
2985
|
+
const a = worldToPx(vw.from.x, vw.from.y, dim, scale);
|
|
2986
|
+
const b = worldToPx(vw.to.x, vw.to.y, dim, scale);
|
|
2987
|
+
drawLine(png, a.x, a.y, b.x, b.y, pal.virtualWall, Math.max(1, scale));
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
if (opts.showPath !== false) {
|
|
2991
|
+
for (const path of map.paths) {
|
|
2992
|
+
drawPolyline(png, path.points, dim, scale, pal.path, Math.max(1, scale));
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
if (opts.showObstacles !== false) {
|
|
2996
|
+
for (const ob of map.obstacles) {
|
|
2997
|
+
const p = worldToPx(ob.x, ob.y, dim, scale);
|
|
2998
|
+
drawDisk(png, p.x, p.y, Math.max(2, scale * 2), pal.obstacle);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
if (opts.showCharger !== false && map.dock !== null) {
|
|
3002
|
+
drawCharger(png, map.dock, dim, scale, pal);
|
|
3003
|
+
}
|
|
3004
|
+
if (opts.showRobot !== false && map.robot !== null) {
|
|
3005
|
+
drawRobot(png, map.robot, dim, scale, pal);
|
|
3006
|
+
}
|
|
3007
|
+
if (opts.showSegmentLabels === true) {
|
|
3008
|
+
for (const seg of map.segments) {
|
|
3009
|
+
const p = worldToPx(seg.centroid.x, seg.centroid.y, dim, scale);
|
|
3010
|
+
drawLabel(png, p.x, p.y, String(seg.id), pal.label, Math.max(1, scale));
|
|
3011
|
+
}
|
|
2746
3012
|
}
|
|
2747
3013
|
return import_pngjs.PNG.sync.write(png);
|
|
2748
3014
|
}
|
|
2749
|
-
function
|
|
2750
|
-
|
|
3015
|
+
function worldToPx(worldX, worldY, dim, scale) {
|
|
3016
|
+
return {
|
|
3017
|
+
x: Math.round((worldX - dim.left) / dim.gridSize * scale),
|
|
3018
|
+
y: Math.round((worldY - dim.top) / dim.gridSize * scale)
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
function paintLayer(png, layer, scale, pal) {
|
|
3022
|
+
const color = layer.type === "segment" ? segmentColor(layer.segmentId ?? 0, pal) : layer.type === "wall" ? pal.wall : layer.type === "carpet" ? pal.carpet : pal.floor;
|
|
2751
3023
|
for (const run of layer.runs) {
|
|
2752
|
-
paintRun(png, run, color, scale
|
|
3024
|
+
paintRun(png, run, color, scale);
|
|
2753
3025
|
}
|
|
2754
3026
|
}
|
|
2755
|
-
function paintRun(png, run, color, scale
|
|
3027
|
+
function paintRun(png, run, color, scale) {
|
|
2756
3028
|
const [xPx, yPx, len] = run;
|
|
2757
3029
|
for (let i = 0; i < len; i += 1) {
|
|
2758
3030
|
const baseX = (xPx + i) * scale;
|
|
2759
3031
|
const baseY = yPx * scale;
|
|
2760
3032
|
for (let dy = 0; dy < scale; dy += 1) {
|
|
2761
|
-
const py = baseY + dy;
|
|
2762
|
-
if (py < 0 || py >= height) {
|
|
2763
|
-
continue;
|
|
2764
|
-
}
|
|
2765
3033
|
for (let dx = 0; dx < scale; dx += 1) {
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
3034
|
+
setPixel(png, baseX + dx, baseY + dy, color);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
function setPixel(png, x, y, color) {
|
|
3040
|
+
if (x < 0 || y < 0 || x >= png.width || y >= png.height) return;
|
|
3041
|
+
const off = (y * png.width + x) * 4;
|
|
3042
|
+
const a = color[3];
|
|
3043
|
+
if (a >= 255) {
|
|
3044
|
+
png.data[off] = color[0];
|
|
3045
|
+
png.data[off + 1] = color[1];
|
|
3046
|
+
png.data[off + 2] = color[2];
|
|
3047
|
+
png.data[off + 3] = 255;
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
const sa = a / 255;
|
|
3051
|
+
const da = (png.data[off + 3] ?? 0) / 255;
|
|
3052
|
+
const outA = sa + da * (1 - sa);
|
|
3053
|
+
if (outA <= 0) return;
|
|
3054
|
+
for (let k = 0; k < 3; k += 1) {
|
|
3055
|
+
const src = color[k] ?? 0;
|
|
3056
|
+
const dst = png.data[off + k] ?? 0;
|
|
3057
|
+
png.data[off + k] = Math.round((src * sa + dst * da * (1 - sa)) / outA);
|
|
3058
|
+
}
|
|
3059
|
+
png.data[off + 3] = Math.round(outA * 255);
|
|
3060
|
+
}
|
|
3061
|
+
function fillRect(png, x0, y0, x1, y1, color) {
|
|
3062
|
+
const xa = Math.min(x0, x1);
|
|
3063
|
+
const xb = Math.max(x0, x1);
|
|
3064
|
+
const ya = Math.min(y0, y1);
|
|
3065
|
+
const yb = Math.max(y0, y1);
|
|
3066
|
+
for (let y = ya; y <= yb; y += 1) {
|
|
3067
|
+
for (let x = xa; x <= xb; x += 1) {
|
|
3068
|
+
setPixel(png, x, y, color);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
function strokeRect(png, x0, y0, x1, y1, color) {
|
|
3073
|
+
const xa = Math.min(x0, x1);
|
|
3074
|
+
const xb = Math.max(x0, x1);
|
|
3075
|
+
const ya = Math.min(y0, y1);
|
|
3076
|
+
const yb = Math.max(y0, y1);
|
|
3077
|
+
for (let x = xa; x <= xb; x += 1) {
|
|
3078
|
+
setPixel(png, x, ya, color);
|
|
3079
|
+
setPixel(png, x, yb, color);
|
|
3080
|
+
}
|
|
3081
|
+
for (let y = ya; y <= yb; y += 1) {
|
|
3082
|
+
setPixel(png, xa, y, color);
|
|
3083
|
+
setPixel(png, xb, y, color);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
function drawLine(png, x0, y0, x1, y1, color, thickness) {
|
|
3087
|
+
let x = x0;
|
|
3088
|
+
let y = y0;
|
|
3089
|
+
const dx = Math.abs(x1 - x0);
|
|
3090
|
+
const dy = -Math.abs(y1 - y0);
|
|
3091
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
3092
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
3093
|
+
let err = dx + dy;
|
|
3094
|
+
const r = Math.max(0, Math.trunc((thickness - 1) / 2));
|
|
3095
|
+
for (; ; ) {
|
|
3096
|
+
for (let oy = -r; oy <= r; oy += 1) {
|
|
3097
|
+
for (let ox = -r; ox <= r; ox += 1) {
|
|
3098
|
+
setPixel(png, x + ox, y + oy, color);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
if (x === x1 && y === y1) break;
|
|
3102
|
+
const e2 = 2 * err;
|
|
3103
|
+
if (e2 >= dy) {
|
|
3104
|
+
err += dy;
|
|
3105
|
+
x += sx;
|
|
3106
|
+
}
|
|
3107
|
+
if (e2 <= dx) {
|
|
3108
|
+
err += dx;
|
|
3109
|
+
y += sy;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function drawPolyline(png, points, dim, scale, color, thickness) {
|
|
3114
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
3115
|
+
const p0 = points[i - 1];
|
|
3116
|
+
const p1 = points[i];
|
|
3117
|
+
if (p0 === void 0 || p1 === void 0) continue;
|
|
3118
|
+
const a = worldToPx(p0.x, p0.y, dim, scale);
|
|
3119
|
+
const b = worldToPx(p1.x, p1.y, dim, scale);
|
|
3120
|
+
drawLine(png, a.x, a.y, b.x, b.y, color, thickness);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
function drawDisk(png, cx, cy, r, color) {
|
|
3124
|
+
const rr = r * r;
|
|
3125
|
+
for (let dy = -r; dy <= r; dy += 1) {
|
|
3126
|
+
for (let dx = -r; dx <= r; dx += 1) {
|
|
3127
|
+
if (dx * dx + dy * dy <= rr) setPixel(png, cx + dx, cy + dy, color);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
function drawRobot(png, robot, dim, scale, pal) {
|
|
3132
|
+
const p = worldToPx(robot.x, robot.y, dim, scale);
|
|
3133
|
+
const r = Math.max(3, scale * 3);
|
|
3134
|
+
drawDisk(png, p.x, p.y, r, pal.robotBody);
|
|
3135
|
+
const rad = robot.angle * Math.PI / 180;
|
|
3136
|
+
const hx = Math.round(p.x + Math.cos(rad) * r);
|
|
3137
|
+
const hy = Math.round(p.y + Math.sin(rad) * r);
|
|
3138
|
+
drawLine(png, p.x, p.y, hx, hy, pal.robotHeading, Math.max(1, Math.trunc(scale / 2) + 1));
|
|
3139
|
+
}
|
|
3140
|
+
function drawCharger(png, dock, dim, scale, pal) {
|
|
3141
|
+
const p = worldToPx(dock.x, dock.y, dim, scale);
|
|
3142
|
+
const r = Math.max(2, scale * 2);
|
|
3143
|
+
fillRect(png, p.x - r, p.y - r, p.x + r, p.y + r, pal.charger);
|
|
3144
|
+
}
|
|
3145
|
+
var GLYPHS = {
|
|
3146
|
+
"0": [7, 5, 5, 5, 7],
|
|
3147
|
+
"1": [2, 6, 2, 2, 7],
|
|
3148
|
+
"2": [7, 1, 7, 4, 7],
|
|
3149
|
+
"3": [7, 1, 7, 1, 7],
|
|
3150
|
+
"4": [5, 5, 7, 1, 1],
|
|
3151
|
+
"5": [7, 4, 7, 1, 7],
|
|
3152
|
+
"6": [7, 4, 7, 5, 7],
|
|
3153
|
+
"7": [7, 1, 2, 2, 2],
|
|
3154
|
+
"8": [7, 5, 7, 5, 7],
|
|
3155
|
+
"9": [7, 5, 7, 1, 7],
|
|
3156
|
+
"-": [0, 0, 7, 0, 0]
|
|
3157
|
+
};
|
|
3158
|
+
function drawLabel(png, cx, cy, text, color, px) {
|
|
3159
|
+
const glyphW = 3 * px;
|
|
3160
|
+
const glyphH = 5 * px;
|
|
3161
|
+
const gap = px;
|
|
3162
|
+
const totalW = text.length * glyphW + Math.max(0, text.length - 1) * gap;
|
|
3163
|
+
let originX = Math.round(cx - totalW / 2);
|
|
3164
|
+
const originY = Math.round(cy - glyphH / 2);
|
|
3165
|
+
for (const ch of text) {
|
|
3166
|
+
const glyph = GLYPHS[ch];
|
|
3167
|
+
if (glyph !== void 0) {
|
|
3168
|
+
for (let row = 0; row < 5; row += 1) {
|
|
3169
|
+
const bits = glyph[row];
|
|
3170
|
+
if (bits === void 0) continue;
|
|
3171
|
+
for (let col = 0; col < 3; col += 1) {
|
|
3172
|
+
if ((bits & 1 << 2 - col) === 0) continue;
|
|
3173
|
+
for (let sy = 0; sy < px; sy += 1) {
|
|
3174
|
+
for (let sx = 0; sx < px; sx += 1) {
|
|
3175
|
+
setPixel(png, originX + col * px + sx, originY + row * px + sy, color);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
2769
3178
|
}
|
|
2770
|
-
const off = (py * width + px) * 4;
|
|
2771
|
-
png.data[off] = color[0];
|
|
2772
|
-
png.data[off + 1] = color[1];
|
|
2773
|
-
png.data[off + 2] = color[2];
|
|
2774
|
-
png.data[off + 3] = color[3];
|
|
2775
3179
|
}
|
|
2776
3180
|
}
|
|
3181
|
+
originX += glyphW + gap;
|
|
2777
3182
|
}
|
|
2778
3183
|
}
|
|
2779
3184
|
|
|
@@ -3364,6 +3769,14 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
3364
3769
|
*/
|
|
3365
3770
|
async getMap(input) {
|
|
3366
3771
|
this.#requireCap(this.#caps.canMap, "getMap", "map decoding");
|
|
3772
|
+
const blob = await this.#fetchMapBlob(input);
|
|
3773
|
+
const map = decodeVacuumMap(blob, this.#decodeOpts(input));
|
|
3774
|
+
this.#lastMap = map;
|
|
3775
|
+
this.emit("map", map);
|
|
3776
|
+
return map;
|
|
3777
|
+
}
|
|
3778
|
+
/** Fetch the raw OSS blob (still the base64+zlib envelope) for a map frame. */
|
|
3779
|
+
async #fetchMapBlob(input) {
|
|
3367
3780
|
const session = this.currentSession();
|
|
3368
3781
|
const region = this.region;
|
|
3369
3782
|
const fetcher = input.fetcher ?? new OssFetcher();
|
|
@@ -3377,14 +3790,77 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
3377
3790
|
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
|
|
3378
3791
|
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3379
3792
|
};
|
|
3380
|
-
|
|
3381
|
-
|
|
3793
|
+
return fetcher.fetchBlob(fetchInput);
|
|
3794
|
+
}
|
|
3795
|
+
/** The AES key/iv decode options, threaded from a {@link VacuumGetMapInput}. */
|
|
3796
|
+
#decodeOpts(input) {
|
|
3797
|
+
return {
|
|
3382
3798
|
...input.key !== void 0 ? { key: input.key } : {},
|
|
3383
3799
|
...input.iv !== void 0 ? { iv: input.iv } : {}
|
|
3384
|
-
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3800
|
+
};
|
|
3801
|
+
}
|
|
3802
|
+
/**
|
|
3803
|
+
* The merge base for live P-frame streaming — always an INFLATED frame buffer.
|
|
3804
|
+
* An I-frame (re)seeds it; each P-frame merge replaces it with the merged
|
|
3805
|
+
* inflated buffer (so further P-frames stack). `null` until the first I-frame
|
|
3806
|
+
* (or after an out-of-order / map-id reset).
|
|
3807
|
+
*/
|
|
3808
|
+
#mapStreamBase = null;
|
|
3809
|
+
/** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
|
|
3810
|
+
resetMapStream() {
|
|
3811
|
+
this.#mapStreamBase = null;
|
|
3812
|
+
}
|
|
3813
|
+
/** Inflate an OSS blob to a raw frame buffer (live envelope → zlib; verbatim if already inflated). */
|
|
3814
|
+
#inflateFrame(blob, opts) {
|
|
3815
|
+
return looksLikeBase64Zlib(blob) ? unwrapEnvelope(blob.toString("latin1"), opts) : blob;
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Fetch the latest advertised map frame and fold it into a continuously
|
|
3819
|
+
* updating map. An I-frame (re)seeds the merge base and renders standalone; a
|
|
3820
|
+
* P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
|
|
3821
|
+
* grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
|
|
3822
|
+
* out-of-order P-frame or a map-id change the base is dropped and `null`
|
|
3823
|
+
* returned — the next I-frame re-seeds. Returns `null` when no frame is
|
|
3824
|
+
* advertised yet, or a P-frame arrives before any I-frame.
|
|
3825
|
+
*
|
|
3826
|
+
* Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
|
|
3827
|
+
* map-watching consumer (e.g. the camstack map child) gets a fresh complete
|
|
3828
|
+
* frame on every push during a live clean.
|
|
3829
|
+
*/
|
|
3830
|
+
async fetchLatestMapStreaming(opts = {}) {
|
|
3831
|
+
this.#requireCap(this.#caps.canMap, "fetchLatestMapStreaming", "map decoding");
|
|
3832
|
+
const filename = this.mapFilename;
|
|
3833
|
+
if (filename === null) return null;
|
|
3834
|
+
const input = { filename, ...opts };
|
|
3835
|
+
const decodeOpts = this.#decodeOpts(input);
|
|
3836
|
+
const blob = await this.#fetchMapBlob(input);
|
|
3837
|
+
const inflated = this.#inflateFrame(blob, decodeOpts);
|
|
3838
|
+
const frame = decodeVacuumMap(inflated, decodeOpts);
|
|
3839
|
+
if (frame.frameType === "I") {
|
|
3840
|
+
this.#mapStreamBase = inflated;
|
|
3841
|
+
this.#lastMap = frame;
|
|
3842
|
+
this.emit("map", frame);
|
|
3843
|
+
return frame;
|
|
3844
|
+
}
|
|
3845
|
+
if (frame.frameType === "P") {
|
|
3846
|
+
if (this.#mapStreamBase === null) return null;
|
|
3847
|
+
try {
|
|
3848
|
+
const { buffer, data } = applyVacuumPFrame(this.#mapStreamBase, inflated);
|
|
3849
|
+
this.#mapStreamBase = buffer;
|
|
3850
|
+
this.#lastMap = data;
|
|
3851
|
+
this.emit("map", data);
|
|
3852
|
+
return data;
|
|
3853
|
+
} catch (err) {
|
|
3854
|
+
if (err instanceof OutOfOrderFrameError || err instanceof MapDecodeError) {
|
|
3855
|
+
this.#mapStreamBase = null;
|
|
3856
|
+
return null;
|
|
3857
|
+
}
|
|
3858
|
+
throw err;
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
this.#lastMap = frame;
|
|
3862
|
+
this.emit("map", frame);
|
|
3863
|
+
return frame;
|
|
3388
3864
|
}
|
|
3389
3865
|
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
3390
3866
|
static DEFAULT_PROPS = [
|