@apocaliss92/nodedreame 1.11.3 → 1.11.5

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,18 +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
+ /** A named map palette — node-dreame's own distinct visual styles. */
1247
+ type MapColorScheme = 'flat' | 'dark-neon' | 'materico';
1240
1248
  interface RenderVacuumPngOptions {
1241
1249
  /** Integer upscale factor (nearest-neighbour). Default 1. */
1242
1250
  scale?: number;
1243
- /** Palette. Default `'dreame-light'`. */
1251
+ /** Palette. Default `'flat'`. */
1244
1252
  colorScheme?: MapColorScheme;
1245
1253
  /** Draw the cleaning/mopping path polylines. Default true. */
1246
1254
  showPath?: boolean;
@@ -1256,6 +1264,15 @@ interface RenderVacuumPngOptions {
1256
1264
  showObstacles?: boolean;
1257
1265
  /** Draw the segment id as a tiny label at each room centroid. Default false. */
1258
1266
  showSegmentLabels?: boolean;
1267
+ /** Draw each room's NAME (falls back to `Room <id>`) at its centroid. Default
1268
+ * false. Mutually useful with `showSegmentLabels` (names take the centroid). */
1269
+ showSegmentNames?: boolean;
1270
+ /** Tint the cleaned-area overlay (`cleanedArea.cleaned`) when present. Default false. */
1271
+ showCleanedArea?: boolean;
1272
+ /** Outline low-clearance "sneak under furniture" zones. Default true. */
1273
+ showFurniture?: boolean;
1274
+ /** Colour AI obstacle markers by their `type` (else a single accent). Default true. */
1275
+ colorObstaclesByType?: boolean;
1259
1276
  }
1260
1277
  declare function renderVacuumPng(map: VacuumMap, opts?: RenderVacuumPngOptions): Buffer;
1261
1278
 
package/dist/index.d.ts CHANGED
@@ -1229,18 +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
+ /** A named map palette — node-dreame's own distinct visual styles. */
1247
+ type MapColorScheme = 'flat' | 'dark-neon' | 'materico';
1240
1248
  interface RenderVacuumPngOptions {
1241
1249
  /** Integer upscale factor (nearest-neighbour). Default 1. */
1242
1250
  scale?: number;
1243
- /** Palette. Default `'dreame-light'`. */
1251
+ /** Palette. Default `'flat'`. */
1244
1252
  colorScheme?: MapColorScheme;
1245
1253
  /** Draw the cleaning/mopping path polylines. Default true. */
1246
1254
  showPath?: boolean;
@@ -1256,6 +1264,15 @@ interface RenderVacuumPngOptions {
1256
1264
  showObstacles?: boolean;
1257
1265
  /** Draw the segment id as a tiny label at each room centroid. Default false. */
1258
1266
  showSegmentLabels?: boolean;
1267
+ /** Draw each room's NAME (falls back to `Room <id>`) at its centroid. Default
1268
+ * false. Mutually useful with `showSegmentLabels` (names take the centroid). */
1269
+ showSegmentNames?: boolean;
1270
+ /** Tint the cleaned-area overlay (`cleanedArea.cleaned`) when present. Default false. */
1271
+ showCleanedArea?: boolean;
1272
+ /** Outline low-clearance "sneak under furniture" zones. Default true. */
1273
+ showFurniture?: boolean;
1274
+ /** Colour AI obstacle markers by their `type` (else a single accent). Default true. */
1275
+ colorObstaclesByType?: boolean;
1259
1276
  }
1260
1277
  declare function renderVacuumPng(map: VacuumMap, opts?: RenderVacuumPngOptions): Buffer;
1261
1278
 
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.5";
4
4
 
5
5
  // src/transport/errors.ts
6
6
  var DreameError = class extends Error {
@@ -2827,77 +2827,65 @@ function pose(x, y, angle, absent) {
2827
2827
  // src/models/vacuum/map/render.ts
2828
2828
  import { PNG } from "pngjs";
2829
2829
  var SCHEMES = {
2830
- "dreame-light": {
2831
- floor: [210, 222, 235, 255],
2832
- wall: [60, 60, 60, 255],
2833
- carpet: [180, 150, 120, 200],
2834
- segSat: 0.45,
2835
- segVal: 0.95,
2836
- path: [60, 110, 180, 235],
2837
- robotBody: [40, 70, 110, 255],
2830
+ // Flat-design: soft neutral floor, bright low-saturation segment fills, thin
2831
+ // mid-grey walls, a single vivid accent for the path.
2832
+ flat: {
2833
+ floor: [236, 239, 241, 255],
2834
+ wall: [120, 132, 145, 255],
2835
+ carpet: [205, 185, 160, 200],
2836
+ segSat: 0.28,
2837
+ segVal: 0.99,
2838
+ path: [255, 138, 101, 240],
2839
+ robotBody: [38, 50, 56, 255],
2838
2840
  robotHeading: [255, 255, 255, 255],
2839
- charger: [40, 160, 80, 255],
2840
- noGoFill: [220, 60, 60, 70],
2841
- noGoBorder: [200, 40, 40, 220],
2842
- noMopFill: [60, 120, 220, 60],
2843
- noMopBorder: [40, 90, 200, 200],
2844
- virtualWall: [200, 40, 40, 230],
2845
- obstacle: [230, 150, 30, 255],
2846
- label: [40, 40, 40, 255]
2847
- },
2848
- "dreame-dark": {
2849
- floor: [40, 46, 56, 255],
2850
- wall: [15, 15, 18, 255],
2851
- carpet: [80, 66, 52, 200],
2852
- segSat: 0.5,
2853
- segVal: 0.62,
2854
- path: [120, 170, 230, 235],
2855
- robotBody: [120, 170, 230, 255],
2856
- robotHeading: [20, 24, 30, 255],
2857
- charger: [60, 200, 110, 255],
2858
- noGoFill: [220, 70, 70, 80],
2859
- noGoBorder: [230, 70, 70, 220],
2860
- noMopFill: [70, 130, 230, 70],
2861
- noMopBorder: [80, 140, 235, 210],
2862
- virtualWall: [230, 70, 70, 235],
2863
- obstacle: [240, 180, 60, 255],
2864
- label: [225, 230, 240, 255]
2841
+ charger: [38, 166, 154, 255],
2842
+ noGoFill: [239, 83, 80, 60],
2843
+ noGoBorder: [229, 57, 53, 210],
2844
+ noMopFill: [66, 165, 245, 55],
2845
+ noMopBorder: [30, 136, 229, 200],
2846
+ virtualWall: [229, 57, 53, 225],
2847
+ obstacle: [255, 167, 38, 255],
2848
+ label: [55, 71, 79, 255]
2865
2849
  },
2866
- "mijia-light": {
2867
- floor: [225, 232, 240, 255],
2868
- wall: [70, 78, 92, 255],
2869
- carpet: [188, 162, 132, 200],
2870
- segSat: 0.35,
2871
- segVal: 0.98,
2872
- path: [80, 130, 200, 230],
2873
- robotBody: [30, 100, 200, 255],
2850
+ // Dark-neon: near-black floor with high-saturation glowing segment fills, a
2851
+ // bright cyan path and magenta robot.
2852
+ "dark-neon": {
2853
+ floor: [16, 18, 27, 255],
2854
+ wall: [5, 6, 10, 255],
2855
+ carpet: [60, 50, 80, 200],
2856
+ segSat: 0.85,
2857
+ segVal: 0.95,
2858
+ path: [0, 230, 255, 240],
2859
+ robotBody: [255, 45, 170, 255],
2874
2860
  robotHeading: [255, 255, 255, 255],
2875
- charger: [40, 170, 90, 255],
2876
- noGoFill: [225, 70, 70, 70],
2877
- noGoBorder: [205, 45, 45, 220],
2878
- noMopFill: [70, 130, 225, 60],
2879
- noMopBorder: [50, 100, 205, 200],
2880
- virtualWall: [205, 45, 45, 230],
2881
- obstacle: [235, 160, 35, 255],
2882
- label: [45, 52, 64, 255]
2861
+ charger: [0, 255, 150, 255],
2862
+ noGoFill: [255, 40, 90, 80],
2863
+ noGoBorder: [255, 40, 90, 235],
2864
+ noMopFill: [40, 130, 255, 75],
2865
+ noMopBorder: [40, 160, 255, 225],
2866
+ virtualWall: [255, 40, 90, 235],
2867
+ obstacle: [255, 220, 0, 255],
2868
+ label: [230, 240, 255, 255]
2883
2869
  },
2884
- tasshack: {
2885
- floor: [200, 210, 225, 255],
2886
- wall: [80, 80, 95, 255],
2887
- carpet: [176, 148, 118, 205],
2888
- segSat: 0.5,
2889
- segVal: 0.9,
2890
- path: [50, 100, 170, 235],
2891
- robotBody: [35, 60, 100, 255],
2870
+ // Materico (Material-design): light grey surface, Material-palette segment
2871
+ // fills, indigo/teal accents.
2872
+ materico: {
2873
+ floor: [245, 245, 245, 255],
2874
+ wall: [97, 97, 97, 255],
2875
+ carpet: [188, 170, 144, 205],
2876
+ segSat: 0.55,
2877
+ segVal: 0.93,
2878
+ path: [63, 81, 181, 235],
2879
+ robotBody: [48, 63, 159, 255],
2892
2880
  robotHeading: [255, 255, 255, 255],
2893
- charger: [35, 150, 75, 255],
2894
- noGoFill: [215, 55, 55, 75],
2895
- noGoBorder: [195, 35, 35, 220],
2896
- noMopFill: [55, 115, 215, 65],
2897
- noMopBorder: [35, 85, 195, 205],
2898
- virtualWall: [195, 35, 35, 230],
2899
- obstacle: [225, 145, 25, 255],
2900
- label: [35, 42, 54, 255]
2881
+ charger: [0, 150, 136, 255],
2882
+ noGoFill: [244, 67, 54, 65],
2883
+ noGoBorder: [211, 47, 47, 215],
2884
+ noMopFill: [33, 150, 243, 60],
2885
+ noMopBorder: [25, 118, 210, 205],
2886
+ virtualWall: [211, 47, 47, 225],
2887
+ obstacle: [255, 152, 0, 255],
2888
+ label: [33, 33, 33, 255]
2901
2889
  }
2902
2890
  };
2903
2891
  function hsvToRgba(h, s, v) {
@@ -2935,15 +2923,18 @@ function segmentColor(id, pal) {
2935
2923
  }
2936
2924
  function renderVacuumPng(map, opts = {}) {
2937
2925
  const scale = Math.max(1, Math.trunc(opts.scale ?? 1));
2938
- const pal = SCHEMES[opts.colorScheme ?? "dreame-light"];
2926
+ const pal = SCHEMES[opts.colorScheme ?? "flat"] ?? SCHEMES.flat;
2939
2927
  const w = Math.max(1, map.dimensions.width * scale);
2940
2928
  const h = Math.max(1, map.dimensions.height * scale);
2941
2929
  const png = new PNG({ width: w, height: h });
2942
2930
  png.data.fill(0);
2931
+ const dim = map.dimensions;
2943
2932
  for (const layer of map.layers) {
2944
- paintLayer(png, layer, scale, pal);
2933
+ paintLayer(png, layer, dim.height, scale, pal);
2934
+ }
2935
+ if (opts.showCleanedArea === true && map.cleanedArea !== null) {
2936
+ paintCleanedArea(png, map.cleanedArea, dim, scale, pal.path);
2945
2937
  }
2946
- const dim = map.dimensions;
2947
2938
  if (opts.showNoGo !== false) {
2948
2939
  for (const area of map.restrictedAreas) {
2949
2940
  const fill = area.kind === "noMop" ? pal.noMopFill : pal.noGoFill;
@@ -2966,10 +2957,17 @@ function renderVacuumPng(map, opts = {}) {
2966
2957
  drawPolyline(png, path.points, dim, scale, pal.path, Math.max(1, scale));
2967
2958
  }
2968
2959
  }
2960
+ if (opts.showFurniture !== false) {
2961
+ for (const zone of map.lowLyingAreas) {
2962
+ paintFurnitureZone(png, zone.points, dim, scale);
2963
+ }
2964
+ }
2969
2965
  if (opts.showObstacles !== false) {
2966
+ const byType = opts.colorObstaclesByType !== false;
2970
2967
  for (const ob of map.obstacles) {
2971
2968
  const p = worldToPx(ob.x, ob.y, dim, scale);
2972
- drawDisk(png, p.x, p.y, Math.max(2, scale * 2), pal.obstacle);
2969
+ const color = byType ? obstacleColor(ob.type) : pal.obstacle;
2970
+ drawDiamond(png, p.x, p.y, Math.max(2, scale * 2), color);
2973
2971
  }
2974
2972
  }
2975
2973
  if (opts.showCharger !== false && map.dock !== null) {
@@ -2978,31 +2976,33 @@ function renderVacuumPng(map, opts = {}) {
2978
2976
  if (opts.showRobot !== false && map.robot !== null) {
2979
2977
  drawRobot(png, map.robot, dim, scale, pal);
2980
2978
  }
2981
- if (opts.showSegmentLabels === true) {
2979
+ if (opts.showSegmentNames === true || opts.showSegmentLabels === true) {
2982
2980
  for (const seg of map.segments) {
2983
2981
  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));
2982
+ const text = opts.showSegmentNames === true ? labelText(seg.name, seg.id) : String(seg.id);
2983
+ drawLabel(png, p.x, p.y, text, pal.label, Math.max(1, scale));
2985
2984
  }
2986
2985
  }
2987
2986
  return PNG.sync.write(png);
2988
2987
  }
2989
2988
  function worldToPx(worldX, worldY, dim, scale) {
2989
+ const cellY = (worldY - dim.top) / dim.gridSize;
2990
2990
  return {
2991
2991
  x: Math.round((worldX - dim.left) / dim.gridSize * scale),
2992
- y: Math.round((worldY - dim.top) / dim.gridSize * scale)
2992
+ y: Math.round((dim.height - 1 - cellY) * scale)
2993
2993
  };
2994
2994
  }
2995
- function paintLayer(png, layer, scale, pal) {
2995
+ function paintLayer(png, layer, dimHeight, scale, pal) {
2996
2996
  const color = layer.type === "segment" ? segmentColor(layer.segmentId ?? 0, pal) : layer.type === "wall" ? pal.wall : layer.type === "carpet" ? pal.carpet : pal.floor;
2997
2997
  for (const run of layer.runs) {
2998
- paintRun(png, run, color, scale);
2998
+ paintRun(png, run, dimHeight, color, scale);
2999
2999
  }
3000
3000
  }
3001
- function paintRun(png, run, color, scale) {
3001
+ function paintRun(png, run, dimHeight, color, scale) {
3002
3002
  const [xPx, yPx, len] = run;
3003
+ const baseY = (dimHeight - 1 - yPx) * scale;
3003
3004
  for (let i = 0; i < len; i += 1) {
3004
3005
  const baseX = (xPx + i) * scale;
3005
- const baseY = yPx * scale;
3006
3006
  for (let dy = 0; dy < scale; dy += 1) {
3007
3007
  for (let dx = 0; dx < scale; dx += 1) {
3008
3008
  setPixel(png, baseX + dx, baseY + dy, color);
@@ -3108,7 +3108,7 @@ function drawRobot(png, robot, dim, scale, pal) {
3108
3108
  drawDisk(png, p.x, p.y, r, pal.robotBody);
3109
3109
  const rad = robot.angle * Math.PI / 180;
3110
3110
  const hx = Math.round(p.x + Math.cos(rad) * r);
3111
- const hy = Math.round(p.y + Math.sin(rad) * r);
3111
+ const hy = Math.round(p.y - Math.sin(rad) * r);
3112
3112
  drawLine(png, p.x, p.y, hx, hy, pal.robotHeading, Math.max(1, Math.trunc(scale / 2) + 1));
3113
3113
  }
3114
3114
  function drawCharger(png, dock, dim, scale, pal) {
@@ -3116,6 +3116,58 @@ function drawCharger(png, dock, dim, scale, pal) {
3116
3116
  const r = Math.max(2, scale * 2);
3117
3117
  fillRect(png, p.x - r, p.y - r, p.x + r, p.y + r, pal.charger);
3118
3118
  }
3119
+ function drawDiamond(png, cx, cy, r, color) {
3120
+ for (let dy = -r; dy <= r; dy += 1) {
3121
+ const w = r - Math.abs(dy);
3122
+ for (let dx = -w; dx <= w; dx += 1) {
3123
+ setPixel(png, cx + dx, cy + dy, color);
3124
+ }
3125
+ }
3126
+ }
3127
+ function obstacleColor(type) {
3128
+ const hue = Math.abs(Math.trunc(type)) * 47 % 360;
3129
+ return hsvToRgba(hue, 0.85, 0.95);
3130
+ }
3131
+ var FURNITURE_LINE = [150, 110, 200, 235];
3132
+ function paintFurnitureZone(png, points, dim, scale) {
3133
+ if (points.length < 2) return;
3134
+ for (let i = 0; i < points.length; i += 1) {
3135
+ const a = points[i];
3136
+ const b = points[(i + 1) % points.length];
3137
+ if (a === void 0 || b === void 0) continue;
3138
+ const pa = worldToPx(a.x, a.y, dim, scale);
3139
+ const pb = worldToPx(b.x, b.y, dim, scale);
3140
+ drawLine(png, pa.x, pa.y, pb.x, pb.y, FURNITURE_LINE, Math.max(1, scale));
3141
+ }
3142
+ }
3143
+ function paintCleanedArea(png, overlay, parentDim, scale, baseColor) {
3144
+ const tint = [baseColor[0], baseColor[1], baseColor[2], 55];
3145
+ const inner = overlay.dimensions;
3146
+ const block = Math.max(1, Math.round(inner.gridSize / parentDim.gridSize * scale));
3147
+ for (const [xPx, yPx, len] of overlay.cleaned) {
3148
+ for (let i = 0; i < len; i += 1) {
3149
+ const worldX = inner.left + (xPx + i) * inner.gridSize;
3150
+ const worldY = inner.top + yPx * inner.gridSize;
3151
+ const p = worldToPx(worldX, worldY, parentDim, scale);
3152
+ for (let dy = 0; dy < block; dy += 1) {
3153
+ for (let dx = 0; dx < block; dx += 1) {
3154
+ setPixel(png, p.x + dx, p.y + dy, tint);
3155
+ }
3156
+ }
3157
+ }
3158
+ }
3159
+ }
3160
+ function labelText(name, id) {
3161
+ const raw = (name ?? "").trim();
3162
+ const base = raw.length > 0 ? raw : `ROOM ${id}`;
3163
+ const upper = base.toUpperCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
3164
+ let out = "";
3165
+ for (const ch of upper) {
3166
+ if (GLYPHS[ch] !== void 0) out += ch;
3167
+ }
3168
+ const trimmed = out.trim().slice(0, 14);
3169
+ return trimmed.length > 0 ? trimmed : `ROOM ${id}`;
3170
+ }
3119
3171
  var GLYPHS = {
3120
3172
  "0": [7, 5, 5, 5, 7],
3121
3173
  "1": [2, 6, 2, 2, 7],
@@ -3127,7 +3179,34 @@ var GLYPHS = {
3127
3179
  "7": [7, 1, 2, 2, 2],
3128
3180
  "8": [7, 5, 7, 5, 7],
3129
3181
  "9": [7, 5, 7, 1, 7],
3130
- "-": [0, 0, 7, 0, 0]
3182
+ "-": [0, 0, 7, 0, 0],
3183
+ " ": [0, 0, 0, 0, 0],
3184
+ A: [2, 5, 7, 5, 5],
3185
+ B: [6, 5, 6, 5, 6],
3186
+ C: [3, 4, 4, 4, 3],
3187
+ D: [6, 5, 5, 5, 6],
3188
+ E: [7, 4, 6, 4, 7],
3189
+ F: [7, 4, 6, 4, 4],
3190
+ G: [3, 4, 5, 5, 3],
3191
+ H: [5, 5, 7, 5, 5],
3192
+ I: [7, 2, 2, 2, 7],
3193
+ J: [1, 1, 1, 5, 2],
3194
+ K: [5, 5, 6, 5, 5],
3195
+ L: [4, 4, 4, 4, 7],
3196
+ M: [5, 7, 7, 5, 5],
3197
+ N: [5, 7, 7, 7, 5],
3198
+ O: [2, 5, 5, 5, 2],
3199
+ P: [6, 5, 6, 4, 4],
3200
+ Q: [2, 5, 5, 6, 3],
3201
+ R: [6, 5, 6, 5, 5],
3202
+ S: [3, 4, 2, 1, 6],
3203
+ T: [7, 2, 2, 2, 2],
3204
+ U: [5, 5, 5, 5, 7],
3205
+ V: [5, 5, 5, 5, 2],
3206
+ W: [5, 5, 7, 7, 5],
3207
+ X: [5, 5, 2, 5, 5],
3208
+ Y: [5, 5, 2, 2, 2],
3209
+ Z: [7, 1, 2, 4, 7]
3131
3210
  };
3132
3211
  function drawLabel(png, cx, cy, text, color, px) {
3133
3212
  const glyphW = 3 * px;