@apocaliss92/nodedreame 1.8.0 → 1.9.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 +311 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -5
- package/dist/index.d.ts +30 -5
- package/dist/index.js +311 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1220,17 +1220,42 @@ interface VacuumMap {
|
|
|
1220
1220
|
* Raster PNG renderer for a decoded `VacuumMap`.
|
|
1221
1221
|
*
|
|
1222
1222
|
* node-dreame ships only the structured model (no image); this is NEW. We paint
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1223
|
+
* the run-length `layers` (floor / segments / carpet / walls) one pixel per grid
|
|
1224
|
+
* cell (× an optional integer upscale `scale`), then composite the world-frame
|
|
1225
|
+
* overlays on top: restricted areas (no-go / no-mop), virtual walls, the cleaning
|
|
1226
|
+
* path polylines, AI obstacles, the charger, and the robot (body + heading). A
|
|
1227
|
+
* `colorScheme` picks the palette and every overlay is independently toggleable,
|
|
1228
|
+
* mirroring how the Home Assistant integration exposes map customization.
|
|
1229
|
+
*
|
|
1230
|
+
* Coordinate transform: every overlay coordinate is mm in the world frame.
|
|
1231
|
+
* `dimensions.left` / `.top` are the world-mm of pixel (0,0) and `.gridSize` is
|
|
1232
|
+
* mm-per-pixel, so `px = (worldX - left) / gridSize` (then × `scale`). Row 0 is
|
|
1233
|
+
* the top edge — no Y flip.
|
|
1227
1234
|
*
|
|
1228
1235
|
* Pure: only `pngjs` + arithmetic. No casts.
|
|
1229
1236
|
*/
|
|
1230
1237
|
|
|
1238
|
+
/** A named map palette. Mirrors the HA integration's `color_scheme` choices. */
|
|
1239
|
+
type MapColorScheme = 'dreame-light' | 'dreame-dark' | 'mijia-light' | 'tasshack';
|
|
1231
1240
|
interface RenderVacuumPngOptions {
|
|
1232
1241
|
/** Integer upscale factor (nearest-neighbour). Default 1. */
|
|
1233
1242
|
scale?: number;
|
|
1243
|
+
/** Palette. Default `'dreame-light'`. */
|
|
1244
|
+
colorScheme?: MapColorScheme;
|
|
1245
|
+
/** Draw the cleaning/mopping path polylines. Default true. */
|
|
1246
|
+
showPath?: boolean;
|
|
1247
|
+
/** Draw the robot marker (body + heading). Default true. */
|
|
1248
|
+
showRobot?: boolean;
|
|
1249
|
+
/** Draw the charger / dock marker. Default true. */
|
|
1250
|
+
showCharger?: boolean;
|
|
1251
|
+
/** Draw no-go / no-mop restricted areas. Default true. */
|
|
1252
|
+
showNoGo?: boolean;
|
|
1253
|
+
/** Draw user-defined virtual walls. Default true. */
|
|
1254
|
+
showVirtualWalls?: boolean;
|
|
1255
|
+
/** Draw AI-detected obstacle markers. Default true. */
|
|
1256
|
+
showObstacles?: boolean;
|
|
1257
|
+
/** Draw the segment id as a tiny label at each room centroid. Default false. */
|
|
1258
|
+
showSegmentLabels?: boolean;
|
|
1234
1259
|
}
|
|
1235
1260
|
declare function renderVacuumPng(map: VacuumMap, opts?: RenderVacuumPngOptions): Buffer;
|
|
1236
1261
|
|
|
@@ -2407,4 +2432,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
|
|
|
2407
2432
|
*/
|
|
2408
2433
|
declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
|
|
2409
2434
|
|
|
2410
|
-
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
|
2435
|
+
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
package/dist/index.d.ts
CHANGED
|
@@ -1220,17 +1220,42 @@ interface VacuumMap {
|
|
|
1220
1220
|
* Raster PNG renderer for a decoded `VacuumMap`.
|
|
1221
1221
|
*
|
|
1222
1222
|
* node-dreame ships only the structured model (no image); this is NEW. We paint
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1223
|
+
* the run-length `layers` (floor / segments / carpet / walls) one pixel per grid
|
|
1224
|
+
* cell (× an optional integer upscale `scale`), then composite the world-frame
|
|
1225
|
+
* overlays on top: restricted areas (no-go / no-mop), virtual walls, the cleaning
|
|
1226
|
+
* path polylines, AI obstacles, the charger, and the robot (body + heading). A
|
|
1227
|
+
* `colorScheme` picks the palette and every overlay is independently toggleable,
|
|
1228
|
+
* mirroring how the Home Assistant integration exposes map customization.
|
|
1229
|
+
*
|
|
1230
|
+
* Coordinate transform: every overlay coordinate is mm in the world frame.
|
|
1231
|
+
* `dimensions.left` / `.top` are the world-mm of pixel (0,0) and `.gridSize` is
|
|
1232
|
+
* mm-per-pixel, so `px = (worldX - left) / gridSize` (then × `scale`). Row 0 is
|
|
1233
|
+
* the top edge — no Y flip.
|
|
1227
1234
|
*
|
|
1228
1235
|
* Pure: only `pngjs` + arithmetic. No casts.
|
|
1229
1236
|
*/
|
|
1230
1237
|
|
|
1238
|
+
/** A named map palette. Mirrors the HA integration's `color_scheme` choices. */
|
|
1239
|
+
type MapColorScheme = 'dreame-light' | 'dreame-dark' | 'mijia-light' | 'tasshack';
|
|
1231
1240
|
interface RenderVacuumPngOptions {
|
|
1232
1241
|
/** Integer upscale factor (nearest-neighbour). Default 1. */
|
|
1233
1242
|
scale?: number;
|
|
1243
|
+
/** Palette. Default `'dreame-light'`. */
|
|
1244
|
+
colorScheme?: MapColorScheme;
|
|
1245
|
+
/** Draw the cleaning/mopping path polylines. Default true. */
|
|
1246
|
+
showPath?: boolean;
|
|
1247
|
+
/** Draw the robot marker (body + heading). Default true. */
|
|
1248
|
+
showRobot?: boolean;
|
|
1249
|
+
/** Draw the charger / dock marker. Default true. */
|
|
1250
|
+
showCharger?: boolean;
|
|
1251
|
+
/** Draw no-go / no-mop restricted areas. Default true. */
|
|
1252
|
+
showNoGo?: boolean;
|
|
1253
|
+
/** Draw user-defined virtual walls. Default true. */
|
|
1254
|
+
showVirtualWalls?: boolean;
|
|
1255
|
+
/** Draw AI-detected obstacle markers. Default true. */
|
|
1256
|
+
showObstacles?: boolean;
|
|
1257
|
+
/** Draw the segment id as a tiny label at each room centroid. Default false. */
|
|
1258
|
+
showSegmentLabels?: boolean;
|
|
1234
1259
|
}
|
|
1235
1260
|
declare function renderVacuumPng(map: VacuumMap, opts?: RenderVacuumPngOptions): Buffer;
|
|
1236
1261
|
|
|
@@ -2407,4 +2432,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
|
|
|
2407
2432
|
*/
|
|
2408
2433
|
declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
|
|
2409
2434
|
|
|
2410
|
-
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
|
2435
|
+
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
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.
|
|
3
|
+
var LIBRARY_VERSION = "1.9.0";
|
|
4
4
|
|
|
5
5
|
// src/transport/errors.ts
|
|
6
6
|
var DreameError = class extends Error {
|
|
@@ -1882,11 +1882,43 @@ function unwrapEnvelope(value, opts = {}) {
|
|
|
1882
1882
|
}
|
|
1883
1883
|
bytes = aesCbcDecrypt(bytes, key2, opts.iv);
|
|
1884
1884
|
}
|
|
1885
|
+
let inflated;
|
|
1885
1886
|
try {
|
|
1886
|
-
|
|
1887
|
+
inflated = zlib.inflateSync(bytes);
|
|
1887
1888
|
} catch (err) {
|
|
1888
1889
|
throw new MapDecodeError("envelope: zlib inflate failed", { cause: err });
|
|
1889
1890
|
}
|
|
1891
|
+
let guard = 0;
|
|
1892
|
+
while (guard < MAX_EXTRA_WRAP_LAYERS && looksLikeBase64Zlib(inflated)) {
|
|
1893
|
+
const text = inflated.toString("latin1").replace(/-/g, "+").replace(/_/g, "/");
|
|
1894
|
+
try {
|
|
1895
|
+
inflated = zlib.inflateSync(Buffer.from(text, "base64"));
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
throw new MapDecodeError("envelope: nested zlib inflate failed", { cause: err });
|
|
1898
|
+
}
|
|
1899
|
+
guard += 1;
|
|
1900
|
+
}
|
|
1901
|
+
return inflated;
|
|
1902
|
+
}
|
|
1903
|
+
var MAX_EXTRA_WRAP_LAYERS = 4;
|
|
1904
|
+
function looksLikeBase64Zlib(buf) {
|
|
1905
|
+
if (buf.length < 8) return false;
|
|
1906
|
+
for (const b of buf) {
|
|
1907
|
+
const isB64 = b >= 65 && b <= 90 || // A-Z
|
|
1908
|
+
b >= 97 && b <= 122 || // a-z
|
|
1909
|
+
b >= 48 && b <= 57 || // 0-9
|
|
1910
|
+
b === 43 || b === 47 || // + /
|
|
1911
|
+
b === 45 || b === 95 || // - _ (url-safe)
|
|
1912
|
+
b === 61;
|
|
1913
|
+
if (!isB64) return false;
|
|
1914
|
+
}
|
|
1915
|
+
try {
|
|
1916
|
+
const prefix = buf.subarray(0, 16).toString("latin1").replace(/-/g, "+").replace(/_/g, "/");
|
|
1917
|
+
const decoded = Buffer.from(prefix, "base64");
|
|
1918
|
+
return decoded.length >= 2 && decoded[0] === 120;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1890
1922
|
}
|
|
1891
1923
|
function aesCbcDecrypt(cipher, rawKey, iv) {
|
|
1892
1924
|
const keyBytes = Buffer.from(
|
|
@@ -2522,7 +2554,7 @@ function decodeCleanedAreaPixels(pixels, width, height) {
|
|
|
2522
2554
|
|
|
2523
2555
|
// src/models/vacuum/map/decode.ts
|
|
2524
2556
|
function decodeVacuumMap(input, opts = {}) {
|
|
2525
|
-
const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : input;
|
|
2557
|
+
const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
|
|
2526
2558
|
const { header, tail } = parseFrame(inflated);
|
|
2527
2559
|
const dimensions = mergeDimensions(header, tail);
|
|
2528
2560
|
const robot = pose(header.robotX, header.robotY, header.robotA, tail.nr === true);
|
|
@@ -2584,10 +2616,79 @@ function pose(x, y, angle, absent) {
|
|
|
2584
2616
|
|
|
2585
2617
|
// src/models/vacuum/map/render.ts
|
|
2586
2618
|
import { PNG } from "pngjs";
|
|
2587
|
-
var
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2619
|
+
var SCHEMES = {
|
|
2620
|
+
"dreame-light": {
|
|
2621
|
+
floor: [210, 222, 235, 255],
|
|
2622
|
+
wall: [60, 60, 60, 255],
|
|
2623
|
+
carpet: [180, 150, 120, 200],
|
|
2624
|
+
segSat: 0.45,
|
|
2625
|
+
segVal: 0.95,
|
|
2626
|
+
path: [60, 110, 180, 235],
|
|
2627
|
+
robotBody: [40, 70, 110, 255],
|
|
2628
|
+
robotHeading: [255, 255, 255, 255],
|
|
2629
|
+
charger: [40, 160, 80, 255],
|
|
2630
|
+
noGoFill: [220, 60, 60, 70],
|
|
2631
|
+
noGoBorder: [200, 40, 40, 220],
|
|
2632
|
+
noMopFill: [60, 120, 220, 60],
|
|
2633
|
+
noMopBorder: [40, 90, 200, 200],
|
|
2634
|
+
virtualWall: [200, 40, 40, 230],
|
|
2635
|
+
obstacle: [230, 150, 30, 255],
|
|
2636
|
+
label: [40, 40, 40, 255]
|
|
2637
|
+
},
|
|
2638
|
+
"dreame-dark": {
|
|
2639
|
+
floor: [40, 46, 56, 255],
|
|
2640
|
+
wall: [15, 15, 18, 255],
|
|
2641
|
+
carpet: [80, 66, 52, 200],
|
|
2642
|
+
segSat: 0.5,
|
|
2643
|
+
segVal: 0.62,
|
|
2644
|
+
path: [120, 170, 230, 235],
|
|
2645
|
+
robotBody: [120, 170, 230, 255],
|
|
2646
|
+
robotHeading: [20, 24, 30, 255],
|
|
2647
|
+
charger: [60, 200, 110, 255],
|
|
2648
|
+
noGoFill: [220, 70, 70, 80],
|
|
2649
|
+
noGoBorder: [230, 70, 70, 220],
|
|
2650
|
+
noMopFill: [70, 130, 230, 70],
|
|
2651
|
+
noMopBorder: [80, 140, 235, 210],
|
|
2652
|
+
virtualWall: [230, 70, 70, 235],
|
|
2653
|
+
obstacle: [240, 180, 60, 255],
|
|
2654
|
+
label: [225, 230, 240, 255]
|
|
2655
|
+
},
|
|
2656
|
+
"mijia-light": {
|
|
2657
|
+
floor: [225, 232, 240, 255],
|
|
2658
|
+
wall: [70, 78, 92, 255],
|
|
2659
|
+
carpet: [188, 162, 132, 200],
|
|
2660
|
+
segSat: 0.35,
|
|
2661
|
+
segVal: 0.98,
|
|
2662
|
+
path: [80, 130, 200, 230],
|
|
2663
|
+
robotBody: [30, 100, 200, 255],
|
|
2664
|
+
robotHeading: [255, 255, 255, 255],
|
|
2665
|
+
charger: [40, 170, 90, 255],
|
|
2666
|
+
noGoFill: [225, 70, 70, 70],
|
|
2667
|
+
noGoBorder: [205, 45, 45, 220],
|
|
2668
|
+
noMopFill: [70, 130, 225, 60],
|
|
2669
|
+
noMopBorder: [50, 100, 205, 200],
|
|
2670
|
+
virtualWall: [205, 45, 45, 230],
|
|
2671
|
+
obstacle: [235, 160, 35, 255],
|
|
2672
|
+
label: [45, 52, 64, 255]
|
|
2673
|
+
},
|
|
2674
|
+
tasshack: {
|
|
2675
|
+
floor: [200, 210, 225, 255],
|
|
2676
|
+
wall: [80, 80, 95, 255],
|
|
2677
|
+
carpet: [176, 148, 118, 205],
|
|
2678
|
+
segSat: 0.5,
|
|
2679
|
+
segVal: 0.9,
|
|
2680
|
+
path: [50, 100, 170, 235],
|
|
2681
|
+
robotBody: [35, 60, 100, 255],
|
|
2682
|
+
robotHeading: [255, 255, 255, 255],
|
|
2683
|
+
charger: [35, 150, 75, 255],
|
|
2684
|
+
noGoFill: [215, 55, 55, 75],
|
|
2685
|
+
noGoBorder: [195, 35, 35, 220],
|
|
2686
|
+
noMopFill: [55, 115, 215, 65],
|
|
2687
|
+
noMopBorder: [35, 85, 195, 205],
|
|
2688
|
+
virtualWall: [195, 35, 35, 230],
|
|
2689
|
+
obstacle: [225, 145, 25, 255],
|
|
2690
|
+
label: [35, 42, 54, 255]
|
|
2691
|
+
}
|
|
2591
2692
|
};
|
|
2592
2693
|
function hsvToRgba(h, s, v) {
|
|
2593
2694
|
const c = v * s;
|
|
@@ -2618,49 +2719,230 @@ function hsvToRgba(h, s, v) {
|
|
|
2618
2719
|
const m = v - c;
|
|
2619
2720
|
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), 255];
|
|
2620
2721
|
}
|
|
2621
|
-
function segmentColor(id) {
|
|
2722
|
+
function segmentColor(id, pal) {
|
|
2622
2723
|
const hue = id * 137.508 % 360;
|
|
2623
|
-
return hsvToRgba(hue,
|
|
2724
|
+
return hsvToRgba(hue, pal.segSat, pal.segVal);
|
|
2624
2725
|
}
|
|
2625
2726
|
function renderVacuumPng(map, opts = {}) {
|
|
2626
2727
|
const scale = Math.max(1, Math.trunc(opts.scale ?? 1));
|
|
2627
|
-
const
|
|
2628
|
-
const
|
|
2629
|
-
const
|
|
2728
|
+
const pal = SCHEMES[opts.colorScheme ?? "dreame-light"];
|
|
2729
|
+
const w = Math.max(1, map.dimensions.width * scale);
|
|
2730
|
+
const h = Math.max(1, map.dimensions.height * scale);
|
|
2731
|
+
const png = new PNG({ width: w, height: h });
|
|
2630
2732
|
png.data.fill(0);
|
|
2631
2733
|
for (const layer of map.layers) {
|
|
2632
|
-
paintLayer(png, layer, scale);
|
|
2734
|
+
paintLayer(png, layer, scale, pal);
|
|
2735
|
+
}
|
|
2736
|
+
const dim = map.dimensions;
|
|
2737
|
+
if (opts.showNoGo !== false) {
|
|
2738
|
+
for (const area of map.restrictedAreas) {
|
|
2739
|
+
const fill = area.kind === "noMop" ? pal.noMopFill : pal.noGoFill;
|
|
2740
|
+
const border = area.kind === "noMop" ? pal.noMopBorder : pal.noGoBorder;
|
|
2741
|
+
const a = worldToPx(area.bbox.xMin, area.bbox.yMin, dim, scale);
|
|
2742
|
+
const b = worldToPx(area.bbox.xMax, area.bbox.yMax, dim, scale);
|
|
2743
|
+
fillRect(png, a.x, a.y, b.x, b.y, fill);
|
|
2744
|
+
strokeRect(png, a.x, a.y, b.x, b.y, border);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
if (opts.showVirtualWalls !== false) {
|
|
2748
|
+
for (const vw of map.virtualWalls) {
|
|
2749
|
+
const a = worldToPx(vw.from.x, vw.from.y, dim, scale);
|
|
2750
|
+
const b = worldToPx(vw.to.x, vw.to.y, dim, scale);
|
|
2751
|
+
drawLine(png, a.x, a.y, b.x, b.y, pal.virtualWall, Math.max(1, scale));
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (opts.showPath !== false) {
|
|
2755
|
+
for (const path of map.paths) {
|
|
2756
|
+
drawPolyline(png, path.points, dim, scale, pal.path, Math.max(1, scale));
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (opts.showObstacles !== false) {
|
|
2760
|
+
for (const ob of map.obstacles) {
|
|
2761
|
+
const p = worldToPx(ob.x, ob.y, dim, scale);
|
|
2762
|
+
drawDisk(png, p.x, p.y, Math.max(2, scale * 2), pal.obstacle);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (opts.showCharger !== false && map.dock !== null) {
|
|
2766
|
+
drawCharger(png, map.dock, dim, scale, pal);
|
|
2767
|
+
}
|
|
2768
|
+
if (opts.showRobot !== false && map.robot !== null) {
|
|
2769
|
+
drawRobot(png, map.robot, dim, scale, pal);
|
|
2770
|
+
}
|
|
2771
|
+
if (opts.showSegmentLabels === true) {
|
|
2772
|
+
for (const seg of map.segments) {
|
|
2773
|
+
const p = worldToPx(seg.centroid.x, seg.centroid.y, dim, scale);
|
|
2774
|
+
drawLabel(png, p.x, p.y, String(seg.id), pal.label, Math.max(1, scale));
|
|
2775
|
+
}
|
|
2633
2776
|
}
|
|
2634
2777
|
return PNG.sync.write(png);
|
|
2635
2778
|
}
|
|
2636
|
-
function
|
|
2637
|
-
|
|
2779
|
+
function worldToPx(worldX, worldY, dim, scale) {
|
|
2780
|
+
return {
|
|
2781
|
+
x: Math.round((worldX - dim.left) / dim.gridSize * scale),
|
|
2782
|
+
y: Math.round((worldY - dim.top) / dim.gridSize * scale)
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
function paintLayer(png, layer, scale, pal) {
|
|
2786
|
+
const color = layer.type === "segment" ? segmentColor(layer.segmentId ?? 0, pal) : layer.type === "wall" ? pal.wall : layer.type === "carpet" ? pal.carpet : pal.floor;
|
|
2638
2787
|
for (const run of layer.runs) {
|
|
2639
|
-
paintRun(png, run, color, scale
|
|
2788
|
+
paintRun(png, run, color, scale);
|
|
2640
2789
|
}
|
|
2641
2790
|
}
|
|
2642
|
-
function paintRun(png, run, color, scale
|
|
2791
|
+
function paintRun(png, run, color, scale) {
|
|
2643
2792
|
const [xPx, yPx, len] = run;
|
|
2644
2793
|
for (let i = 0; i < len; i += 1) {
|
|
2645
2794
|
const baseX = (xPx + i) * scale;
|
|
2646
2795
|
const baseY = yPx * scale;
|
|
2647
2796
|
for (let dy = 0; dy < scale; dy += 1) {
|
|
2648
|
-
const py = baseY + dy;
|
|
2649
|
-
if (py < 0 || py >= height) {
|
|
2650
|
-
continue;
|
|
2651
|
-
}
|
|
2652
2797
|
for (let dx = 0; dx < scale; dx += 1) {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2798
|
+
setPixel(png, baseX + dx, baseY + dy, color);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
function setPixel(png, x, y, color) {
|
|
2804
|
+
if (x < 0 || y < 0 || x >= png.width || y >= png.height) return;
|
|
2805
|
+
const off = (y * png.width + x) * 4;
|
|
2806
|
+
const a = color[3];
|
|
2807
|
+
if (a >= 255) {
|
|
2808
|
+
png.data[off] = color[0];
|
|
2809
|
+
png.data[off + 1] = color[1];
|
|
2810
|
+
png.data[off + 2] = color[2];
|
|
2811
|
+
png.data[off + 3] = 255;
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
const sa = a / 255;
|
|
2815
|
+
const da = (png.data[off + 3] ?? 0) / 255;
|
|
2816
|
+
const outA = sa + da * (1 - sa);
|
|
2817
|
+
if (outA <= 0) return;
|
|
2818
|
+
for (let k = 0; k < 3; k += 1) {
|
|
2819
|
+
const src = color[k] ?? 0;
|
|
2820
|
+
const dst = png.data[off + k] ?? 0;
|
|
2821
|
+
png.data[off + k] = Math.round((src * sa + dst * da * (1 - sa)) / outA);
|
|
2822
|
+
}
|
|
2823
|
+
png.data[off + 3] = Math.round(outA * 255);
|
|
2824
|
+
}
|
|
2825
|
+
function fillRect(png, x0, y0, x1, y1, color) {
|
|
2826
|
+
const xa = Math.min(x0, x1);
|
|
2827
|
+
const xb = Math.max(x0, x1);
|
|
2828
|
+
const ya = Math.min(y0, y1);
|
|
2829
|
+
const yb = Math.max(y0, y1);
|
|
2830
|
+
for (let y = ya; y <= yb; y += 1) {
|
|
2831
|
+
for (let x = xa; x <= xb; x += 1) {
|
|
2832
|
+
setPixel(png, x, y, color);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
function strokeRect(png, x0, y0, x1, y1, color) {
|
|
2837
|
+
const xa = Math.min(x0, x1);
|
|
2838
|
+
const xb = Math.max(x0, x1);
|
|
2839
|
+
const ya = Math.min(y0, y1);
|
|
2840
|
+
const yb = Math.max(y0, y1);
|
|
2841
|
+
for (let x = xa; x <= xb; x += 1) {
|
|
2842
|
+
setPixel(png, x, ya, color);
|
|
2843
|
+
setPixel(png, x, yb, color);
|
|
2844
|
+
}
|
|
2845
|
+
for (let y = ya; y <= yb; y += 1) {
|
|
2846
|
+
setPixel(png, xa, y, color);
|
|
2847
|
+
setPixel(png, xb, y, color);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
function drawLine(png, x0, y0, x1, y1, color, thickness) {
|
|
2851
|
+
let x = x0;
|
|
2852
|
+
let y = y0;
|
|
2853
|
+
const dx = Math.abs(x1 - x0);
|
|
2854
|
+
const dy = -Math.abs(y1 - y0);
|
|
2855
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
2856
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
2857
|
+
let err = dx + dy;
|
|
2858
|
+
const r = Math.max(0, Math.trunc((thickness - 1) / 2));
|
|
2859
|
+
for (; ; ) {
|
|
2860
|
+
for (let oy = -r; oy <= r; oy += 1) {
|
|
2861
|
+
for (let ox = -r; ox <= r; ox += 1) {
|
|
2862
|
+
setPixel(png, x + ox, y + oy, color);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
if (x === x1 && y === y1) break;
|
|
2866
|
+
const e2 = 2 * err;
|
|
2867
|
+
if (e2 >= dy) {
|
|
2868
|
+
err += dy;
|
|
2869
|
+
x += sx;
|
|
2870
|
+
}
|
|
2871
|
+
if (e2 <= dx) {
|
|
2872
|
+
err += dx;
|
|
2873
|
+
y += sy;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
function drawPolyline(png, points, dim, scale, color, thickness) {
|
|
2878
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
2879
|
+
const p0 = points[i - 1];
|
|
2880
|
+
const p1 = points[i];
|
|
2881
|
+
if (p0 === void 0 || p1 === void 0) continue;
|
|
2882
|
+
const a = worldToPx(p0.x, p0.y, dim, scale);
|
|
2883
|
+
const b = worldToPx(p1.x, p1.y, dim, scale);
|
|
2884
|
+
drawLine(png, a.x, a.y, b.x, b.y, color, thickness);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
function drawDisk(png, cx, cy, r, color) {
|
|
2888
|
+
const rr = r * r;
|
|
2889
|
+
for (let dy = -r; dy <= r; dy += 1) {
|
|
2890
|
+
for (let dx = -r; dx <= r; dx += 1) {
|
|
2891
|
+
if (dx * dx + dy * dy <= rr) setPixel(png, cx + dx, cy + dy, color);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
function drawRobot(png, robot, dim, scale, pal) {
|
|
2896
|
+
const p = worldToPx(robot.x, robot.y, dim, scale);
|
|
2897
|
+
const r = Math.max(3, scale * 3);
|
|
2898
|
+
drawDisk(png, p.x, p.y, r, pal.robotBody);
|
|
2899
|
+
const rad = robot.angle * Math.PI / 180;
|
|
2900
|
+
const hx = Math.round(p.x + Math.cos(rad) * r);
|
|
2901
|
+
const hy = Math.round(p.y + Math.sin(rad) * r);
|
|
2902
|
+
drawLine(png, p.x, p.y, hx, hy, pal.robotHeading, Math.max(1, Math.trunc(scale / 2) + 1));
|
|
2903
|
+
}
|
|
2904
|
+
function drawCharger(png, dock, dim, scale, pal) {
|
|
2905
|
+
const p = worldToPx(dock.x, dock.y, dim, scale);
|
|
2906
|
+
const r = Math.max(2, scale * 2);
|
|
2907
|
+
fillRect(png, p.x - r, p.y - r, p.x + r, p.y + r, pal.charger);
|
|
2908
|
+
}
|
|
2909
|
+
var GLYPHS = {
|
|
2910
|
+
"0": [7, 5, 5, 5, 7],
|
|
2911
|
+
"1": [2, 6, 2, 2, 7],
|
|
2912
|
+
"2": [7, 1, 7, 4, 7],
|
|
2913
|
+
"3": [7, 1, 7, 1, 7],
|
|
2914
|
+
"4": [5, 5, 7, 1, 1],
|
|
2915
|
+
"5": [7, 4, 7, 1, 7],
|
|
2916
|
+
"6": [7, 4, 7, 5, 7],
|
|
2917
|
+
"7": [7, 1, 2, 2, 2],
|
|
2918
|
+
"8": [7, 5, 7, 5, 7],
|
|
2919
|
+
"9": [7, 5, 7, 1, 7],
|
|
2920
|
+
"-": [0, 0, 7, 0, 0]
|
|
2921
|
+
};
|
|
2922
|
+
function drawLabel(png, cx, cy, text, color, px) {
|
|
2923
|
+
const glyphW = 3 * px;
|
|
2924
|
+
const glyphH = 5 * px;
|
|
2925
|
+
const gap = px;
|
|
2926
|
+
const totalW = text.length * glyphW + Math.max(0, text.length - 1) * gap;
|
|
2927
|
+
let originX = Math.round(cx - totalW / 2);
|
|
2928
|
+
const originY = Math.round(cy - glyphH / 2);
|
|
2929
|
+
for (const ch of text) {
|
|
2930
|
+
const glyph = GLYPHS[ch];
|
|
2931
|
+
if (glyph !== void 0) {
|
|
2932
|
+
for (let row = 0; row < 5; row += 1) {
|
|
2933
|
+
const bits = glyph[row];
|
|
2934
|
+
if (bits === void 0) continue;
|
|
2935
|
+
for (let col = 0; col < 3; col += 1) {
|
|
2936
|
+
if ((bits & 1 << 2 - col) === 0) continue;
|
|
2937
|
+
for (let sy = 0; sy < px; sy += 1) {
|
|
2938
|
+
for (let sx = 0; sx < px; sx += 1) {
|
|
2939
|
+
setPixel(png, originX + col * px + sx, originY + row * px + sy, color);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2656
2942
|
}
|
|
2657
|
-
const off = (py * width + px) * 4;
|
|
2658
|
-
png.data[off] = color[0];
|
|
2659
|
-
png.data[off + 1] = color[1];
|
|
2660
|
-
png.data[off + 2] = color[2];
|
|
2661
|
-
png.data[off + 3] = color[3];
|
|
2662
2943
|
}
|
|
2663
2944
|
}
|
|
2945
|
+
originX += glyphW + gap;
|
|
2664
2946
|
}
|
|
2665
2947
|
}
|
|
2666
2948
|
|