@fleet-frontend/mower-maps 0.0.9-beta.10 → 0.0.9-beta.11

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.
@@ -27,6 +27,7 @@ export declare const DEFAULT_OPACITIES: {
27
27
  readonly FULL: 1;
28
28
  readonly HIGH: 0.7;
29
29
  readonly MEDIUM: 0.6;
30
+ readonly DOODLE: 0.8;
30
31
  readonly LOW: 0.4;
31
32
  readonly VERY_LOW: 0.2;
32
33
  };
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/config/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAUtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;CAMpB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;CAMhB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;CAUf,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;CAWrB,CAAC;AAGX,OAAO,EACL,WAAW,EACX,SAAS,EACT,mBAAmB,EACpB,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,qBAAqB,sqUAqB3B,CAAC;AAER;;GAEG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B;;GAEG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC;;GAEG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC;;GAEG;AACH,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAC9C;;GAEG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC;;GAEG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC;;GAEG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC;;GAEG;AACH,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAChD;;GAEG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC;;GAEG;AACH,eAAO,MAAM,UAAU,KAAK,CAAC;AAE7B,eAAO,MAAM,eAAe,4BAA4B,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/config/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;CAUtB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;CAOpB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;CAMhB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;CAUf,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;CAWrB,CAAC;AAGX,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEjF,eAAO,MAAM,qBAAqB,sqUAqB3B,CAAC;AAER;;GAEG;AACH,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B;;GAEG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC;;GAEG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC;;GAEG;AACH,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAC9C;;GAEG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC;;GAEG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC;;GAEG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC;;GAEG;AACH,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAChD;;GAEG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC;;GAEG;AACH,eAAO,MAAM,UAAU,KAAK,CAAC;AAE7B,eAAO,MAAM,eAAe,4BAA4B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/config/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,SAAS,EACV,MAAM,UAAU,CAAC;AAElB;;GAEG;AACH,eAAO,MAAM,eAAe,EAMvB,aAAa,CAAC;AAEnB,eAAO,MAAM,sBAAsB,EAK9B,aAAa,CAAC;AAEnB,eAAO,MAAM,eAAe,EAKvB,aAAa,CAAC;AAEnB,eAAO,MAAM,oBAAoB,EAM5B,iBAAiB,CAAC;AAEvB,eAAO,MAAM,aAAa,EAKrB,eAAe,CAAC;AAErB,eAAO,MAAM,gBAAgB;;;;;;;CAO5B,CAAC;AAEF,eAAO,MAAM,cAAc;;;;;;CAM1B,CAAC;AAEF,eAAO,MAAM,cAAc,EAKtB,YAAY,CAAC;AAElB,eAAO,MAAM,cAAc,EAAE,SAS5B,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/config/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEpG;;GAEG;AACH,eAAO,MAAM,eAAe,EAMvB,aAAa,CAAC;AAEnB,eAAO,MAAM,sBAAsB,EAK9B,aAAa,CAAC;AAEnB,eAAO,MAAM,eAAe,EAKvB,aAAa,CAAC;AAEnB,eAAO,MAAM,oBAAoB,EAM5B,iBAAiB,CAAC;AAEvB,eAAO,MAAM,aAAa,EAKrB,eAAe,CAAC;AAErB,eAAO,MAAM,gBAAgB;;;;;;;CAO5B,CAAC;AAEF,eAAO,MAAM,cAAc;;;;;;CAM1B,CAAC;AAEF,eAAO,MAAM,cAAc,EAKtB,YAAY,CAAC;AAElB,eAAO,MAAM,cAAc,EAAE,SAS5B,CAAC"}
package/dist/index.esm.js CHANGED
@@ -67,7 +67,8 @@ const DEFAULT_LINE_WIDTHS = {
67
67
  const DEFAULT_OPACITIES = {
68
68
  FULL: 1.0,
69
69
  HIGH: 0.7,
70
- MEDIUM: 0.6};
70
+ MEDIUM: 0.6,
71
+ DOODLE: 0.8};
71
72
  /**
72
73
  * 默认半径设置
73
74
  */
@@ -1044,15 +1045,37 @@ class PathLayer extends BaseLayer {
1044
1045
  d += ' Z ';
1045
1046
  }
1046
1047
  });
1047
- // 3. svgElements(直接拼接path字符串,建议逆时针)
1048
- if (Array.isArray(svgElements)) {
1049
- svgElements.forEach((svgPath) => {
1050
- const svgPathString = svgPath?.metadata?.svg;
1051
- if (svgPathString && typeof svgPathString === 'string' && svgPathString.trim()) {
1052
- d += svgPathString + ' ';
1048
+ // 3. svgElements(解析 SVG 字符串并提取 path 数据)
1049
+ Object.values(svgElements).forEach((svgPath) => {
1050
+ const svgPathString = svgPath?.metadata?.svg;
1051
+ if (svgPathString && typeof svgPathString === 'string' && svgPathString.trim()) {
1052
+ // 处理转义字符
1053
+ const processedSvgString = svgPathString.replace(/\\n/g, '\n').replace(/\\"/g, '"');
1054
+ // 解析 SVG 字符串
1055
+ const parser = new DOMParser();
1056
+ const svgDoc = parser.parseFromString(processedSvgString, 'image/svg+xml');
1057
+ const svgElement = svgDoc.documentElement;
1058
+ if (svgElement.tagName === 'svg') {
1059
+ // 查找 path 元素
1060
+ const pathElement = svgElement.querySelector('path');
1061
+ if (pathElement) {
1062
+ const pathData = pathElement.getAttribute('d');
1063
+ if (pathData) {
1064
+ // 获取 SVG 元素的变换参数
1065
+ const centerCoords = svgPath.coordinates?.[0] || [0, 0];
1066
+ const center = [centerCoords[0], centerCoords[1]];
1067
+ const userScale = svgPath.metadata.scale || 1;
1068
+ const direction = svgPath.metadata?.direction || 0;
1069
+ const originalWidth = parseFloat(svgElement.getAttribute('width') || '76');
1070
+ const originalHeight = parseFloat(svgElement.getAttribute('height') || '68');
1071
+ // 应用变换到路径数据
1072
+ const transformedPathData = this.transformSvgPath(pathData, center, userScale, direction, originalWidth, originalHeight);
1073
+ d += transformedPathData + ' ';
1074
+ }
1075
+ }
1053
1076
  }
1054
- });
1055
- }
1077
+ }
1078
+ });
1056
1079
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1057
1080
  path.setAttribute('d', d);
1058
1081
  const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
@@ -1077,47 +1100,132 @@ class PathLayer extends BaseLayer {
1077
1100
  // 2. 创建一个组,应用 clipPath
1078
1101
  const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
1079
1102
  group.setAttribute('clip-path', `url(#${clipPathId})`);
1080
- group.setAttribute('opacity', '0.6'); // 统一透明度,防止叠加脏乱
1081
- // 3. 渲染所有路径
1103
+ group.setAttribute('opacity', '0.5'); // 统一透明度,防止叠加脏乱
1104
+ // 3. 优化渲染:按样式分组并合并路径
1105
+ this.renderOptimizedPaths(group);
1106
+ svgGroup.appendChild(group);
1107
+ }
1108
+ /**
1109
+ * 优化渲染:按样式分组并合并路径,减少 DOM 节点数量
1110
+ */
1111
+ renderOptimizedPaths(group) {
1112
+ // 按样式分组存储路径数据
1113
+ const styleGroups = new Map();
1114
+ // 收集所有路径数据并按样式分组
1082
1115
  for (const element of this.elements) {
1083
- const { id, elements } = element;
1116
+ // 类型断言:PathLayer 中的 elements 实际上是 PathElements 结构
1117
+ const pathElement = element;
1118
+ const { id, elements } = pathElement;
1084
1119
  this.boundaryPaths[id] = [];
1120
+ elements.forEach((pathElement) => {
1121
+ const { coordinates, style } = pathElement;
1122
+ if (coordinates.length < 2)
1123
+ return;
1124
+ // 生成样式键(用于分组)
1125
+ const styleKey = this.generateStyleKey(style);
1126
+ // 构建路径数据
1127
+ let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
1128
+ for (let i = 1; i < coordinates.length; i++) {
1129
+ pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
1130
+ }
1131
+ // 按样式分组存储
1132
+ if (!styleGroups.has(styleKey)) {
1133
+ styleGroups.set(styleKey, { pathData: [], elements: [] });
1134
+ }
1135
+ styleGroups.get(styleKey).pathData.push(pathData);
1136
+ styleGroups.get(styleKey).elements.push(pathElement);
1137
+ });
1138
+ }
1139
+ // 为每种样式创建一个合并的 path 元素
1140
+ styleGroups.forEach((groupData) => {
1141
+ const { pathData, elements } = groupData;
1142
+ if (pathData.length === 0)
1143
+ return;
1144
+ // 使用第一个元素的样式作为该组的样式
1145
+ const firstElement = elements[0];
1146
+ const style = firstElement.style;
1147
+ // 创建合并的 path 元素
1148
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1149
+ // 合并所有路径数据
1150
+ const mergedPathData = pathData.join(' ');
1151
+ path.setAttribute('d', mergedPathData);
1152
+ // 设置样式属性
1153
+ path.setAttribute('fill', 'none');
1154
+ path.setAttribute('stroke', style.lineColor || '#000000');
1155
+ path.setAttribute('mix-blend-mode', 'normal');
1156
+ const lineWidth = Math.max(style.lineWidth || 1, 0.5);
1157
+ path.setAttribute('stroke-width', lineWidth.toString());
1158
+ path.setAttribute('stroke-linecap', 'round');
1159
+ path.setAttribute('stroke-linejoin', 'round');
1160
+ path.classList.add('vector-path');
1161
+ // 将合并的 path 添加到组中
1162
+ group.appendChild(path);
1163
+ // 保存引用到 boundaryPaths 中(保持兼容性)
1085
1164
  elements.forEach((element) => {
1086
- this.renderPathToGroup(group, id, element);
1165
+ const { id } = element;
1166
+ if (!this.boundaryPaths[id]) {
1167
+ this.boundaryPaths[id] = [];
1168
+ }
1169
+ this.boundaryPaths[id].push(path);
1087
1170
  });
1171
+ });
1172
+ }
1173
+ /**
1174
+ * 变换 SVG 路径数据
1175
+ */
1176
+ transformSvgPath(pathData, center, scale, direction, originalWidth, originalHeight) {
1177
+ // 解析路径数据并应用变换
1178
+ const commands = pathData.match(/[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*/g) || [];
1179
+ let transformedCommands = [];
1180
+ for (const command of commands) {
1181
+ const type = command[0];
1182
+ const params = command
1183
+ .slice(1)
1184
+ .trim()
1185
+ .split(/[\s,]+/)
1186
+ .filter(Boolean)
1187
+ .map(Number);
1188
+ if (type === 'Z' || type === 'z') {
1189
+ // 闭合路径,不需要变换
1190
+ transformedCommands.push(command);
1191
+ continue;
1192
+ }
1193
+ // 处理坐标参数
1194
+ let transformedParams = [];
1195
+ for (let i = 0; i < params.length; i += 2) {
1196
+ if (i + 1 < params.length) {
1197
+ let x = params[i];
1198
+ let y = params[i + 1];
1199
+ // 应用变换:先平移到中心,然后缩放、旋转,最后平移到目标位置
1200
+ // 1. 平移到原点(相对于原始尺寸的中心)
1201
+ x -= originalWidth / 2;
1202
+ y -= originalHeight / 2;
1203
+ // 2. 应用缩放
1204
+ x *= scale;
1205
+ y *= scale;
1206
+ // 3. 应用旋转
1207
+ const cos = Math.cos(-direction);
1208
+ const sin = Math.sin(-direction);
1209
+ const newX = x * cos - y * sin;
1210
+ const newY = x * sin + y * cos;
1211
+ // 4. 平移到目标位置
1212
+ x = newX + center[0];
1213
+ y = newY + center[1];
1214
+ transformedParams.push(x, y);
1215
+ }
1216
+ }
1217
+ // 重建命令
1218
+ if (transformedParams.length > 0) {
1219
+ transformedCommands.push(type + transformedParams.join(' '));
1220
+ }
1088
1221
  }
1089
- svgGroup.appendChild(group);
1222
+ return transformedCommands.join(' ');
1090
1223
  }
1091
1224
  /**
1092
- * 渲染单个路径到指定的组中
1225
+ * 生成样式键,用于路径分组
1093
1226
  */
1094
- renderPathToGroup(group, id, element) {
1095
- const { coordinates, style } = element;
1096
- if (coordinates.length < 2)
1097
- return;
1098
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1099
- // 构建路径数据
1100
- let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
1101
- for (let i = 1; i < coordinates.length; i++) {
1102
- pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
1103
- }
1104
- path.style.mixBlendMode = 'normal';
1105
- // 设置路径属性
1106
- path.setAttribute('d', pathData);
1107
- // 直接给fill的颜色设置透明度会导致path重叠的部分颜色叠加,所以使用fill填充实色,通过fill-opacity设置透明度
1108
- path.setAttribute('fill', 'none');
1109
- // path.setAttribute('fill-opacity', '0.4');
1110
- path.setAttribute('stroke', style.lineColor || '#000000');
1111
- path.setAttribute('mix-blend-mode', 'normal');
1112
- const lineWidth = Math.max(style.lineWidth || 1, 0.5);
1113
- path.setAttribute('stroke-width', lineWidth.toString());
1114
- path.setAttribute('stroke-linecap', 'round');
1115
- path.setAttribute('stroke-linejoin', 'round');
1116
- // 注意:这里不设置 opacity,因为透明度由父组控制
1117
- // path.setAttribute('vector-effect', 'non-scaling-stroke');
1118
- path.classList.add('vector-path');
1119
- this.boundaryPaths[id].push(path);
1120
- group.appendChild(path);
1227
+ generateStyleKey(style) {
1228
+ return `${style.lineColor || '#000000'}-${style.lineWidth || 1}-${style.opacity || 1}`;
1121
1229
  }
1122
1230
  }
1123
1231
 
@@ -1541,7 +1649,7 @@ const DOODLE_STYLES = {
1541
1649
  lineColor: '#ff5722',
1542
1650
  fillColor: '#ff9800', // 粉色半透明填充
1543
1651
  lineWidth: DEFAULT_LINE_WIDTHS.TIME_LIMIT_OBSTACLE,
1544
- opacity: DEFAULT_OPACITIES.HIGH,
1652
+ opacity: DEFAULT_OPACITIES.DOODLE,
1545
1653
  };
1546
1654
  const PATH_EDGE_STYLES = {
1547
1655
  lineWidth: DEFAULT_LINE_WIDTHS.PATH,
@@ -8902,7 +9010,7 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
8902
9010
  // const mapRef = useMap();
8903
9011
  const [isGoogleMapsReady, setIsGoogleMapsReady] = useState(false);
8904
9012
  const [hasInitializedBounds, setHasInitializedBounds] = useState(false);
8905
- const { clearSubBoundaryBorder, clearObstacles } = useSubBoundaryBorderStore();
9013
+ const { clearSubBoundaryBorder, clearObstacles, clearSvgElements } = useSubBoundaryBorderStore();
8906
9014
  const currentProcessMowingStatusRef = useRef(false);
8907
9015
  const { updateProcessStateIsMowing, processStateIsMowing } = useProcessMowingState();
8908
9016
  const [mowPartitionData, setMowPartitionData] = useState(null);
@@ -9071,6 +9179,7 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
9071
9179
  return () => {
9072
9180
  clearSubBoundaryBorder();
9073
9181
  clearObstacles();
9182
+ clearSvgElements();
9074
9183
  updateProcessStateIsMowing(false);
9075
9184
  currentProcessMowingStatusRef.current = false;
9076
9185
  if (overlayRef.current) {
@@ -9261,8 +9370,8 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
9261
9370
  setMowPartitionData(mowingPartition);
9262
9371
  curMowPartitionData = mowingPartition;
9263
9372
  }
9264
- const positionData = realTimeData?.find(item => item?.type === RealTimeDataType.LOCATION);
9265
- const statusData = realTimeData?.find(item => item?.type === RealTimeDataType.STATUS);
9373
+ const positionData = realTimeData?.find((item) => item?.type === RealTimeDataType.LOCATION);
9374
+ const statusData = realTimeData?.find((item) => item?.type === RealTimeDataType.STATUS);
9266
9375
  if (statusData || positionData) {
9267
9376
  const currentStatus = statusData?.vehicleState || positionData?.vehicleState;
9268
9377
  // 车辆回桩不会回传最后的park的位置,所以根据实时数据的状态数据判断车辆回到桩上
@@ -9275,20 +9384,20 @@ const MowerMapRenderer = forwardRef(({ unitType = UnitsType.Imperial, language =
9275
9384
  setMowPartitionData(null);
9276
9385
  curMowPartitionData = null;
9277
9386
  }
9278
- else if (currentStatus === RobotStatus.MOWING && (curMowPartitionData && !curMowPartitionData?.partitionIds)) {
9387
+ else if (currentStatus === RobotStatus.MOWING &&
9388
+ curMowPartitionData &&
9389
+ !curMowPartitionData?.partitionIds) {
9279
9390
  // 如果当前是割草状态,但是地块数据初始化过且不存在则认为是全局割草,则把所有地块都高亮
9280
- const allPartitionIds = mapJson?.sub_maps?.map(item => item?.id);
9391
+ const allPartitionIds = mapJson?.sub_maps?.map((item) => item?.id);
9281
9392
  setMowPartitionData({
9282
- partitionIds: allPartitionIds
9393
+ partitionIds: allPartitionIds,
9283
9394
  });
9284
9395
  curMowPartitionData = {
9285
- partitionIds: allPartitionIds
9396
+ partitionIds: allPartitionIds,
9286
9397
  };
9287
9398
  }
9288
9399
  }
9289
- if (!mapJson ||
9290
- !pathJson ||
9291
- !overlayRef.current)
9400
+ if (!mapJson || !pathJson || !overlayRef.current)
9292
9401
  return;
9293
9402
  // 根据后端推送的实时数据,进行不同处理
9294
9403
  if (curMowPartitionData) {
package/dist/index.js CHANGED
@@ -69,7 +69,8 @@ const DEFAULT_LINE_WIDTHS = {
69
69
  const DEFAULT_OPACITIES = {
70
70
  FULL: 1.0,
71
71
  HIGH: 0.7,
72
- MEDIUM: 0.6};
72
+ MEDIUM: 0.6,
73
+ DOODLE: 0.8};
73
74
  /**
74
75
  * 默认半径设置
75
76
  */
@@ -1046,15 +1047,37 @@ class PathLayer extends BaseLayer {
1046
1047
  d += ' Z ';
1047
1048
  }
1048
1049
  });
1049
- // 3. svgElements(直接拼接path字符串,建议逆时针)
1050
- if (Array.isArray(svgElements)) {
1051
- svgElements.forEach((svgPath) => {
1052
- const svgPathString = svgPath?.metadata?.svg;
1053
- if (svgPathString && typeof svgPathString === 'string' && svgPathString.trim()) {
1054
- d += svgPathString + ' ';
1050
+ // 3. svgElements(解析 SVG 字符串并提取 path 数据)
1051
+ Object.values(svgElements).forEach((svgPath) => {
1052
+ const svgPathString = svgPath?.metadata?.svg;
1053
+ if (svgPathString && typeof svgPathString === 'string' && svgPathString.trim()) {
1054
+ // 处理转义字符
1055
+ const processedSvgString = svgPathString.replace(/\\n/g, '\n').replace(/\\"/g, '"');
1056
+ // 解析 SVG 字符串
1057
+ const parser = new DOMParser();
1058
+ const svgDoc = parser.parseFromString(processedSvgString, 'image/svg+xml');
1059
+ const svgElement = svgDoc.documentElement;
1060
+ if (svgElement.tagName === 'svg') {
1061
+ // 查找 path 元素
1062
+ const pathElement = svgElement.querySelector('path');
1063
+ if (pathElement) {
1064
+ const pathData = pathElement.getAttribute('d');
1065
+ if (pathData) {
1066
+ // 获取 SVG 元素的变换参数
1067
+ const centerCoords = svgPath.coordinates?.[0] || [0, 0];
1068
+ const center = [centerCoords[0], centerCoords[1]];
1069
+ const userScale = svgPath.metadata.scale || 1;
1070
+ const direction = svgPath.metadata?.direction || 0;
1071
+ const originalWidth = parseFloat(svgElement.getAttribute('width') || '76');
1072
+ const originalHeight = parseFloat(svgElement.getAttribute('height') || '68');
1073
+ // 应用变换到路径数据
1074
+ const transformedPathData = this.transformSvgPath(pathData, center, userScale, direction, originalWidth, originalHeight);
1075
+ d += transformedPathData + ' ';
1076
+ }
1077
+ }
1055
1078
  }
1056
- });
1057
- }
1079
+ }
1080
+ });
1058
1081
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1059
1082
  path.setAttribute('d', d);
1060
1083
  const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
@@ -1079,47 +1102,132 @@ class PathLayer extends BaseLayer {
1079
1102
  // 2. 创建一个组,应用 clipPath
1080
1103
  const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
1081
1104
  group.setAttribute('clip-path', `url(#${clipPathId})`);
1082
- group.setAttribute('opacity', '0.6'); // 统一透明度,防止叠加脏乱
1083
- // 3. 渲染所有路径
1105
+ group.setAttribute('opacity', '0.5'); // 统一透明度,防止叠加脏乱
1106
+ // 3. 优化渲染:按样式分组并合并路径
1107
+ this.renderOptimizedPaths(group);
1108
+ svgGroup.appendChild(group);
1109
+ }
1110
+ /**
1111
+ * 优化渲染:按样式分组并合并路径,减少 DOM 节点数量
1112
+ */
1113
+ renderOptimizedPaths(group) {
1114
+ // 按样式分组存储路径数据
1115
+ const styleGroups = new Map();
1116
+ // 收集所有路径数据并按样式分组
1084
1117
  for (const element of this.elements) {
1085
- const { id, elements } = element;
1118
+ // 类型断言:PathLayer 中的 elements 实际上是 PathElements 结构
1119
+ const pathElement = element;
1120
+ const { id, elements } = pathElement;
1086
1121
  this.boundaryPaths[id] = [];
1122
+ elements.forEach((pathElement) => {
1123
+ const { coordinates, style } = pathElement;
1124
+ if (coordinates.length < 2)
1125
+ return;
1126
+ // 生成样式键(用于分组)
1127
+ const styleKey = this.generateStyleKey(style);
1128
+ // 构建路径数据
1129
+ let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
1130
+ for (let i = 1; i < coordinates.length; i++) {
1131
+ pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
1132
+ }
1133
+ // 按样式分组存储
1134
+ if (!styleGroups.has(styleKey)) {
1135
+ styleGroups.set(styleKey, { pathData: [], elements: [] });
1136
+ }
1137
+ styleGroups.get(styleKey).pathData.push(pathData);
1138
+ styleGroups.get(styleKey).elements.push(pathElement);
1139
+ });
1140
+ }
1141
+ // 为每种样式创建一个合并的 path 元素
1142
+ styleGroups.forEach((groupData) => {
1143
+ const { pathData, elements } = groupData;
1144
+ if (pathData.length === 0)
1145
+ return;
1146
+ // 使用第一个元素的样式作为该组的样式
1147
+ const firstElement = elements[0];
1148
+ const style = firstElement.style;
1149
+ // 创建合并的 path 元素
1150
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1151
+ // 合并所有路径数据
1152
+ const mergedPathData = pathData.join(' ');
1153
+ path.setAttribute('d', mergedPathData);
1154
+ // 设置样式属性
1155
+ path.setAttribute('fill', 'none');
1156
+ path.setAttribute('stroke', style.lineColor || '#000000');
1157
+ path.setAttribute('mix-blend-mode', 'normal');
1158
+ const lineWidth = Math.max(style.lineWidth || 1, 0.5);
1159
+ path.setAttribute('stroke-width', lineWidth.toString());
1160
+ path.setAttribute('stroke-linecap', 'round');
1161
+ path.setAttribute('stroke-linejoin', 'round');
1162
+ path.classList.add('vector-path');
1163
+ // 将合并的 path 添加到组中
1164
+ group.appendChild(path);
1165
+ // 保存引用到 boundaryPaths 中(保持兼容性)
1087
1166
  elements.forEach((element) => {
1088
- this.renderPathToGroup(group, id, element);
1167
+ const { id } = element;
1168
+ if (!this.boundaryPaths[id]) {
1169
+ this.boundaryPaths[id] = [];
1170
+ }
1171
+ this.boundaryPaths[id].push(path);
1089
1172
  });
1173
+ });
1174
+ }
1175
+ /**
1176
+ * 变换 SVG 路径数据
1177
+ */
1178
+ transformSvgPath(pathData, center, scale, direction, originalWidth, originalHeight) {
1179
+ // 解析路径数据并应用变换
1180
+ const commands = pathData.match(/[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*/g) || [];
1181
+ let transformedCommands = [];
1182
+ for (const command of commands) {
1183
+ const type = command[0];
1184
+ const params = command
1185
+ .slice(1)
1186
+ .trim()
1187
+ .split(/[\s,]+/)
1188
+ .filter(Boolean)
1189
+ .map(Number);
1190
+ if (type === 'Z' || type === 'z') {
1191
+ // 闭合路径,不需要变换
1192
+ transformedCommands.push(command);
1193
+ continue;
1194
+ }
1195
+ // 处理坐标参数
1196
+ let transformedParams = [];
1197
+ for (let i = 0; i < params.length; i += 2) {
1198
+ if (i + 1 < params.length) {
1199
+ let x = params[i];
1200
+ let y = params[i + 1];
1201
+ // 应用变换:先平移到中心,然后缩放、旋转,最后平移到目标位置
1202
+ // 1. 平移到原点(相对于原始尺寸的中心)
1203
+ x -= originalWidth / 2;
1204
+ y -= originalHeight / 2;
1205
+ // 2. 应用缩放
1206
+ x *= scale;
1207
+ y *= scale;
1208
+ // 3. 应用旋转
1209
+ const cos = Math.cos(-direction);
1210
+ const sin = Math.sin(-direction);
1211
+ const newX = x * cos - y * sin;
1212
+ const newY = x * sin + y * cos;
1213
+ // 4. 平移到目标位置
1214
+ x = newX + center[0];
1215
+ y = newY + center[1];
1216
+ transformedParams.push(x, y);
1217
+ }
1218
+ }
1219
+ // 重建命令
1220
+ if (transformedParams.length > 0) {
1221
+ transformedCommands.push(type + transformedParams.join(' '));
1222
+ }
1090
1223
  }
1091
- svgGroup.appendChild(group);
1224
+ return transformedCommands.join(' ');
1092
1225
  }
1093
1226
  /**
1094
- * 渲染单个路径到指定的组中
1227
+ * 生成样式键,用于路径分组
1095
1228
  */
1096
- renderPathToGroup(group, id, element) {
1097
- const { coordinates, style } = element;
1098
- if (coordinates.length < 2)
1099
- return;
1100
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1101
- // 构建路径数据
1102
- let pathData = `M ${coordinates[0][0]} ${coordinates[0][1]}`;
1103
- for (let i = 1; i < coordinates.length; i++) {
1104
- pathData += ` L ${coordinates[i][0]} ${coordinates[i][1]}`;
1105
- }
1106
- path.style.mixBlendMode = 'normal';
1107
- // 设置路径属性
1108
- path.setAttribute('d', pathData);
1109
- // 直接给fill的颜色设置透明度会导致path重叠的部分颜色叠加,所以使用fill填充实色,通过fill-opacity设置透明度
1110
- path.setAttribute('fill', 'none');
1111
- // path.setAttribute('fill-opacity', '0.4');
1112
- path.setAttribute('stroke', style.lineColor || '#000000');
1113
- path.setAttribute('mix-blend-mode', 'normal');
1114
- const lineWidth = Math.max(style.lineWidth || 1, 0.5);
1115
- path.setAttribute('stroke-width', lineWidth.toString());
1116
- path.setAttribute('stroke-linecap', 'round');
1117
- path.setAttribute('stroke-linejoin', 'round');
1118
- // 注意:这里不设置 opacity,因为透明度由父组控制
1119
- // path.setAttribute('vector-effect', 'non-scaling-stroke');
1120
- path.classList.add('vector-path');
1121
- this.boundaryPaths[id].push(path);
1122
- group.appendChild(path);
1229
+ generateStyleKey(style) {
1230
+ return `${style.lineColor || '#000000'}-${style.lineWidth || 1}-${style.opacity || 1}`;
1123
1231
  }
1124
1232
  }
1125
1233
 
@@ -1543,7 +1651,7 @@ const DOODLE_STYLES = {
1543
1651
  lineColor: '#ff5722',
1544
1652
  fillColor: '#ff9800', // 粉色半透明填充
1545
1653
  lineWidth: DEFAULT_LINE_WIDTHS.TIME_LIMIT_OBSTACLE,
1546
- opacity: DEFAULT_OPACITIES.HIGH,
1654
+ opacity: DEFAULT_OPACITIES.DOODLE,
1547
1655
  };
1548
1656
  const PATH_EDGE_STYLES = {
1549
1657
  lineWidth: DEFAULT_LINE_WIDTHS.PATH,
@@ -8904,7 +9012,7 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
8904
9012
  // const mapRef = useMap();
8905
9013
  const [isGoogleMapsReady, setIsGoogleMapsReady] = React.useState(false);
8906
9014
  const [hasInitializedBounds, setHasInitializedBounds] = React.useState(false);
8907
- const { clearSubBoundaryBorder, clearObstacles } = useSubBoundaryBorderStore();
9015
+ const { clearSubBoundaryBorder, clearObstacles, clearSvgElements } = useSubBoundaryBorderStore();
8908
9016
  const currentProcessMowingStatusRef = React.useRef(false);
8909
9017
  const { updateProcessStateIsMowing, processStateIsMowing } = useProcessMowingState();
8910
9018
  const [mowPartitionData, setMowPartitionData] = React.useState(null);
@@ -9073,6 +9181,7 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
9073
9181
  return () => {
9074
9182
  clearSubBoundaryBorder();
9075
9183
  clearObstacles();
9184
+ clearSvgElements();
9076
9185
  updateProcessStateIsMowing(false);
9077
9186
  currentProcessMowingStatusRef.current = false;
9078
9187
  if (overlayRef.current) {
@@ -9263,8 +9372,8 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
9263
9372
  setMowPartitionData(mowingPartition);
9264
9373
  curMowPartitionData = mowingPartition;
9265
9374
  }
9266
- const positionData = realTimeData?.find(item => item?.type === RealTimeDataType.LOCATION);
9267
- const statusData = realTimeData?.find(item => item?.type === RealTimeDataType.STATUS);
9375
+ const positionData = realTimeData?.find((item) => item?.type === RealTimeDataType.LOCATION);
9376
+ const statusData = realTimeData?.find((item) => item?.type === RealTimeDataType.STATUS);
9268
9377
  if (statusData || positionData) {
9269
9378
  const currentStatus = statusData?.vehicleState || positionData?.vehicleState;
9270
9379
  // 车辆回桩不会回传最后的park的位置,所以根据实时数据的状态数据判断车辆回到桩上
@@ -9277,20 +9386,20 @@ const MowerMapRenderer = React.forwardRef(({ unitType = UnitsType.Imperial, lang
9277
9386
  setMowPartitionData(null);
9278
9387
  curMowPartitionData = null;
9279
9388
  }
9280
- else if (currentStatus === RobotStatus.MOWING && (curMowPartitionData && !curMowPartitionData?.partitionIds)) {
9389
+ else if (currentStatus === RobotStatus.MOWING &&
9390
+ curMowPartitionData &&
9391
+ !curMowPartitionData?.partitionIds) {
9281
9392
  // 如果当前是割草状态,但是地块数据初始化过且不存在则认为是全局割草,则把所有地块都高亮
9282
- const allPartitionIds = mapJson?.sub_maps?.map(item => item?.id);
9393
+ const allPartitionIds = mapJson?.sub_maps?.map((item) => item?.id);
9283
9394
  setMowPartitionData({
9284
- partitionIds: allPartitionIds
9395
+ partitionIds: allPartitionIds,
9285
9396
  });
9286
9397
  curMowPartitionData = {
9287
- partitionIds: allPartitionIds
9398
+ partitionIds: allPartitionIds,
9288
9399
  };
9289
9400
  }
9290
9401
  }
9291
- if (!mapJson ||
9292
- !pathJson ||
9293
- !overlayRef.current)
9402
+ if (!mapJson || !pathJson || !overlayRef.current)
9294
9403
  return;
9295
9404
  // 根据后端推送的实时数据,进行不同处理
9296
9405
  if (curMowPartitionData) {
@@ -1 +1 @@
1
- {"version":3,"file":"MowerMapRenderer.d.ts","sourceRoot":"","sources":["../../src/render/MowerMapRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AA6Cf,OAAO,EAAa,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAO1F,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,MAAM,EAAE,GAAG,CAAC;KACb;CACF;AAgGD,eAAO,MAAM,gBAAgB,mGA8sB5B,CAAC;AAIF,eAAe,gBAAgB,CAAC;AAChC,YAAY,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,CAAC"}
1
+ {"version":3,"file":"MowerMapRenderer.d.ts","sourceRoot":"","sources":["../../src/render/MowerMapRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AA6Cf,OAAO,EAAa,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAO1F,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,MAAM,EAAE,GAAG,CAAC;KACb;CACF;AAgGD,eAAO,MAAM,gBAAgB,mGA+sB5B,CAAC;AAIF,eAAe,gBAAgB,CAAC;AAChC,YAAY,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,CAAC"}
@@ -18,8 +18,16 @@ export declare class PathLayer extends BaseLayer {
18
18
  */
19
19
  drawSVG(svgGroup: SVGGElement, scale: number, lineScale: number): void;
20
20
  /**
21
- * 渲染单个路径到指定的组中
21
+ * 优化渲染:按样式分组并合并路径,减少 DOM 节点数量
22
22
  */
23
- private renderPathToGroup;
23
+ private renderOptimizedPaths;
24
+ /**
25
+ * 变换 SVG 路径数据
26
+ */
27
+ private transformSvgPath;
28
+ /**
29
+ * 生成样式键,用于路径分组
30
+ */
31
+ private generateStyleKey;
24
32
  }
25
33
  //# sourceMappingURL=PathLayer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PathLayer.d.ts","sourceRoot":"","sources":["../../../src/render/layers/PathLayer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC;;;GAGG;AACH,qBAAa,SAAU,SAAQ,SAAS;IACtC,KAAK,EAAE,MAAM,CAAK;IAClB,KAAK,EAAE,MAAM,CAAK;IAClB,SAAS,EAAE,MAAM,CAAK;IACtB,aAAa,EAAE,GAAG,CAAM;;IAOxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAiE3B;;OAEG;IACI,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IA4B7E;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAiC1B"}
1
+ {"version":3,"file":"PathLayer.d.ts","sourceRoot":"","sources":["../../../src/render/layers/PathLayer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;GAGG;AACH,qBAAa,SAAU,SAAQ,SAAS;IACtC,KAAK,EAAE,MAAM,CAAK;IAClB,KAAK,EAAE,MAAM,CAAK;IAClB,SAAS,EAAE,MAAM,CAAK;IACtB,aAAa,EAAE,GAAG,CAAM;;IAOxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAmG3B;;OAEG;IACI,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAuB7E;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA2E5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAkExB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAGzB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleet-frontend/mower-maps",
3
- "version": "0.0.9-beta.10",
3
+ "version": "0.0.9-beta.11",
4
4
  "type": "module",
5
5
  "description": "a mower maps in google maps",
6
6
  "main": "dist/index.js",