feat: 添加颜色配置面板和相关功能,优化场景信息和绘制逻辑

This commit is contained in:
xudan 2025-09-05 11:50:40 +08:00
parent 32a8e04635
commit 9bd97f7248
8 changed files with 1433 additions and 27 deletions

View File

@ -1,6 +1,8 @@
import type { RobotGroup, RobotInfo } from '@api/robot';
import type { Meta2dData } from '@meta2d/core';
import type { EditorColorConfig } from '../../services/color-config.service';
export interface SceneInfo {
id: string; // 场景id
label: string; // 场景名称
@ -30,6 +32,7 @@ export interface StandardScene {
width?: number; // 场景宽度
height?: number; // 场景高度
ratio?: number; // 坐标缩放比例
colorConfig?: EditorColorConfig; // 颜色配置
}
export interface StandardScenePoint {
id: string;

View File

@ -0,0 +1,388 @@
<template>
<div class="color-config-panel">
<h3>编辑器颜色配置</h3>
<!-- 点位颜色配置 -->
<div class="color-section">
<h4>小点位颜色</h4>
<div class="color-group">
<label>普通点填充</label>
<ColorPickerWithAlpha
:modelValue="config.point.small.fill[1]"
@update:modelValue="(value) => updateColor('point.small.fill.1', { target: { value } })"
/>
</div>
<div class="color-group">
<label>等待点填充</label>
<ColorPickerWithAlpha
:modelValue="config.point.small.fill[2]"
@update:modelValue="(value) => updateColor('point.small.fill.2', { target: { value } })"
/>
</div>
<div class="color-group">
<label>避让点填充</label>
<ColorPickerWithAlpha
:modelValue="config.point.small.fill[3]"
@update:modelValue="(value) => updateColor('point.small.fill.3', { target: { value } })"
/>
</div>
<div class="color-group">
<label>临时避让点填充</label>
<ColorPickerWithAlpha
:modelValue="config.point.small.fill[4]"
@update:modelValue="(value) => updateColor('point.small.fill.4', { target: { value } })"
/>
</div>
<div class="color-group">
<label>库区点填充</label>
<ColorPickerWithAlpha
:modelValue="config.point.small.fill[5]"
@update:modelValue="(value) => updateColor('point.small.fill.5', { target: { value } })"
/>
</div>
</div>
<!-- 路线颜色配置 -->
<div class="color-section">
<h4>路线颜色</h4>
<div class="color-group">
<label>可通行路线</label>
<ColorPickerWithAlpha
:modelValue="config.route.stroke[1]"
@update:modelValue="(value) => updateColor('route.stroke.1', { target: { value } })"
/>
</div>
<div class="color-group">
<label>双向路线</label>
<ColorPickerWithAlpha
:modelValue="config.route.stroke[2]"
@update:modelValue="(value) => updateColor('route.stroke.2', { target: { value } })"
/>
</div>
<div class="color-group">
<label>禁行路线</label>
<ColorPickerWithAlpha
:modelValue="config.route.stroke[10]"
@update:modelValue="(value) => updateColor('route.stroke.10', { target: { value } })"
/>
</div>
</div>
<!-- 区域颜色配置 -->
<div class="color-section">
<h4>区域颜色</h4>
<div class="color-group">
<label>库区填充</label>
<ColorPickerWithAlpha
:modelValue="config.area.fill[1]"
@update:modelValue="(value) => updateColor('area.fill.1', { target: { value } })"
/>
</div>
<div class="color-group">
<label>互斥区填充</label>
<ColorPickerWithAlpha
:modelValue="config.area.fill[11]"
@update:modelValue="(value) => updateColor('area.fill.11', { target: { value } })"
/>
</div>
<div class="color-group">
<label>非互斥区填充</label>
<ColorPickerWithAlpha
:modelValue="config.area.fill[12]"
@update:modelValue="(value) => updateColor('area.fill.12', { target: { value } })"
/>
</div>
<div class="color-group">
<label>约束区填充</label>
<ColorPickerWithAlpha
:modelValue="config.area.fill[13]"
@update:modelValue="(value) => updateColor('area.fill.13', { target: { value } })"
/>
</div>
<div class="color-group">
<label>描述区填充</label>
<ColorPickerWithAlpha
:modelValue="config.area.fill[14]"
@update:modelValue="(value) => updateColor('area.fill.14', { target: { value } })"
/>
</div>
</div>
<!-- 路线颜色配置 -->
<div class="color-section">
<h4>路线颜色</h4>
<div class="color-group">
<label>空载路线</label>
<ColorPickerWithAlpha
:modelValue="config.route.strokeEmpty"
@update:modelValue="(value) => updateColor('route.strokeEmpty', { target: { value } })"
/>
</div>
<div class="color-group">
<label>载货路线</label>
<ColorPickerWithAlpha
:modelValue="config.route.strokeLoaded"
@update:modelValue="(value) => updateColor('route.strokeLoaded', { target: { value } })"
/>
</div>
</div>
<!-- 机器人颜色配置 -->
<div class="color-section">
<h4>机器人颜色</h4>
<div class="color-group">
<label>正常状态填充</label>
<ColorPickerWithAlpha
:modelValue="config.robot.fillNormal"
@update:modelValue="(value) => updateColor('robot.fillNormal', { target: { value } })"
/>
</div>
<div class="color-group">
<label>告警状态填充</label>
<ColorPickerWithAlpha
:modelValue="config.robot.fillWarning"
@update:modelValue="(value) => updateColor('robot.fillWarning', { target: { value } })"
/>
</div>
<div class="color-group">
<label>故障状态填充</label>
<ColorPickerWithAlpha
:modelValue="config.robot.fillFault"
@update:modelValue="(value) => updateColor('robot.fillFault', { target: { value } })"
/>
</div>
<div class="color-group">
<label>路径线条</label>
<ColorPickerWithAlpha
:modelValue="config.robot.line"
@update:modelValue="(value) => updateColor('robot.line', { target: { value } })"
/>
</div>
</div>
<!-- 库位颜色配置 -->
<div class="color-section">
<h4>库位颜色</h4>
<div class="color-group">
<label>占用状态</label>
<ColorPickerWithAlpha
:modelValue="config.storage.occupied"
@update:modelValue="(value) => updateColor('storage.occupied', { target: { value } })"
/>
</div>
<div class="color-group">
<label>可用状态</label>
<ColorPickerWithAlpha
:modelValue="config.storage.available"
@update:modelValue="(value) => updateColor('storage.available', { target: { value } })"
/>
</div>
<div class="color-group">
<label>默认状态</label>
<ColorPickerWithAlpha
:modelValue="config.storage.default"
@update:modelValue="(value) => updateColor('storage.default', { target: { value } })"
/>
</div>
<div class="color-group">
<label>锁定状态</label>
<ColorPickerWithAlpha
:modelValue="config.storage.locked"
@update:modelValue="(value) => updateColor('storage.locked', { target: { value } })"
/>
</div>
<div class="color-group">
<label>更多按钮背景</label>
<ColorPickerWithAlpha
:modelValue="config.storage.moreButton.background"
@update:modelValue="(value) => updateColor('storage.moreButton.background', { target: { value } })"
/>
</div>
<div class="color-group">
<label>更多按钮边框</label>
<ColorPickerWithAlpha
:modelValue="config.storage.moreButton.border"
@update:modelValue="(value) => updateColor('storage.moreButton.border', { target: { value } })"
/>
</div>
<div class="color-group">
<label>更多按钮文字</label>
<ColorPickerWithAlpha
:modelValue="config.storage.moreButton.text"
@update:modelValue="(value) => updateColor('storage.moreButton.text', { target: { value } })"
/>
</div>
</div>
<div class="actions">
<button @click="resetToDefault" class="btn-reset">重置为默认</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, defineAsyncComponent } from 'vue';
import colorConfig from '../services/color-config.service';
//
const ColorPickerWithAlpha = defineAsyncComponent(() => import('./color-picker-with-alpha.vue'));
//
defineOptions({
name: 'ColorConfigPanel'
});
const config = computed(() => colorConfig.currentConfig);
const updateColor = (path: string, event: Event | { target: { value: string } }) => {
const target = event.target as HTMLInputElement;
if (target) {
colorConfig.setColor(path, target.value);
}
};
const resetToDefault = () => {
colorConfig.resetToDefault();
};
</script>
<style scoped>
.color-config-panel {
padding: 20px;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.color-config-panel h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 18px;
font-weight: 600;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 10px;
}
.color-section {
margin-bottom: 24px;
padding: 16px;
border: 1px solid #e8e8e8;
border-radius: 6px;
background: #fafafa;
}
.color-section h4 {
margin: 0 0 16px 0;
color: #555;
font-size: 16px;
font-weight: 600;
}
.color-subsection {
margin-bottom: 16px;
padding: 12px;
background: #fff;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.color-subsection h5 {
margin: 0 0 12px 0;
color: #666;
font-size: 14px;
font-weight: 500;
}
.color-group {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 12px;
}
.color-group label {
min-width: 120px;
font-size: 14px;
color: #666;
font-weight: 500;
}
.color-group input[type="color"] {
width: 60px;
height: 36px;
border: 2px solid #d9d9d9;
border-radius: 6px;
cursor: pointer;
transition: border-color 0.2s;
}
.color-group input[type="color"]:hover {
border-color: #40a9ff;
}
.color-group input[type="color"]:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.actions {
display: flex;
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid #e8e8e8;
}
.actions button {
padding: 10px 20px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.btn-reset {
color: #ff4d4f;
border-color: #ff4d4f;
}
.btn-reset:hover {
background: #fff2f0;
border-color: #ff7875;
}
.actions button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 滚动条样式 */
.color-config-panel::-webkit-scrollbar {
width: 6px;
}
.color-config-panel::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.color-config-panel::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.color-config-panel::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>

View File

@ -0,0 +1,356 @@
<template>
<div class="color-picker-with-alpha">
<div class="color-preview" :style="{ backgroundColor: displayColor }" @click="showPicker = !showPicker">
<span class="color-value">{{ displayColor }}</span>
</div>
<div v-if="showPicker" class="color-picker-popup" @click.stop>
<!-- 基础颜色选择器 -->
<div class="color-section">
<label>基础颜色</label>
<input
type="color"
:value="baseColor"
@input="updateBaseColor"
class="base-color-input"
/>
</div>
<!-- 透明度滑块 -->
<div class="color-section">
<label>透明度</label>
<div class="alpha-slider-container">
<input
type="range"
min="0"
max="255"
:value="alphaValue"
@input="updateAlpha"
class="alpha-slider"
/>
<span class="alpha-value">{{ Math.round(alphaValue / 255 * 100) }}%</span>
</div>
</div>
<!-- 预设透明度选项 -->
<div class="color-section">
<label>快速选择</label>
<div class="alpha-presets">
<button
v-for="preset in alphaPresets"
:key="preset.value"
@click="setAlpha(preset.value)"
:class="{ active: alphaValue === preset.value }"
class="alpha-preset-btn"
>
{{ preset.label }}
</button>
</div>
</div>
<!-- 操作按钮 -->
<div class="color-actions">
<button @click="confirmColor" class="btn-confirm">确定</button>
<button @click="cancelColor" class="btn-cancel">取消</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
interface Props {
modelValue: string;
}
interface Emits {
(e: 'update:modelValue', value: string): void;
}
//
defineOptions({
name: 'ColorPickerWithAlpha'
});
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const showPicker = ref(false);
const baseColor = ref('#FF0000');
const alphaValue = ref(255);
//
const alphaPresets = [
{ label: '100%', value: 255 },
{ label: '80%', value: 204 },
{ label: '60%', value: 153 },
{ label: '40%', value: 102 },
{ label: '20%', value: 51 },
{ label: '0%', value: 0 }
];
//
const displayColor = computed(() => {
if (props.modelValue) {
return props.modelValue;
}
return baseColor.value + alphaValue.value.toString(16).padStart(2, '0').toUpperCase();
});
//
const parseColor = (color: string) => {
if (!color) return { base: '#FF0000', alpha: 255 };
// 8
if (color.match(/^#[0-9A-Fa-f]{8}$/)) {
const base = color.substring(0, 7);
const alpha = parseInt(color.substring(7, 9), 16);
return { base, alpha };
}
// 6
if (color.match(/^#[0-9A-Fa-f]{6}$/)) {
return { base: color, alpha: 255 };
}
return { base: '#FF0000', alpha: 255 };
};
//
const initColor = () => {
const { base, alpha } = parseColor(props.modelValue);
baseColor.value = base;
alphaValue.value = alpha;
};
//
const updateBaseColor = (event: Event) => {
const target = event.target as HTMLInputElement;
baseColor.value = target.value;
//
updateColorValue();
};
//
const updateAlpha = (event: Event) => {
const target = event.target as HTMLInputElement;
alphaValue.value = parseInt(target.value);
//
updateColorValue();
};
//
const setAlpha = (value: number) => {
alphaValue.value = value;
//
updateColorValue();
};
//
const updateColorValue = () => {
const newColor = baseColor.value + alphaValue.value.toString(16).padStart(2, '0').toUpperCase();
emit('update:modelValue', newColor);
};
//
const confirmColor = () => {
const newColor = baseColor.value + alphaValue.value.toString(16).padStart(2, '0').toUpperCase();
emit('update:modelValue', newColor);
showPicker.value = false;
};
//
const cancelColor = () => {
initColor();
showPicker.value = false;
};
//
watch(() => props.modelValue, () => {
initColor();
}, { immediate: true });
//
const handleClickOutside = (event: Event) => {
if (!(event.target as Element).closest('.color-picker-with-alpha')) {
showPicker.value = false;
}
};
//
if (typeof window !== 'undefined') {
document.addEventListener('click', handleClickOutside);
}
</script>
<style scoped>
.color-picker-with-alpha {
position: relative;
display: inline-block;
}
.color-preview {
width: 100%;
height: 36px;
border: 2px solid #d9d9d9;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
background-image:
linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(-45deg, #ccc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(-45deg, transparent 75%, #ccc 75%);
background-size: 8px 8px;
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
}
.color-preview:hover {
border-color: #40a9ff;
}
.color-value {
font-size: 12px;
font-weight: 500;
color: #333;
text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8);
}
.color-picker-popup {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
background: white;
border: 1px solid #d9d9d9;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 16px;
min-width: 280px;
margin-top: 4px;
}
.color-section {
margin-bottom: 16px;
}
.color-section label {
display: block;
font-size: 14px;
font-weight: 500;
color: #666;
margin-bottom: 8px;
}
.base-color-input {
width: 100%;
height: 40px;
border: 1px solid #d9d9d9;
border-radius: 6px;
cursor: pointer;
}
.alpha-slider-container {
display: flex;
align-items: center;
gap: 12px;
}
.alpha-slider {
flex: 1;
height: 6px;
border-radius: 3px;
background: linear-gradient(to right, transparent, #000);
outline: none;
cursor: pointer;
}
.alpha-slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #1890ff;
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.alpha-value {
font-size: 12px;
color: #666;
min-width: 40px;
text-align: right;
}
.alpha-presets {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.alpha-preset-btn {
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.alpha-preset-btn:hover {
border-color: #40a9ff;
color: #40a9ff;
}
.alpha-preset-btn.active {
background: #1890ff;
border-color: #1890ff;
color: white;
}
.color-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.btn-confirm,
.btn-cancel {
padding: 6px 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.btn-confirm {
background: #1890ff;
border-color: #1890ff;
color: white;
}
.btn-confirm:hover {
background: #40a9ff;
border-color: #40a9ff;
}
.btn-cancel {
background: white;
color: #666;
}
.btn-cancel:hover {
border-color: #40a9ff;
color: #40a9ff;
}
</style>

View File

@ -4,6 +4,8 @@ import type { EditorService } from '@core/editor.service';
import { useToolbar } from '@core/useToolbar';
import { computed, inject, type InjectionKey, onBeforeUnmount, onMounted, ref, type ShallowRef } from 'vue';
import ColorConfigPanel from './color-config-panel.vue';
//
//
@ -17,6 +19,9 @@ const editorRef = inject(props.token)!;
const isFullscreen = computed(() => !!document.fullscreenElement);
//
const showColorConfig = ref(false);
// 使 useToolbar 使 jumpToPosition store
const {
toggleGrid: _toggleGrid,
@ -103,12 +108,26 @@ const toggleMeasure = () => {
}
};
//
const toggleColorConfig = () => {
showColorConfig.value = !showColorConfig.value;
//
if (showColorConfig.value && measuring.value) {
measuring.value = false;
start.value = null;
end.value = null;
current.value = null;
}
};
const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
measuring.value = false;
start.value = null;
end.value = null;
current.value = null;
//
showColorConfig.value = false;
}
};
@ -172,6 +191,16 @@ defineOptions({
</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'标尺'" @click="toggleRule">标尺</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'网格'" @click="toggleGrid">网格</a-button>
<a-button
class="icon-btn tool-btn"
size="small"
:title="'颜色配置'"
:type="showColorConfig ? 'primary' : 'default'"
:ghost="!showColorConfig"
@click="toggleColorConfig"
>
<SvgIcon name="palette" :size="16" :forceColor="true" :active="showColorConfig" />
</a-button>
<!-- <a-button class="icon-btn tool-btn" size="small" :title="'截图'" @click="exportImage">截图</a-button>
<a-button
class="icon-btn tool-btn"
@ -184,6 +213,26 @@ defineOptions({
</a-space>
</div>
<!-- 颜色配置面板 -->
<div v-if="showColorConfig" class="color-config-overlay" @click.self="showColorConfig = false">
<div class="color-config-container" @click.stop>
<div class="color-config-header">
<h3>编辑器颜色配置</h3>
<a-button
type="text"
size="small"
@click="showColorConfig = false"
class="close-btn"
>
<SvgIcon name="close" :size="16" />
</a-button>
</div>
<div class="color-config-content">
<ColorConfigPanel />
</div>
</div>
</div>
<!-- 测量叠加层全屏SVG仅在测量模式下启用点击事件 -->
<div
class="measure-overlay"
@ -201,15 +250,15 @@ defineOptions({
<line
:x1="start.x"
:y1="start.y"
:x2="(end || current)!.x"
:y2="(end || current)!.y"
:x2="(end || current)?.x || 0"
:y2="(end || current)?.y || 0"
stroke="#1e90ff"
stroke-width="2"
marker-end="url(#arrow)"
/>
<!-- 起点/终点标记 -->
<circle :cx="start.x" :cy="start.y" r="4" fill="#1e90ff" />
<circle :cx="(end || current)!.x" :cy="(end || current)!.y" r="4" fill="#1e90ff" />
<circle :cx="(end || current)?.x || 0" :cy="(end || current)?.y || 0" r="4" fill="#1e90ff" />
<!-- 文本标签像素距离 -->
<g :transform="`translate(${midpoint.x}, ${midpoint.y})`">
<rect x="-40" y="-22" width="80" height="20" rx="4" ry="4" fill="rgba(0,0,0,0.6)" />
@ -294,6 +343,64 @@ defineOptions({
background-size: 20px 20px;
}
/* 颜色配置面板 */
.color-config-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.color-config-container {
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 90vw;
max-height: 90vh;
width: 800px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.color-config-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid #e8e8e8;
background: #fafafa;
}
.color-config-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
color: #999;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
color: #666;
background: #f0f0f0;
}
.color-config-content {
flex: 1;
overflow-y: auto;
padding: 0;
}
/* 测量叠加层 */
.measure-overlay {
position: fixed;

View File

@ -10,6 +10,7 @@ import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } fro
import { useRoute } from 'vue-router';
import { autoDoorSimulationService, type AutoDoorWebSocketData } from '../services/auto-door-simulation.service';
import colorConfig from '../services/color-config.service';
import {
type ContextMenuState,
createContextMenuManager,
@ -231,6 +232,8 @@ onMounted(async () => {
//
if (editor.value) {
autoDoorSimulationService.setEditorService(editor.value);
//
// colorConfig.setEditorService(editor.value);
// 使WebSocket
// autoDoorSimulationService.startSimulation({

View File

@ -0,0 +1,483 @@
import sTheme from '@core/theme.service';
import { ref, watch } from 'vue';
/**
*
*/
export interface EditorColorConfig {
// 点位颜色
point: {
small: {
stroke: string;
strokeActive: string;
fill: Record<number, string>; // 按点位类型索引
};
large: {
stroke: string;
strokeActive: string;
strokeOccupied: string;
strokeUnoccupied: string;
strokeEmpty: string;
strokeDisabled: string;
strokeEnabled: string;
strokeLocked: string;
strokeUnlocked: string;
};
// 各类型点位专用颜色
types: {
[key: number]: {
stroke: string;
strokeActive: string;
fill: string;
};
};
};
// 路线颜色
route: {
strokeActive: string;
stroke: Record<number, string>; // 按路线类型索引
// 空载和载货路线专用颜色
strokeEmpty: string; // 空载路线颜色
strokeLoaded: string; // 载货路线颜色
};
// 区域颜色
area: {
strokeActive: string;
stroke: Record<number, string>; // 按区域类型索引
fill: Record<number, string>; // 按区域类型索引
// 各类型区域专用颜色
types: {
[key: number]: {
stroke: string;
strokeActive: string;
fill: string;
};
};
};
// 机器人颜色
robot: {
stroke: string;
fill: string;
line: string;
strokeNormal: string;
fillNormal: string;
strokeWarning: string;
fillWarning: string;
strokeFault: string;
fillFault: string;
};
// 库位颜色
storage: {
occupied: string;
available: string;
default: string;
locked: string;
moreButton: {
background: string;
border: string;
text: string;
};
};
// 自动门颜色
autoDoor: {
strokeClosed: string;
fillClosed: string;
strokeOpen: string;
fillOpen: string;
};
// 通用颜色
common: {
color: string;
background: string;
};
}
/**
*
*/
const DEFAULT_COLORS: EditorColorConfig = {
point: {
small: {
stroke: '#8C8C8C',
strokeActive: '#EBB214',
fill: {
1: '#14D1A5',
2: '#69C6F5',
3: '#E48B1D',
4: '#E48B1D',
5: '#a72b69'
}
},
large: {
stroke: '#595959',
strokeActive: '#EBB214',
strokeOccupied: '#ff4d4f',
strokeUnoccupied: '#52c41a',
strokeEmpty: '#1890ff',
strokeDisabled: '#bfbfbf',
strokeEnabled: '#52c41a',
strokeLocked: '#faad14',
strokeUnlocked: '#52c41a'
},
types: {
// 小点位类型 (1-9)
1: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#14D1A5' }, // 普通点
2: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#69C6F5' }, // 等待点
3: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#E48B1D' }, // 避让点
4: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#E48B1D' }, // 临时避让点
5: { stroke: '#8C8C8C', strokeActive: '#EBB214', fill: '#a72b69' }, // 库区点
// 大点位类型 (11+)
11: { stroke: '#595959', strokeActive: '#EBB214', fill: '#1890ff' }, // 电梯点
12: { stroke: '#595959', strokeActive: '#EBB214', fill: '#52c41a' }, // 自动门点
13: { stroke: '#595959', strokeActive: '#EBB214', fill: '#faad14' }, // 充电点
14: { stroke: '#595959', strokeActive: '#EBB214', fill: '#722ed1' }, // 停靠点
15: { stroke: '#595959', strokeActive: '#EBB214', fill: '#13c2c2' }, // 动作点
16: { stroke: '#595959', strokeActive: '#EBB214', fill: '#ff4d4f' }, // 禁行点
}
},
route: {
strokeActive: '#EBB214',
stroke: {
0: '#8C8C8C',
1: '#52C41A',
2: '#1982F3',
10: '#E63A3A'
},
strokeEmpty: '#52C41A', // 空载路线 - 绿色
strokeLoaded: '#1982F3' // 载货路线 - 蓝色
},
area: {
strokeActive: '#EBB214',
stroke: {
1: '#9ACDFF99',
11: '#FF535399',
12: '#0DBB8A99',
13: '#e61e4aad',
14: '#FFD70099'
},
fill: {
1: '#9ACDFF33',
11: '#FF9A9A33',
12: '#0DBB8A33',
13: '#e61e4a33',
14: '#FFD70033'
},
types: {
// 区域类型
1: { stroke: '#9ACDFF99', strokeActive: '#EBB214', fill: '#9ACDFF33' }, // 库区
11: { stroke: '#FF535399', strokeActive: '#EBB214', fill: '#FF9A9A33' }, // 互斥区
12: { stroke: '#0DBB8A99', strokeActive: '#EBB214', fill: '#0DBB8A33' }, // 非互斥区
13: { stroke: '#e61e4aad', strokeActive: '#EBB214', fill: '#e61e4a33' }, // 约束区
14: { stroke: '#FFD70099', strokeActive: '#EBB214', fill: '#FFD70033' }, // 描述区
}
},
robot: {
stroke: '#01FDAF99',
fill: '#01FAAD33',
line: '#01fdaf',
strokeNormal: '#01FDAF99',
fillNormal: '#01FAAD33',
strokeWarning: '#FF851B99',
fillWarning: '#FF851B33',
strokeFault: '#FF4D4F99',
fillFault: '#FF4D4F33'
},
storage: {
occupied: '#ff4d4f',
available: '#52c41a',
default: '#f5f5f5',
locked: '#faad14',
moreButton: {
background: '#e6f4ff',
border: '#1677ff',
text: '#1677ff'
}
},
autoDoor: {
strokeClosed: '#FF4D4F99',
fillClosed: '#FF4D4F33',
strokeOpen: '#1890FF99',
fillOpen: '#1890FF33'
},
common: {
color: '#595959',
background: '#F0F2F5'
}
};
/**
*
*/
class ColorConfigService {
private config = ref<EditorColorConfig>({ ...DEFAULT_COLORS });
private editorService: any = null;
private readonly STORAGE_KEY = 'editor-color-config';
constructor() {
// 从本地存储加载配置
this.loadFromLocalStorage();
// 监听主题变化,重新加载配置
watch(
() => sTheme.theme,
() => {
this.loadConfig();
}
);
// 监听配置变化,自动保存到本地存储
watch(
() => this.config.value,
(newConfig) => {
this.saveToLocalStorage(newConfig);
},
{ deep: true }
);
}
/**
*
*/
public get currentConfig(): EditorColorConfig {
return this.config.value;
}
/**
*
*/
public setEditorService(editor: any): void {
this.editorService = editor;
}
/**
*
*/
private triggerRender(): void {
if (this.editorService && typeof this.editorService.render === 'function') {
this.editorService.render();
}
}
/**
*
*/
private loadFromLocalStorage(): void {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
if (stored) {
const parsedConfig = JSON.parse(stored);
this.config.value = this.mergeConfig(DEFAULT_COLORS, parsedConfig);
} else {
this.config.value = { ...DEFAULT_COLORS };
}
} catch (error) {
console.warn('Failed to load color config from localStorage:', error);
this.config.value = { ...DEFAULT_COLORS };
}
}
/**
*
*/
private saveToLocalStorage(config: EditorColorConfig): void {
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(config));
} catch (error) {
console.warn('Failed to save color config to localStorage:', error);
}
}
/**
* 使
* @deprecated 使
*/
public loadFromScene(): void {
// 不再从场景数据加载,保持向后兼容
console.warn('loadFromScene is deprecated, color config now uses localStorage');
}
/**
* 使
* @deprecated 使
*/
public getConfigForSave(): EditorColorConfig {
console.warn('getConfigForSave is deprecated, color config now uses localStorage');
return { ...this.config.value };
}
/**
*
* @param updates
*/
public updateConfig(updates: Partial<EditorColorConfig>): void {
this.config.value = {
...this.config.value,
...updates
};
// 触发编辑器重新渲染
this.triggerRender();
}
/**
*
*/
public resetToDefault(): void {
this.config.value = { ...DEFAULT_COLORS };
// 触发编辑器重新渲染
this.triggerRender();
}
/**
*
* @param path 'point.small.stroke'
*/
public getColor(path: string): string {
return this.getNestedValue(this.config.value, path) || '';
}
/**
*
* @param path
* @param value
*/
public setColor(path: string, value: string): void {
this.setNestedValue(this.config.value, path, value);
// 触发编辑器重新渲染
this.triggerRender();
}
/**
*
*/
private loadConfig(): void {
const theme = sTheme.editor as any;
if (theme) {
// 从主题配置中加载颜色,如果没有则使用默认值
this.config.value = this.mergeConfig(DEFAULT_COLORS, {
point: {
small: {
stroke: theme['point-s']?.stroke || DEFAULT_COLORS.point.small.stroke,
strokeActive: theme['point-s']?.strokeActive || DEFAULT_COLORS.point.small.strokeActive,
fill: {
1: theme['point-s']?.['fill-1'] || DEFAULT_COLORS.point.small.fill[1],
2: theme['point-s']?.['fill-2'] || DEFAULT_COLORS.point.small.fill[2],
3: theme['point-s']?.['fill-3'] || DEFAULT_COLORS.point.small.fill[3],
4: theme['point-s']?.['fill-4'] || DEFAULT_COLORS.point.small.fill[4],
5: theme['point-s']?.['fill-5'] || DEFAULT_COLORS.point.small.fill[5],
}
},
large: {
stroke: theme['point-l']?.stroke || DEFAULT_COLORS.point.large.stroke,
strokeActive: theme['point-l']?.strokeActive || DEFAULT_COLORS.point.large.strokeActive,
strokeOccupied: theme['point-l']?.['stroke-occupied'] || DEFAULT_COLORS.point.large.strokeOccupied,
strokeUnoccupied: theme['point-l']?.['stroke-unoccupied'] || DEFAULT_COLORS.point.large.strokeUnoccupied,
strokeEmpty: theme['point-l']?.['stroke-empty'] || DEFAULT_COLORS.point.large.strokeEmpty,
strokeDisabled: theme['point-l']?.['stroke-disabled'] || DEFAULT_COLORS.point.large.strokeDisabled,
strokeEnabled: theme['point-l']?.['stroke-enabled'] || DEFAULT_COLORS.point.large.strokeEnabled,
strokeLocked: theme['point-l']?.['stroke-locked'] || DEFAULT_COLORS.point.large.strokeLocked,
strokeUnlocked: theme['point-l']?.['stroke-unlocked'] || DEFAULT_COLORS.point.large.strokeUnlocked,
}
},
route: {
strokeActive: theme.route?.strokeActive || DEFAULT_COLORS.route.strokeActive,
stroke: {
0: theme.route?.['stroke-0'] || DEFAULT_COLORS.route.stroke[0],
1: theme.route?.['stroke-1'] || DEFAULT_COLORS.route.stroke[1],
2: theme.route?.['stroke-2'] || DEFAULT_COLORS.route.stroke[2],
10: theme.route?.['stroke-10'] || DEFAULT_COLORS.route.stroke[10],
},
strokeEmpty: theme.route?.['stroke-empty'] || DEFAULT_COLORS.route.strokeEmpty,
strokeLoaded: theme.route?.['stroke-loaded'] || DEFAULT_COLORS.route.strokeLoaded,
},
area: {
strokeActive: theme.area?.strokeActive || DEFAULT_COLORS.area.strokeActive,
stroke: {
1: theme.area?.['stroke-1'] || DEFAULT_COLORS.area.stroke[1],
11: theme.area?.['stroke-11'] || DEFAULT_COLORS.area.stroke[11],
12: theme.area?.['stroke-12'] || DEFAULT_COLORS.area.stroke[12],
13: theme.area?.['stroke-13'] || DEFAULT_COLORS.area.stroke[13],
14: theme.area?.['stroke-14'] || DEFAULT_COLORS.area.stroke[14],
},
fill: {
1: theme.area?.['fill-1'] || DEFAULT_COLORS.area.fill[1],
11: theme.area?.['fill-11'] || DEFAULT_COLORS.area.fill[11],
12: theme.area?.['fill-12'] || DEFAULT_COLORS.area.fill[12],
13: theme.area?.['fill-13'] || DEFAULT_COLORS.area.fill[13],
14: theme.area?.['fill-14'] || DEFAULT_COLORS.area.fill[14],
}
},
robot: {
stroke: theme.robot?.stroke || DEFAULT_COLORS.robot.stroke,
fill: theme.robot?.fill || DEFAULT_COLORS.robot.fill,
line: theme.robot?.line || DEFAULT_COLORS.robot.line,
strokeNormal: theme.robot?.['stroke-normal'] || DEFAULT_COLORS.robot.strokeNormal,
fillNormal: theme.robot?.['fill-normal'] || DEFAULT_COLORS.robot.fillNormal,
strokeWarning: theme.robot?.['stroke-warning'] || DEFAULT_COLORS.robot.strokeWarning,
fillWarning: theme.robot?.['fill-warning'] || DEFAULT_COLORS.robot.fillWarning,
strokeFault: theme.robot?.['stroke-fault'] || DEFAULT_COLORS.robot.strokeFault,
fillFault: theme.robot?.['fill-fault'] || DEFAULT_COLORS.robot.fillFault,
},
autoDoor: {
strokeClosed: theme.autoDoor?.['stroke-closed'] || DEFAULT_COLORS.autoDoor.strokeClosed,
fillClosed: theme.autoDoor?.['fill-closed'] || DEFAULT_COLORS.autoDoor.fillClosed,
strokeOpen: theme.autoDoor?.['stroke-open'] || DEFAULT_COLORS.autoDoor.strokeOpen,
fillOpen: theme.autoDoor?.['fill-open'] || DEFAULT_COLORS.autoDoor.fillOpen,
},
common: {
color: theme.color || DEFAULT_COLORS.common.color,
background: theme.background || DEFAULT_COLORS.common.background,
}
});
}
}
/**
*
*/
private mergeConfig(target: any, source: any): any {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.mergeConfig(target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
/**
*
*/
private getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
/**
*
*/
private setNestedValue(obj: any, path: string, value: any): void {
const keys = path.split('.');
const lastKey = keys.pop()!;
const target = keys.reduce((current, key) => {
if (!current[key]) {
// 如果是数字键,创建数组或对象
if (!isNaN(Number(key))) {
current[key] = {};
} else {
current[key] = {};
}
}
return current[key];
}, obj);
target[lastKey] = value;
}
}
export default new ColorConfigService();

View File

@ -1,6 +1,8 @@
import type { MapPen } from '@api/map';
import { LockState } from '@meta2d/core';
import colorConfig from '../color-config.service';
// 预加载锁定图标(仅一次)
const lockedIcon = new Image();
lockedIcon.src = new URL('../../assets/icons/png/locked.png', import.meta.url).toString();
@ -207,8 +209,10 @@ export function drawStorageLocation(ctx: CanvasRenderingContext2D, pen: MapPen):
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
// 填充颜色:占用为红色,否则默认灰底
ctx.fillStyle = occupied ? '#ff4d4f' : '#f5f5f5';
// 使用配置的颜色,如果没有配置则使用默认值
ctx.fillStyle = occupied
? colorConfig.getColor('storage.occupied') || '#ff4d4f'
: colorConfig.getColor('storage.default') || '#f5f5f5';
ctx.fill();
ctx.strokeStyle = '#999999';
ctx.stroke();
@ -269,14 +273,14 @@ export function drawStorageMore(ctx: CanvasRenderingContext2D, pen: MapPen): voi
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
// 填充颜色和边框
ctx.fillStyle = '#e6f4ff';
// 使用配置的颜色,如果没有配置则使用默认值
ctx.fillStyle = colorConfig.getColor('storage.moreButton.background') || '#e6f4ff';
ctx.fill();
ctx.strokeStyle = '#1677ff';
ctx.strokeStyle = colorConfig.getColor('storage.moreButton.border') || '#1677ff';
ctx.stroke();
// 绘制文字
ctx.fillStyle = '#1677ff';
ctx.fillStyle = colorConfig.getColor('storage.moreButton.text') || '#1677ff';
ctx.font = `${Math.floor(w * 0.6)}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';

View File

@ -30,6 +30,7 @@ import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from '
import { reactive, watch } from 'vue';
import { AreaOperationService } from './area-operation.service';
import colorConfig from './color-config.service';
import {
drawStorageBackground,
drawStorageLocation,
@ -80,6 +81,8 @@ export class EditorService extends Meta2d {
const { robotGroups, robots, points, routes, areas, ...extraFields } = scene;
// 保存所有额外字段包括width、height等
this.#originalSceneData = extraFields;
// 颜色配置现在使用本地存储,不再从场景数据加载
this.open();
this.setState(editable);
@ -118,6 +121,7 @@ export class EditorService extends Meta2d {
routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
areas: this.areas.value.map((v) => this.#mapSceneArea(v)).filter((v) => !isNil(v)),
blocks: [],
colorConfig: colorConfig.getConfigForSave(), // 添加颜色配置到场景数据
...this.#originalSceneData, // 统一保留所有额外字段包括width、height等
};
@ -1386,6 +1390,9 @@ export class EditorService extends Meta2d {
// 初始化库位服务
this.storageLocationService = new StorageLocationService(this, '');
// 设置颜色配置服务的编辑器实例
colorConfig.setEditorService(this);
// 禁用第6个子元素的拖放功能
(<HTMLDivElement>container.children.item(5)).ondrop = null;
// 监听所有画布事件
@ -1438,7 +1445,7 @@ export class EditorService extends Meta2d {
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) {
switch (e) {
case 'opened':
@ -1584,13 +1591,13 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
if (isConnected === false) {
// 未连接:深红色实心,不描边
ctx.fillStyle = '#fe5a5ae0';
ctx.fillStyle = colorConfig.getColor('autoDoor.strokeClosed') || '#fe5a5ae0';
} else {
// 已连接根据门状态显示颜色0=关门-浅红1=开门-蓝色)
if (deviceStatus === 0) {
ctx.fillStyle = '#cddc39';
ctx.fillStyle = colorConfig.getColor('autoDoor.fillClosed') || '#cddc39';
} else {
ctx.fillStyle = '#1890FF';
ctx.fillStyle = colorConfig.getColor('autoDoor.fillOpen') || '#1890FF';
}
}
ctx.fill();
@ -1603,7 +1610,7 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.: {
ctx.beginPath();
ctx.moveTo(x + w / 2 - r, y + r);
ctx.arcTo(x + w / 2, y, x + w - r, y + h / 2 - r, r);
@ -1611,9 +1618,20 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.arcTo(x + w / 2, y + h, x + r, y + h / 2 + r, r);
ctx.arcTo(x, y + h / 2, x + r, y + h / 2 - r, r);
ctx.closePath();
ctx.fillStyle = get(theme, `point-s.fill-${type}`) ?? '';
// 优先使用小点位专用颜色,如果没有则使用类型专用颜色
const smallGeneralColor = colorConfig.getColor(`point.small.fill.${type}`);
const smallTypeColor = colorConfig.getColor(`point.types.${type}.fill`);
const smallThemeColor = get(theme, `point-s.fill-${type}`);
const finalColor = smallGeneralColor || smallTypeColor || smallThemeColor || '';
ctx.fillStyle = finalColor;
ctx.fill();
ctx.strokeStyle = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke') ?? '';
const smallTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`);
const smallGeneralStrokeColor = colorConfig.getColor(active ? 'point.small.strokeActive' : 'point.small.stroke');
const smallThemeStrokeColor = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke');
ctx.strokeStyle = smallTypeStrokeColor || smallGeneralStrokeColor || smallThemeStrokeColor || '';
if (type === MapPointType.) {
ctx.lineCap = 'round';
ctx.beginPath();
@ -1636,20 +1654,34 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
}
ctx.stroke();
break;
}
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.: {
ctx.roundRect(x, y, w, h, r);
ctx.strokeStyle = statusStyle ?? get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke') ?? '';
// 优先使用类型专用颜色,如果没有则使用通用颜色
const largeTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`);
const largeGeneralStrokeColor = colorConfig.getColor(active ? 'point.large.strokeActive' : 'point.large.stroke');
const largeThemeStrokeColor = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke');
ctx.strokeStyle = statusStyle ?? (largeTypeStrokeColor || largeGeneralStrokeColor || largeThemeStrokeColor || '');
ctx.stroke();
// 填充颜色
const largeTypeFillColor = colorConfig.getColor(`point.types.${type}.fill`);
if (largeTypeFillColor) {
ctx.fillStyle = largeTypeFillColor;
ctx.fill();
}
break;
}
default:
break;
}
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
@ -1706,7 +1738,25 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.save();
ctx.beginPath();
ctx.strokeStyle = get(theme, active ? 'route.strokeActive' : `route.stroke-${pass}`) ?? '';
// 根据路线通行类型获取颜色
let routeColor = '';
if (active) {
routeColor = colorConfig.getColor('route.strokeActive') || get(theme, 'route.strokeActive') || '';
} else {
// 根据通行类型选择颜色
switch (pass) {
case MapRoutePassType.:
routeColor = colorConfig.getColor('route.strokeEmpty') || get(theme, 'route.stroke-empty') || '';
break;
case MapRoutePassType.:
routeColor = colorConfig.getColor('route.strokeLoaded') || get(theme, 'route.stroke-loaded') || '';
break;
default:
routeColor = colorConfig.getColor(`route.stroke.${pass}`) || get(theme, `route.stroke-${pass}`) || '';
break;
}
}
ctx.strokeStyle = routeColor;
ctx.lineWidth = active ? 3 * s : 2 * s;
ctx.moveTo(x1, y1);
switch (type) {
@ -1802,14 +1852,26 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.save();
ctx.rect(x, y, w, h);
ctx.fillStyle = get(theme, `area.fill-${type}`) ?? '';
// 优先使用通用颜色,如果没有则使用类型专用颜色
const generalFillColor = colorConfig.getColor(`area.fill.${type}`);
const typeFillColor = colorConfig.getColor(`area.types.${type}.fill`);
const themeFillColor = get(theme, `area.fill-${type}`);
const finalFillColor = generalFillColor || typeFillColor || themeFillColor || '';
ctx.fillStyle = finalFillColor;
ctx.fill();
ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? '';
const generalStrokeColor = colorConfig.getColor(active ? 'area.strokeActive' : `area.stroke.${type}`);
const typeStrokeColor = colorConfig.getColor(`area.types.${type}.stroke`);
const themeStrokeColor = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`);
ctx.strokeStyle = generalStrokeColor || typeStrokeColor || themeStrokeColor || '';
ctx.stroke();
// 如果是描述区且有描述内容,渲染描述文字
if (type === MapAreaType. && desc) {
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
// 动态计算字体大小,让文字填充区域
let descFontSize = Math.min(w / 6, h / 4, 200);
@ -1854,7 +1916,7 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
});
} else if (type !== MapAreaType. && label) {
// 非描述区才显示标签
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
@ -1905,12 +1967,12 @@ function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const oy = y + h / 2;
ctx.save();
ctx.ellipse(ox, oy, w / 2, h / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = get(theme, `robot.fill-${status}`) ?? get(theme, 'robot.fill') ?? '';
ctx.fillStyle = colorConfig.getColor(`robot.fill${status === 'normal' ? '' : `-${status}`}`) || (get(theme, `robot.fill-${status}`) ?? (get(theme, 'robot.fill') ?? ''));
ctx.fill();
ctx.strokeStyle = get(theme, `robot.stroke-${status}`) ?? get(theme, 'robot.stroke') ?? '';
ctx.strokeStyle = colorConfig.getColor(`robot.stroke${status === 'normal' ? '' : `-${status}`}`) || (get(theme, `robot.stroke-${status}`) ?? (get(theme, 'robot.stroke') ?? ''));
ctx.stroke();
if (path?.length) {
ctx.strokeStyle = get(theme, 'robot.line') ?? '';
ctx.strokeStyle = colorConfig.getColor('robot.line') || (get(theme, 'robot.line') ?? '');
ctx.lineCap = 'round';
ctx.lineWidth = s * 4;
ctx.setLineDash([s * 5, s * 10]);