@apocaliss92/nodedreame 1.11.3 → 1.11.4

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.d.cts CHANGED
@@ -1229,14 +1229,26 @@ interface VacuumMap {
1229
1229
  *
1230
1230
  * Coordinate transform: every overlay coordinate is mm in the world frame.
1231
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.
1232
+ * mm-per-pixel, so `xPx = (worldX - left) / gridSize` (then × `scale`).
1233
+ *
1234
+ * Y IS FLIPPED. Dreame's grid has world-Y increasing DOWNWARD relative to how
1235
+ * the robot/dock/app present the floor, so rendering rows top-down produced a
1236
+ * VERTICALLY MIRRORED map vs the Dreamehome app / Home Assistant (live-confirmed
1237
+ * on r2538z). We reflect in cell space — `yPx = ((height - 1) - cellY) × scale` —
1238
+ * applied identically to the run-length base layers ({@link paintRun}) and the
1239
+ * world-frame overlays ({@link worldToPx}) so they stay pixel-aligned, and the
1240
+ * robot heading's Y component is negated ({@link drawRobot}). Glyph labels are
1241
+ * drawn upright (only their anchor is flipped).
1234
1242
  *
1235
1243
  * Pure: only `pngjs` + arithmetic. No casts.
1236
1244
  */
1237
1245
 
1238
- /** A named map palette. Mirrors the HA integration's `color_scheme` choices. */
1239
- type MapColorScheme = 'dreame-light' | 'dreame-dark' | 'mijia-light' | 'tasshack';
1246
+ /**
1247
+ * A named map palette. The first four mirror the HA integration's `color_scheme`
1248
+ * choices; `flat` / `dark-neon` / `materico` are node-dreame additions for
1249
+ * distinct visual styles.
1250
+ */
1251
+ type MapColorScheme = 'dreame-light' | 'dreame-dark' | 'mijia-light' | 'tasshack' | 'flat' | 'dark-neon' | 'materico';
1240
1252
  interface RenderVacuumPngOptions {
1241
1253
  /** Integer upscale factor (nearest-neighbour). Default 1. */
1242
1254
  scale?: number;
@@ -1256,6 +1268,15 @@ interface RenderVacuumPngOptions {
1256
1268
  showObstacles?: boolean;
1257
1269
  /** Draw the segment id as a tiny label at each room centroid. Default false. */
1258
1270
  showSegmentLabels?: boolean;
1271
+ /** Draw each room's NAME (falls back to `Room <id>`) at its centroid. Default
1272
+ * false. Mutually useful with `showSegmentLabels` (names take the centroid). */
1273
+ showSegmentNames?: boolean;
1274
+ /** Tint the cleaned-area overlay (`cleanedArea.cleaned`) when present. Default false. */
1275
+ showCleanedArea?: boolean;
1276
+ /** Outline low-clearance "sneak under furniture" zones. Default true. */
1277
+ showFurniture?: boolean;
1278
+ /** Colour AI obstacle markers by their `type` (else a single accent). Default true. */
1279
+ colorObstaclesByType?: boolean;
1259
1280
  }
1260
1281
  declare function renderVacuumPng(map: VacuumMap, opts?: RenderVacuumPngOptions): Buffer;
1261
1282
 
package/dist/index.d.ts CHANGED
@@ -1229,14 +1229,26 @@ interface VacuumMap {
1229
1229
  *
1230
1230
  * Coordinate transform: every overlay coordinate is mm in the world frame.
1231
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.
1232
+ * mm-per-pixel, so `xPx = (worldX - left) / gridSize` (then × `scale`).
1233
+ *
1234
+ * Y IS FLIPPED. Dreame's grid has world-Y increasing DOWNWARD relative to how
1235
+ * the robot/dock/app present the floor, so rendering rows top-down produced a
1236
+ * VERTICALLY MIRRORED map vs the Dreamehome app / Home Assistant (live-confirmed
1237
+ * on r2538z). We reflect in cell space — `yPx = ((height - 1) - cellY) × scale` —
1238
+ * applied identically to the run-length base layers ({@link paintRun}) and the
1239
+ * world-frame overlays ({@link worldToPx}) so they stay pixel-aligned, and the
1240
+ * robot heading's Y component is negated ({@link drawRobot}). Glyph labels are
1241
+ * drawn upright (only their anchor is flipped).
1234
1242
  *
1235
1243
  * Pure: only `pngjs` + arithmetic. No casts.
1236
1244
  */
1237
1245
 
1238
- /** A named map palette. Mirrors the HA integration's `color_scheme` choices. */
1239
- type MapColorScheme = 'dreame-light' | 'dreame-dark' | 'mijia-light' | 'tasshack';
1246
+ /**
1247
+ * A named map palette. The first four mirror the HA integration's `color_scheme`
1248
+ * choices; `flat` / `dark-neon` / `materico` are node-dreame additions for
1249
+ * distinct visual styles.
1250
+ */
1251
+ type MapColorScheme = 'dreame-light' | 'dreame-dark' | 'mijia-light' | 'tasshack' | 'flat' | 'dark-neon' | 'materico';
1240
1252
  interface RenderVacuumPngOptions {
1241
1253
  /** Integer upscale factor (nearest-neighbour). Default 1. */
1242
1254
  scale?: number;
@@ -1256,6 +1268,15 @@ interface RenderVacuumPngOptions {
1256
1268
  showObstacles?: boolean;
1257
1269
  /** Draw the segment id as a tiny label at each room centroid. Default false. */
1258
1270
  showSegmentLabels?: boolean;
1271
+ /** Draw each room's NAME (falls back to `Room <id>`) at its centroid. Default
1272
+ * false. Mutually useful with `showSegmentLabels` (names take the centroid). */
1273
+ showSegmentNames?: boolean;
1274
+ /** Tint the cleaned-area overlay (`cleanedArea.cleaned`) when present. Default false. */
1275
+ showCleanedArea?: boolean;
1276
+ /** Outline low-clearance "sneak under furniture" zones. Default true. */
1277
+ showFurniture?: boolean;
1278
+ /** Colour AI obstacle markers by their `type` (else a single accent). Default true. */
1279
+ colorObstaclesByType?: boolean;
1259
1280
  }
1260
1281
  declare function renderVacuumPng(map: VacuumMap, opts?: RenderVacuumPngOptions): Buffer;
1261
1282
 
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.11.3";
3
+ var LIBRARY_VERSION = "1.11.4";
4
4
 
5
5
  // src/transport/errors.ts
6
6
  var DreameError = class extends Error {
@@ -2898,6 +2898,66 @@ var SCHEMES = {
2898
2898
  virtualWall: [195, 35, 35, 230],
2899
2899
  obstacle: [225, 145, 25, 255],
2900
2900
  label: [35, 42, 54, 255]
2901
+ },
2902
+ // Flat-design: soft neutral floor, bright low-saturation segment fills, thin
2903
+ // mid-grey walls, a single vivid accent for the path.
2904
+ flat: {
2905
+ floor: [236, 239, 241, 255],
2906
+ wall: [120, 132, 145, 255],
2907
+ carpet: [205, 185, 160, 200],
2908
+ segSat: 0.28,
2909
+ segVal: 0.99,
2910
+ path: [255, 138, 101, 240],
2911
+ robotBody: [38, 50, 56, 255],
2912
+ robotHeading: [255, 255, 255, 255],
2913
+ charger: [38, 166, 154, 255],
2914
+ noGoFill: [239, 83, 80, 60],
2915
+ noGoBorder: [229, 57, 53, 210],
2916
+ noMopFill: [66, 165, 245, 55],
2917
+ noMopBorder: [30, 136, 229, 200],
2918
+ virtualWall: [229, 57, 53, 225],
2919
+ obstacle: [255, 167, 38, 255],
2920
+ label: [55, 71, 79, 255]
2921
+ },
2922
+ // Dark-neon: near-black floor with high-saturation glowing segment fills, a
2923
+ // bright cyan path and magenta robot.
2924
+ "dark-neon": {
2925
+ floor: [16, 18, 27, 255],
2926
+ wall: [5, 6, 10, 255],
2927
+ carpet: [60, 50, 80, 200],
2928
+ segSat: 0.85,
2929
+ segVal: 0.95,
2930
+ path: [0, 230, 255, 240],
2931
+ robotBody: [255, 45, 170, 255],
2932
+ robotHeading: [255, 255, 255, 255],
2933
+ charger: [0, 255, 150, 255],
2934
+ noGoFill: [255, 40, 90, 80],
2935
+ noGoBorder: [255, 40, 90, 235],
2936
+ noMopFill: [40, 130, 255, 75],
2937
+ noMopBorder: [40, 160, 255, 225],
2938
+ virtualWall: [255, 40, 90, 235],
2939
+ obstacle: [255, 220, 0, 255],
2940
+ label: [230, 240, 255, 255]
2941
+ },
2942
+ // Materico (Material-design): light grey surface, Material-palette segment
2943
+ // fills, indigo/teal accents.
2944
+ materico: {
2945
+ floor: [245, 245, 245, 255],
2946
+ wall: [97, 97, 97, 255],
2947
+ carpet: [188, 170, 144, 205],
2948
+ segSat: 0.55,
2949
+ segVal: 0.93,
2950
+ path: [63, 81, 181, 235],
2951
+ robotBody: [48, 63, 159, 255],
2952
+ robotHeading: [255, 255, 255, 255],
2953
+ charger: [0, 150, 136, 255],
2954
+ noGoFill: [244, 67, 54, 65],
2955
+ noGoBorder: [211, 47, 47, 215],
2956
+ noMopFill: [33, 150, 243, 60],
2957
+ noMopBorder: [25, 118, 210, 205],
2958
+ virtualWall: [211, 47, 47, 225],
2959
+ obstacle: [255, 152, 0, 255],
2960
+ label: [33, 33, 33, 255]
2901
2961
  }
2902
2962
  };
2903
2963
  function hsvToRgba(h, s, v) {
@@ -2940,10 +3000,13 @@ function renderVacuumPng(map, opts = {}) {
2940
3000
  const h = Math.max(1, map.dimensions.height * scale);
2941
3001
  const png = new PNG({ width: w, height: h });
2942
3002
  png.data.fill(0);
3003
+ const dim = map.dimensions;
2943
3004
  for (const layer of map.layers) {
2944
- paintLayer(png, layer, scale, pal);
3005
+ paintLayer(png, layer, dim.height, scale, pal);
3006
+ }
3007
+ if (opts.showCleanedArea === true && map.cleanedArea !== null) {
3008
+ paintCleanedArea(png, map.cleanedArea, dim, scale, pal.path);
2945
3009
  }
2946
- const dim = map.dimensions;
2947
3010
  if (opts.showNoGo !== false) {
2948
3011
  for (const area of map.restrictedAreas) {
2949
3012
  const fill = area.kind === "noMop" ? pal.noMopFill : pal.noGoFill;
@@ -2966,10 +3029,17 @@ function renderVacuumPng(map, opts = {}) {
2966
3029
  drawPolyline(png, path.points, dim, scale, pal.path, Math.max(1, scale));
2967
3030
  }
2968
3031
  }
3032
+ if (opts.showFurniture !== false) {
3033
+ for (const zone of map.lowLyingAreas) {
3034
+ paintFurnitureZone(png, zone.points, dim, scale);
3035
+ }
3036
+ }
2969
3037
  if (opts.showObstacles !== false) {
3038
+ const byType = opts.colorObstaclesByType !== false;
2970
3039
  for (const ob of map.obstacles) {
2971
3040
  const p = worldToPx(ob.x, ob.y, dim, scale);
2972
- drawDisk(png, p.x, p.y, Math.max(2, scale * 2), pal.obstacle);
3041
+ const color = byType ? obstacleColor(ob.type) : pal.obstacle;
3042
+ drawDiamond(png, p.x, p.y, Math.max(2, scale * 2), color);
2973
3043
  }
2974
3044
  }
2975
3045
  if (opts.showCharger !== false && map.dock !== null) {
@@ -2978,31 +3048,33 @@ function renderVacuumPng(map, opts = {}) {
2978
3048
  if (opts.showRobot !== false && map.robot !== null) {
2979
3049
  drawRobot(png, map.robot, dim, scale, pal);
2980
3050
  }
2981
- if (opts.showSegmentLabels === true) {
3051
+ if (opts.showSegmentNames === true || opts.showSegmentLabels === true) {
2982
3052
  for (const seg of map.segments) {
2983
3053
  const p = worldToPx(seg.centroid.x, seg.centroid.y, dim, scale);
2984
- drawLabel(png, p.x, p.y, String(seg.id), pal.label, Math.max(1, scale));
3054
+ const text = opts.showSegmentNames === true ? labelText(seg.name, seg.id) : String(seg.id);
3055
+ drawLabel(png, p.x, p.y, text, pal.label, Math.max(1, scale));
2985
3056
  }
2986
3057
  }
2987
3058
  return PNG.sync.write(png);
2988
3059
  }
2989
3060
  function worldToPx(worldX, worldY, dim, scale) {
3061
+ const cellY = (worldY - dim.top) / dim.gridSize;
2990
3062
  return {
2991
3063
  x: Math.round((worldX - dim.left) / dim.gridSize * scale),
2992
- y: Math.round((worldY - dim.top) / dim.gridSize * scale)
3064
+ y: Math.round((dim.height - 1 - cellY) * scale)
2993
3065
  };
2994
3066
  }
2995
- function paintLayer(png, layer, scale, pal) {
3067
+ function paintLayer(png, layer, dimHeight, scale, pal) {
2996
3068
  const color = layer.type === "segment" ? segmentColor(layer.segmentId ?? 0, pal) : layer.type === "wall" ? pal.wall : layer.type === "carpet" ? pal.carpet : pal.floor;
2997
3069
  for (const run of layer.runs) {
2998
- paintRun(png, run, color, scale);
3070
+ paintRun(png, run, dimHeight, color, scale);
2999
3071
  }
3000
3072
  }
3001
- function paintRun(png, run, color, scale) {
3073
+ function paintRun(png, run, dimHeight, color, scale) {
3002
3074
  const [xPx, yPx, len] = run;
3075
+ const baseY = (dimHeight - 1 - yPx) * scale;
3003
3076
  for (let i = 0; i < len; i += 1) {
3004
3077
  const baseX = (xPx + i) * scale;
3005
- const baseY = yPx * scale;
3006
3078
  for (let dy = 0; dy < scale; dy += 1) {
3007
3079
  for (let dx = 0; dx < scale; dx += 1) {
3008
3080
  setPixel(png, baseX + dx, baseY + dy, color);
@@ -3108,7 +3180,7 @@ function drawRobot(png, robot, dim, scale, pal) {
3108
3180
  drawDisk(png, p.x, p.y, r, pal.robotBody);
3109
3181
  const rad = robot.angle * Math.PI / 180;
3110
3182
  const hx = Math.round(p.x + Math.cos(rad) * r);
3111
- const hy = Math.round(p.y + Math.sin(rad) * r);
3183
+ const hy = Math.round(p.y - Math.sin(rad) * r);
3112
3184
  drawLine(png, p.x, p.y, hx, hy, pal.robotHeading, Math.max(1, Math.trunc(scale / 2) + 1));
3113
3185
  }
3114
3186
  function drawCharger(png, dock, dim, scale, pal) {
@@ -3116,6 +3188,58 @@ function drawCharger(png, dock, dim, scale, pal) {
3116
3188
  const r = Math.max(2, scale * 2);
3117
3189
  fillRect(png, p.x - r, p.y - r, p.x + r, p.y + r, pal.charger);
3118
3190
  }
3191
+ function drawDiamond(png, cx, cy, r, color) {
3192
+ for (let dy = -r; dy <= r; dy += 1) {
3193
+ const w = r - Math.abs(dy);
3194
+ for (let dx = -w; dx <= w; dx += 1) {
3195
+ setPixel(png, cx + dx, cy + dy, color);
3196
+ }
3197
+ }
3198
+ }
3199
+ function obstacleColor(type) {
3200
+ const hue = Math.abs(Math.trunc(type)) * 47 % 360;
3201
+ return hsvToRgba(hue, 0.85, 0.95);
3202
+ }
3203
+ var FURNITURE_LINE = [150, 110, 200, 235];
3204
+ function paintFurnitureZone(png, points, dim, scale) {
3205
+ if (points.length < 2) return;
3206
+ for (let i = 0; i < points.length; i += 1) {
3207
+ const a = points[i];
3208
+ const b = points[(i + 1) % points.length];
3209
+ if (a === void 0 || b === void 0) continue;
3210
+ const pa = worldToPx(a.x, a.y, dim, scale);
3211
+ const pb = worldToPx(b.x, b.y, dim, scale);
3212
+ drawLine(png, pa.x, pa.y, pb.x, pb.y, FURNITURE_LINE, Math.max(1, scale));
3213
+ }
3214
+ }
3215
+ function paintCleanedArea(png, overlay, parentDim, scale, baseColor) {
3216
+ const tint = [baseColor[0], baseColor[1], baseColor[2], 55];
3217
+ const inner = overlay.dimensions;
3218
+ const block = Math.max(1, Math.round(inner.gridSize / parentDim.gridSize * scale));
3219
+ for (const [xPx, yPx, len] of overlay.cleaned) {
3220
+ for (let i = 0; i < len; i += 1) {
3221
+ const worldX = inner.left + (xPx + i) * inner.gridSize;
3222
+ const worldY = inner.top + yPx * inner.gridSize;
3223
+ const p = worldToPx(worldX, worldY, parentDim, scale);
3224
+ for (let dy = 0; dy < block; dy += 1) {
3225
+ for (let dx = 0; dx < block; dx += 1) {
3226
+ setPixel(png, p.x + dx, p.y + dy, tint);
3227
+ }
3228
+ }
3229
+ }
3230
+ }
3231
+ }
3232
+ function labelText(name, id) {
3233
+ const raw = (name ?? "").trim();
3234
+ const base = raw.length > 0 ? raw : `ROOM ${id}`;
3235
+ const upper = base.toUpperCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
3236
+ let out = "";
3237
+ for (const ch of upper) {
3238
+ if (GLYPHS[ch] !== void 0) out += ch;
3239
+ }
3240
+ const trimmed = out.trim().slice(0, 14);
3241
+ return trimmed.length > 0 ? trimmed : `ROOM ${id}`;
3242
+ }
3119
3243
  var GLYPHS = {
3120
3244
  "0": [7, 5, 5, 5, 7],
3121
3245
  "1": [2, 6, 2, 2, 7],
@@ -3127,7 +3251,34 @@ var GLYPHS = {
3127
3251
  "7": [7, 1, 2, 2, 2],
3128
3252
  "8": [7, 5, 7, 5, 7],
3129
3253
  "9": [7, 5, 7, 1, 7],
3130
- "-": [0, 0, 7, 0, 0]
3254
+ "-": [0, 0, 7, 0, 0],
3255
+ " ": [0, 0, 0, 0, 0],
3256
+ A: [2, 5, 7, 5, 5],
3257
+ B: [6, 5, 6, 5, 6],
3258
+ C: [3, 4, 4, 4, 3],
3259
+ D: [6, 5, 5, 5, 6],
3260
+ E: [7, 4, 6, 4, 7],
3261
+ F: [7, 4, 6, 4, 4],
3262
+ G: [3, 4, 5, 5, 3],
3263
+ H: [5, 5, 7, 5, 5],
3264
+ I: [7, 2, 2, 2, 7],
3265
+ J: [1, 1, 1, 5, 2],
3266
+ K: [5, 5, 6, 5, 5],
3267
+ L: [4, 4, 4, 4, 7],
3268
+ M: [5, 7, 7, 5, 5],
3269
+ N: [5, 7, 7, 7, 5],
3270
+ O: [2, 5, 5, 5, 2],
3271
+ P: [6, 5, 6, 4, 4],
3272
+ Q: [2, 5, 5, 6, 3],
3273
+ R: [6, 5, 6, 5, 5],
3274
+ S: [3, 4, 2, 1, 6],
3275
+ T: [7, 2, 2, 2, 2],
3276
+ U: [5, 5, 5, 5, 7],
3277
+ V: [5, 5, 5, 5, 2],
3278
+ W: [5, 5, 7, 7, 5],
3279
+ X: [5, 5, 2, 5, 5],
3280
+ Y: [5, 5, 2, 2, 2],
3281
+ Z: [7, 1, 2, 4, 7]
3131
3282
  };
3132
3283
  function drawLabel(png, cx, cy, text, color, px) {
3133
3284
  const glyphW = 3 * px;