Ver código fonte

refactor: 阶段图/相位图 icon 渲染参数 + 资源全配置化 (src/config/phase*)

新建 5 个 config 文件:
- phaseIconTokens.js     POS_MAP (16 个 token 几何)
- phaseIconActions.js    CORNER_REFERENCE + actionPriority + sortIconsByLane
- phaseDiagramConfig.js  PHASE_DIAGRAM_LAYOUT (阶段图独立参数)
- signalTimingConfig.js  SIGNAL_TIMING_LAYOUT (相位图独立参数)
- phaseIconAssets.js     TOKEN_ASSETS (SVG/PNG 资源 + getTokenSvg/getTokenImage)

改造 4 个组件/工具:
- phaseLayout.js         改用 config, 保留 POS_MAP/actionPriority re-export 兼容
- phaseSvgIcons.js       改为兼容层从 TOKEN_ASSETS 派生 SVG_MAP
- SignalTimingChart.vue  改用 SIGNAL_TIMING_LAYOUT + getTokenImage, 删 IMAGE_MAP
- PhaseDiagram.vue       主动迁移到 getTokenSvg()
画安 2 semanas atrás
pai
commit
6f960a578f

+ 3 - 3
src/components/ui/PhaseDiagram.vue

@@ -15,7 +15,7 @@
 </template>
 
 <script>
-import { SVG_MAP } from '@/utils/phaseSvgIcons';
+import { getTokenSvg } from '@/config/phaseIconAssets';
 import { layoutIconsAdaptive } from '@/utils/phaseLayout';
 
 // 根据数据动态渲染单个"相位方块":
@@ -39,7 +39,7 @@ export default {
     computed: {
         // 丢弃没有对应 svg 的 token, 避免空 mask 渲染
         validIcons() {
-            return this.icons.filter(t => SVG_MAP[t]);
+            return this.icons.filter(t => getTokenSvg(t));
         },
         // 同 corner 多 icon 时自适应分槽; 单 icon 时退化为 POS_MAP 原位置
         iconLayouts() {
@@ -57,7 +57,7 @@ export default {
             const color = this.arrowColors[token] || this.arrowColor;
             return {
                 ...(this.iconLayouts[token] || {}),
-                '--svg': `url("${SVG_MAP[token]}")`,
+                '--svg': `url("${getTokenSvg(token)}")`,
                 '--arrow-color': color,
             };
         },

+ 22 - 31
src/components/ui/SignalTimingChart.vue

@@ -13,7 +13,8 @@
 import * as echarts from 'echarts';
 import echartsResize from '@/mixins/echartsResize.js';
 import { POS_MAP, actionPriority } from '@/utils/phaseLayout';
-import { SVG_MAP } from '@/utils/phaseSvgIcons';
+import { getTokenImage } from '@/config/phaseIconAssets';
+import { SIGNAL_TIMING_LAYOUT } from '@/config/signalTimingConfig';
 
 // 全局心跳定时器:所有 autoScan 实例共享同一个 setInterval
 // 各实例基于 Date.now() 对自身 cycleLength 取模计算位置,天然同步
@@ -53,23 +54,8 @@ ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, 4, 20);
 ctx.fillStyle = COLORS.STRIPE_GREEN; ctx.fillRect(0, 0, 2, 20); 
 const stripePattern = { image: stripeCanvas, repeat: 'repeat' };
 
-const IMAGE_MAP = {
-  'STRAIGHT_DOWN': require('@/assets/images/icon_straight_down.png'),
-  'TURN_DOWN_LEFT': require('@/assets/images/icon_turn_down_left.png'),
-  'TURN_DOWN_LEFT_UTURN': require('@/assets/images/icon_turn_down_left_uturn.png'),
-
-  'STRAIGHT_UP': require('@/assets/images/icon_straight_up.png'),
-  'TURN_UP_LEFT': require('@/assets/images/icon_turn_up_left.png'),
-  'TURN_UP_LEFT_UTURN': require('@/assets/images/icon_turn_up_left_uturn.png'),
-
-  'STRAIGHT_LEFT': require('@/assets/images/icon_straight_left.png'),
-  'TURN_LEFT_DOWN': require('@/assets/images/icon_turn_left_down.png'),
-  'TURN_LEFT_DOWN_UTURN': require('@/assets/images/icon_turn_left_down_uturn.png'), 
-
-  'STRAIGHT_RIGHT': require('@/assets/images/icon_straight_right.png'),
-  'TURN_RIGHT_UP': require('@/assets/images/icon_turn_right_up.png'),
-  'TURN_RIGHT_UP_UTURN': require('@/assets/images/icon_turn_right_up_uturn.png' )
-};
+// 旧 IMAGE_MAP (12 行 require) 已删除, 改用 getTokenImage(token, 'png')
+// 资源统一从 src/config/phaseIconAssets.js 取
 
 // POS_MAP 抽到了 @/utils/phaseLayout 与 PhaseDiagram 共用
 
@@ -580,30 +566,35 @@ export default {
           }
         });
 
-        // 统一缩放(替换原 1/√n 自适应缩放,让 icon 大小不随数量变)
-        const STD_FS = dynamicFs * 0.7;
-        const iconGap = Math.max(1, Math.round(0.5 * dynamicFs));
+        // 参数全部从 SIGNAL_TIMING_LAYOUT 读取, 调整请改 src/config/signalTimingConfig.js
+        const {
+          STD_FS_RATIO, ICON_GAP_RATIO, PAD_FACTOR,
+          ENABLE_STRAIGHT_SHRINK, TURN_RATIO,
+        } = SIGNAL_TIMING_LAYOUT;
+        const STD_FS = dynamicFs * STD_FS_RATIO;
+        const iconGap = Math.max(1, Math.round(ICON_GAP_RATIO * dynamicFs));
 
         Object.entries(byCorner).forEach(([corner, items]) => {
           const n = items.length;
           if (n === 0) return;
           const cornerIsNS = corner === 'LT' || corner === 'RB';
 
-          // 按车道布局排序:右转 → 直行 → 左转 → 左转掉头(外到内)
+          // 按车道布局排序: 右转 → 直行 → 左转 → 左转掉头 (外到内)
           items.sort((a, b) => actionPriority(a.token) - actionPriority(b.token));
 
-          // 相位图色块矮长,与阶段图(30×30 方块)的几何约束不同。
-          // 阶段图里 STRAIGHT 高瘦比例 (2.95:1) 会显得太高,需缩小 (sf=0.556);
-          // 但相位图色块本身已经矮,STRAIGHT 再缩反而显得太小,所以这里 sf=1
-          // 起始偏移让 icon 整体远离色块边缘,向色块中央方向偏
+          // 起始偏移让 icon 整体远离色块边缘, 向色块中央方向偏
           const firstAnchor = items[0] ? items[0].m : null;
           const firstPad = firstAnchor
-            ? Math.round((cornerIsNS ? firstAnchor.padX : firstAnchor.padY) * dynamicFs * 0.5)
+            ? Math.round((cornerIsNS ? firstAnchor.padX : firstAnchor.padY) * dynamicFs * PAD_FACTOR)
             : 0;
           let cumOffset = firstPad;
           items.forEach(({ token, m }) => {
-            const drawW = Math.round(m.baseW * STD_FS);
-            const drawH = Math.round(m.baseH * STD_FS);
+            // STRAIGHT 缩小开关 (相位图默认关, 见 SIGNAL_TIMING_LAYOUT 注释)
+            const isStraight = token.startsWith('STRAIGHT_');
+            const straightRatio = cornerIsNS ? (m.baseH / m.baseW) : (m.baseW / m.baseH);
+            const sf = (isStraight && ENABLE_STRAIGHT_SHRINK) ? (TURN_RATIO / straightRatio) : 1;
+            const drawW = Math.round(m.baseW * STD_FS * sf);
+            const drawH = Math.round(m.baseH * STD_FS * sf);
 
             let iconX, iconY;
             if (corner === 'LT') {
@@ -622,8 +613,8 @@ export default {
 
             cumOffset += (cornerIsNS ? drawW : drawH) + iconGap;
 
-            // 优先用 PNG (IMAGE_MAP), 没有则 fallback 到 SVG (新增的 right turn 等)
-            const imageUrl = IMAGE_MAP[token] || SVG_MAP[token];
+            // 优先用 PNG, 没有则 fallback 到 SVG (由 phaseIconAssets 统一管理)
+            const imageUrl = getTokenImage(token, 'png');
             if (imageUrl) {
               innerGroup.children.push({
                 type: 'image',

+ 31 - 0
src/config/phaseDiagramConfig.js

@@ -0,0 +1,31 @@
+// 阶段图 (PhaseDiagram, 路口方块组件) 几何参数
+//
+// 调整任何值后, 所有路口方块组件的渲染会同步变化。
+// 单位: 30 × 30 路口方块坐标系下的"单位" (30 = 100%)
+//
+// 调参建议:
+//   - 想 icon 更大     → ICON_MAIN_SIZE +
+//   - 想中央编号更宽   → CENTER_RESERVED +
+//   - 想 icon 更靠中央 → PAD_FACTOR +
+//   - 想直行更高       → ENABLE_STRAIGHT_SHRINK = false
+
+export const PHASE_DIAGRAM_LAYOUT = {
+  // ─── 容器几何 ───
+  HALF_BLOCK:       15,     // 半区大小 (固定, 与 30×30 路口方块对应)
+  CENTER_RESERVED:  1,      // 中央编号留白 (半边距) → 中央总宽 2 单位
+
+  // ─── icon 基础尺寸 ───
+  ICON_MAIN_SIZE:   12,     // 每个 icon 主轴尺寸 (× sizeScale)
+                            // 1 icon 时主轴 = 12×0.55 = 6.6 单位 (pct 22%)
+                            // 3 icon 时受 EFFECTIVE_HALF=14 限制 compress 到 pct 14.3%
+  ICON_GAP:         0.5,    // icon 之间的间距
+
+  // ─── 排列 ───
+  PAD_FACTOR:       0.5,    // 起始偏移系数 (× POS_MAP.padX × padScale)
+
+  // ─── 直行 icon 视觉调整 ───
+  // STRAIGHT 原始 baseH/baseW ≈ 2.95 偏瘦高, 会显得比 TURN 大
+  // 启用后整体缩 sf = TURN_RATIO / 原比例, 让副轴与 TURN 对齐
+  ENABLE_STRAIGHT_SHRINK: true,
+  TURN_RATIO:       1.64,
+};

+ 34 - 0
src/config/phaseIconActions.js

@@ -0,0 +1,34 @@
+// 车道动作业务规则
+//
+// 决定 icon 渲染顺序、3 slot 映射, 与司机视角一致:
+//   外侧 → 内侧: 右转(0) → 直行(1) → 左转(2) → 左转掉头(3)
+//
+// CORNER_REFERENCE 用于"固定 3 slot"算法 (layoutIconsAdaptive):
+//   不论实际数据有几个 icon, 都按 corner 的 3 个 reference token (右转/直行/左转) 算 slot 位置
+//   实际 icon 按它的 actionPriority 找对应 slot
+//   → 1 icon 与 3 icon 中同位置 icon 完全对齐 (大小 + 位置)
+
+// 右转 token 集合
+export const RIGHT_TURN_TOKENS = new Set([
+  'TURN_DOWN_RIGHT', 'TURN_UP_RIGHT', 'TURN_LEFT_UP', 'TURN_RIGHT_DOWN'
+]);
+
+// 车道动作优先级
+export function actionPriority(token) {
+  if (RIGHT_TURN_TOKENS.has(token))    return 0; // 右转最外
+  if (token.startsWith('STRAIGHT_'))   return 1; // 直行居中
+  if (token.endsWith('_UTURN'))        return 3; // 左转掉头最内
+  return 2;                                       // 其它为左转
+}
+
+// 每个 corner 的 3 个 reference token: [右转, 直行, 左转]
+export const CORNER_REFERENCE = {
+  LT: ['TURN_DOWN_RIGHT',  'STRAIGHT_DOWN',  'TURN_DOWN_LEFT'],
+  RB: ['TURN_UP_RIGHT',    'STRAIGHT_UP',    'TURN_UP_LEFT'],
+  RT: ['TURN_LEFT_UP',     'STRAIGHT_LEFT',  'TURN_LEFT_DOWN'],
+  LB: ['TURN_RIGHT_DOWN',  'STRAIGHT_RIGHT', 'TURN_RIGHT_UP'],
+};
+
+export function sortIconsByLane(tokens) {
+  return [...tokens].sort((a, b) => actionPriority(a) - actionPriority(b));
+}

+ 93 - 0
src/config/phaseIconAssets.js

@@ -0,0 +1,93 @@
+// Token → 视觉资源(SVG + PNG)显式映射
+//
+// 替代旧机制 (require.context 自动收集 SVG + 手写 IMAGE_MAP 加载 PNG)。
+// 加新 token 时必须在 phaseIconTokens.js 加 POS_MAP + 这里加 ASSETS, 否则 webpack 编译报错。
+//
+// 资源约定:
+//   svg: PhaseDiagram CSS mask-image 用 (必填, 16 个 token 都有)
+//   png: SignalTimingChart ECharts image 用 (可选, 当前 12 个 token 有, 4 个右转无 PNG fallback 到 svg)
+
+// ─── 北方向 (LT) ───
+import straightDown_svg     from '@/assets/images/svg/icon_straight_down.svg';
+import straightDown_png     from '@/assets/images/icon_straight_down.png';
+import turnDownLeft_svg     from '@/assets/images/svg/icon_turn_down_left.svg';
+import turnDownLeft_png     from '@/assets/images/icon_turn_down_left.png';
+import turnDownLeftU_svg    from '@/assets/images/svg/icon_turn_down_left_uturn.svg';
+import turnDownLeftU_png    from '@/assets/images/icon_turn_down_left_uturn.png';
+import turnDownRight_svg    from '@/assets/images/svg/icon_turn_down_right.svg';
+// 北右转无 png (镜像生成)
+
+// ─── 南方向 (RB) ───
+import straightUp_svg       from '@/assets/images/svg/icon_straight_up.svg';
+import straightUp_png       from '@/assets/images/icon_straight_up.png';
+import turnUpLeft_svg       from '@/assets/images/svg/icon_turn_up_left.svg';
+import turnUpLeft_png       from '@/assets/images/icon_turn_up_left.png';
+import turnUpLeftU_svg      from '@/assets/images/svg/icon_turn_up_left_uturn.svg';
+import turnUpLeftU_png      from '@/assets/images/icon_turn_up_left_uturn.png';
+import turnUpRight_svg      from '@/assets/images/svg/icon_turn_up_right.svg';
+// 南右转无 png
+
+// ─── 东方向 (RT) ───
+import straightLeft_svg     from '@/assets/images/svg/icon_straight_left.svg';
+import straightLeft_png     from '@/assets/images/icon_straight_left.png';
+import turnLeftDown_svg     from '@/assets/images/svg/icon_turn_left_down.svg';
+import turnLeftDown_png     from '@/assets/images/icon_turn_left_down.png';
+import turnLeftDownU_svg    from '@/assets/images/svg/icon_turn_left_down_uturn.svg';
+import turnLeftDownU_png    from '@/assets/images/icon_turn_left_down_uturn.png';
+import turnLeftUp_svg       from '@/assets/images/svg/icon_turn_left_up.svg';
+// 东右转无 png
+
+// ─── 西方向 (LB) ───
+import straightRight_svg    from '@/assets/images/svg/icon_straight_right.svg';
+import straightRight_png    from '@/assets/images/icon_straight_right.png';
+import turnRightUp_svg      from '@/assets/images/svg/icon_turn_right_up.svg';
+import turnRightUp_png      from '@/assets/images/icon_turn_right_up.png';
+import turnRightUpU_svg     from '@/assets/images/svg/icon_turn_right_up_uturn.svg';
+import turnRightUpU_png     from '@/assets/images/icon_turn_right_up_uturn.png';
+import turnRightDown_svg    from '@/assets/images/svg/icon_turn_right_down.svg';
+// 西右转无 png
+
+export const TOKEN_ASSETS = {
+  // 北方向 (LT)
+  'STRAIGHT_DOWN':         { svg: straightDown_svg,  png: straightDown_png  },
+  'TURN_DOWN_LEFT':        { svg: turnDownLeft_svg,  png: turnDownLeft_png  },
+  'TURN_DOWN_LEFT_UTURN':  { svg: turnDownLeftU_svg, png: turnDownLeftU_png },
+  'TURN_DOWN_RIGHT':       { svg: turnDownRight_svg },
+
+  // 南方向 (RB)
+  'STRAIGHT_UP':           { svg: straightUp_svg,    png: straightUp_png    },
+  'TURN_UP_LEFT':          { svg: turnUpLeft_svg,    png: turnUpLeft_png    },
+  'TURN_UP_LEFT_UTURN':    { svg: turnUpLeftU_svg,   png: turnUpLeftU_png   },
+  'TURN_UP_RIGHT':         { svg: turnUpRight_svg },
+
+  // 东方向 (RT)
+  'STRAIGHT_LEFT':         { svg: straightLeft_svg,  png: straightLeft_png  },
+  'TURN_LEFT_DOWN':        { svg: turnLeftDown_svg,  png: turnLeftDown_png  },
+  'TURN_LEFT_DOWN_UTURN':  { svg: turnLeftDownU_svg, png: turnLeftDownU_png },
+  'TURN_LEFT_UP':          { svg: turnLeftUp_svg },
+
+  // 西方向 (LB)
+  'STRAIGHT_RIGHT':        { svg: straightRight_svg, png: straightRight_png },
+  'TURN_RIGHT_UP':         { svg: turnRightUp_svg,   png: turnRightUp_png   },
+  'TURN_RIGHT_UP_UTURN':   { svg: turnRightUpU_svg,  png: turnRightUpU_png  },
+  'TURN_RIGHT_DOWN':       { svg: turnRightDown_svg },
+};
+
+// ─── 工具函数 ───
+
+/** 取 token 的 SVG URL (PhaseDiagram CSS mask 用) */
+export function getTokenSvg(token) {
+  const a = TOKEN_ASSETS[token];
+  return a ? (a.svg || null) : null;
+}
+
+/**
+ * 取 token 的图像 URL, 优先 prefer, fallback 另一种
+ * @param {string} token
+ * @param {'png'|'svg'} prefer
+ */
+export function getTokenImage(token, prefer = 'png') {
+  const a = TOKEN_ASSETS[token];
+  if (!a) return null;
+  return prefer === 'png' ? (a.png || a.svg) : (a.svg || a.png);
+}

+ 43 - 0
src/config/phaseIconTokens.js

@@ -0,0 +1,43 @@
+// 路口方向 icon token 几何注册表
+//
+// 16 个 token = 4 方向 × {直行/左转/左转掉头/右转}
+//
+// 命名约定: TOKEN = (动作类型)_(车头方向)[_(变体)]
+//   STRAIGHT_<dir>           直行 (4 个)
+//   TURN_<dir>_LEFT          左转 (4 个)
+//   TURN_<dir>_LEFT_UTURN    左转掉头 (4 个)
+//   TURN_<dir>_RIGHT|UP|DOWN 右转 (4 个, 镜像左转 SVG 生成)
+//
+// 坐标系: 30 × 30 单位的路口方块, 四角 + padX/padY + baseW/baseH
+//   pos: LT / RT / LB / RB  — 哪个角落锚定
+//     LT = 北方向 (上方驶入)
+//     RB = 南方向 (下方驶入)
+//     RT = 东方向 (右侧驶入)
+//     LB = 西方向 (左侧驶入)
+//   padX, padY: 离该角落的偏移
+//   baseW, baseH: 箭头本身占的尺寸
+export const POS_MAP = {
+  // 1. 上方驶入 (北方向) -> 靠左上角 (LT)
+  'STRAIGHT_DOWN':         { pos: 'LT', padX: 10, padY: 0, baseW: 7,  baseH: 20.67 },
+  'TURN_DOWN_LEFT':        { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },
+  'TURN_DOWN_LEFT_UTURN':  { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 22.67 },
+  'TURN_DOWN_RIGHT':       { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },  // 北右转(镜像)
+
+  // 2. 下方驶入 (南方向) -> 靠右下角 (RB)
+  'STRAIGHT_UP':           { pos: 'RB', padX: 10, padY: 0, baseW: 7,  baseH: 20.67 },
+  'TURN_UP_LEFT':          { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },
+  'TURN_UP_LEFT_UTURN':    { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 22.67 },
+  'TURN_UP_RIGHT':         { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },  // 南右转(镜像)
+
+  // 3. 右侧驶入 (东方向) -> 靠右上角 (RT)
+  'STRAIGHT_LEFT':         { pos: 'RT', padX: 0, padY: 10, baseW: 20.33, baseH: 6.33 },
+  'TURN_LEFT_DOWN':        { pos: 'RT', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },
+  'TURN_LEFT_DOWN_UTURN':  { pos: 'RT', padX: 0, padY: 10, baseW: 22.67, baseH: 12.33 },
+  'TURN_LEFT_UP':          { pos: 'RT', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },  // 东右转(镜像)
+
+  // 4. 左侧驶入 (西方向) -> 靠左下角 (LB)
+  'STRAIGHT_RIGHT':        { pos: 'LB', padX: 0, padY: 10, baseW: 20.33, baseH: 6.33 },
+  'TURN_RIGHT_UP':         { pos: 'LB', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },
+  'TURN_RIGHT_UP_UTURN':   { pos: 'LB', padX: 0, padY: 10, baseW: 22.67, baseH: 12.33 },
+  'TURN_RIGHT_DOWN':       { pos: 'LB', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },  // 西右转(镜像)
+};

+ 24 - 0
src/config/signalTimingConfig.js

@@ -0,0 +1,24 @@
+// 相位图 (SignalTimingChart, 时间条带组件) 几何参数
+//
+// 调整任何值后, 所有时间条带图的 icon 渲染会同步变化。
+// 单位: 基于 dynamicFs (色块的几何缩放比) × 比例
+//
+// 调参建议:
+//   - 想 icon 更大       → STD_FS_RATIO +
+//   - 想 icon 间距更大   → ICON_GAP_RATIO +
+//   - 想 icon 更靠色块内 → PAD_FACTOR +
+//   - 想直行更小         → ENABLE_STRAIGHT_SHRINK = true
+
+export const SIGNAL_TIMING_LAYOUT = {
+  // ─── icon 缩放 ───
+  STD_FS_RATIO:     0.7,    // icon 缩放系数 (× dynamicFs)
+
+  // ─── 间距与排列 ───
+  ICON_GAP_RATIO:   0.5,    // icon 间距系数 (× dynamicFs, 最少 1px)
+  PAD_FACTOR:       0.5,    // 起始偏移系数 (× dynamicFs × POS_MAP.padX)
+
+  // ─── 直行 icon 视觉调整 ───
+  // 相位图色块本身已经矮, STRAIGHT 缩小会显得太小, 所以默认关
+  ENABLE_STRAIGHT_SHRINK: false,
+  TURN_RATIO:       1.64,
+};

+ 40 - 87
src/utils/phaseLayout.js

@@ -1,43 +1,33 @@
 // 相位方向箭头在"路口方块"内的几何布局
-// 共享于 SignalTimingChart (canvas 内绘图) 与 PhaseDiagram (CSS 布局)
 //
-// 坐标系: 30 × 30 单位的路口方块, 四角 + padX/padY + baseW/baseH
-//   pos: LT / RT / LB / RB  -- 哪个角落锚定
-//   padX, padY: 离该角落的偏移 (与 pos 共同决定 anchor 边)
-//   baseW, baseH: 箭头本身占的尺寸
-
-export const POS_MAP = {
-  // 1. 上方驶入 (北方向) -> 靠左上角 (LT)
-  'STRAIGHT_DOWN':         { pos: 'LT', padX: 10, padY: 0, baseW: 7,  baseH: 20.67 },
-  'TURN_DOWN_LEFT':        { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },
-  'TURN_DOWN_LEFT_UTURN':  { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 22.67 },
-  'TURN_DOWN_RIGHT':       { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },  // 北右转(镜像)
-
-  // 2. 下方驶入 (南方向) -> 靠右下角 (RB)
-  'STRAIGHT_UP':           { pos: 'RB', padX: 10, padY: 0, baseW: 7,  baseH: 20.67 },
-  'TURN_UP_LEFT':          { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },
-  'TURN_UP_LEFT_UTURN':    { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 22.67 },
-  'TURN_UP_RIGHT':         { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },  // 南右转(镜像)
-
-  // 3. 右侧驶入 (东方向) -> 靠右上角 (RT)
-  'STRAIGHT_LEFT':         { pos: 'RT', padX: 0, padY: 10, baseW: 20.33, baseH: 6.33 },
-  'TURN_LEFT_DOWN':        { pos: 'RT', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },
-  'TURN_LEFT_DOWN_UTURN':  { pos: 'RT', padX: 0, padY: 10, baseW: 22.67, baseH: 12.33 },
-  'TURN_LEFT_UP':          { pos: 'RT', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },  // 东右转(镜像)
-
-  // 4. 左侧驶入 (西方向) -> 靠左下角 (LB)
-  'STRAIGHT_RIGHT':        { pos: 'LB', padX: 0, padY: 10, baseW: 20.33, baseH: 6.33 },
-  'TURN_RIGHT_UP':         { pos: 'LB', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },
-  'TURN_RIGHT_UP_UTURN':   { pos: 'LB', padX: 0, padY: 10, baseW: 22.67, baseH: 12.33 },
-  'TURN_RIGHT_DOWN':       { pos: 'LB', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },  // 西右转(镜像)
-};
+// 本文件聚焦于 layoutIconsAdaptive 算法本身。
+// 数据 / 规则 / 参数全部托管到 src/config/:
+//   - POS_MAP                 → @/config/phaseIconTokens
+//   - actionPriority          → @/config/phaseIconActions
+//   - CORNER_REFERENCE        → @/config/phaseIconActions
+//   - sortIconsByLane         → @/config/phaseIconActions
+//   - RIGHT_TURN_TOKENS       → @/config/phaseIconActions
+//   - PHASE_DIAGRAM_LAYOUT    → @/config/phaseDiagramConfig
+//
+// 为向后兼容, 仍 re-export 常用 token 工具, 旧调用
+// (`import { POS_MAP } from '@/utils/phaseLayout'`) 仍然有效。
+
+import { POS_MAP } from '@/config/phaseIconTokens';
+import {
+  actionPriority,
+  sortIconsByLane,
+  CORNER_REFERENCE,
+} from '@/config/phaseIconActions';
+import { PHASE_DIAGRAM_LAYOUT } from '@/config/phaseDiagramConfig';
+
+// 向后兼容 re-export
+export { POS_MAP, actionPriority, sortIconsByLane };
 
 // 把 token 对应的 POS_MAP 项换算成 CSS 绝对定位 style (百分比, 自适应任意尺寸的方框)
 //
 // 两个独立缩放参数:
 //   sizeScale - 作用于 baseW/baseH, 控制 "箭头本身大小" (1=原图, <1 变小, >1 变大)
 //   padScale  - 作用于 padX/padY,   控制 "离角偏移" (1=原图, <1 更靠角=中央留白大, >1 更靠中心)
-// 拆成两个是因为常见需求是"大小别动, 多腾点中央空间"。
 export function positionStyleOf(token, sizeScale = 1, padScale = 1) {
   const m = POS_MAP[token];
   if (!m) return {};
@@ -53,54 +43,22 @@ export function positionStyleOf(token, sizeScale = 1, padScale = 1) {
   };
 }
 
-// ----------------------------------------------------------------------------
-// 自适应多 icon 布局:当同一 corner 有多个 token (NS 或 EW 同向多 icon) 时,
-// 沿对应方向均分 slot,每个 icon 按其原始 baseW/baseH 比例 fit 到 slot 内。
+// 自适应多 icon 布局: 固定 3 slot, 让 1 icon 与多 icon 中同位置 icon 完全对齐
 //
-// 角落分组规则:
-//   LT/RB → NS corner,icons 沿 X 轴水平排列(北/南方向用上下半区)
-//   RT/LB → EW corner,icons 沿 Y 轴垂直排列(东/西方向用左右半区)
+// 算法步骤:
+//   1. icons 按 corner (LT/RT/LB/RB) 分组
+//   2. 每 corner 始终按 3 个 reference token (右转/直行/左转) 算 slot 位置
+//   3. 实际 icon 按 actionPriority (0/1/2/3) 落到对应 slot
 //
-// 单 corner 单 icon 时退化为 positionStyleOf 行为(保持向后兼容)。
-//
-// @returns { [token: string]: style }
-// ----------------------------------------------------------------------------
-const HALF_BLOCK       = 15;   // 30×30 路口方块的半区大小(单位)
-const CENTER_RESERVED  = 1;    // 中央留给编号 (.phase-no) 的半边距(单位)→ 中央总宽 2 单位
-const EFFECTIVE_HALF   = HALF_BLOCK - CENTER_RESERVED;  // icon 主轴最大可占用(14 单位 = 46.7%)
-const ICON_GAP         = 0.5;  // 多 icon 之间的间距(单位)
-const ICON_MAIN_SIZE   = 12;   // 每个 icon 主轴方向的统一尺寸(单位)
-                               // 1 icon 时主轴 = 12×0.55 = 6.6 单位 (pct 22%)
-                               // 3 icon 时受 EFFECTIVE_HALF=14 限制 compress 到 pct 14.3%
-
-// 按真实车道布局的排列优先级:从外(路口边)到内(路口中心)依次为
-//   右转 → 直行 → 左转 → 左转掉头
-// corner 内多 icon 按此顺序沿主轴累加,与司机视角一致
-const RIGHT_TURN_TOKENS = new Set([
-  'TURN_DOWN_RIGHT', 'TURN_UP_RIGHT', 'TURN_LEFT_UP', 'TURN_RIGHT_DOWN'
-]);
-
-export function actionPriority(token) {
-  if (RIGHT_TURN_TOKENS.has(token))    return 0; // 右转最外
-  if (token.startsWith('STRAIGHT_'))   return 1; // 直行居中
-  if (token.endsWith('_UTURN'))        return 3; // 左转掉头最内
-  return 2;                                       // 其它为左转
-}
-
-export function sortIconsByLane(tokens) {
-  return [...tokens].sort((a, b) => actionPriority(a) - actionPriority(b));
-}
-
-// 每个 corner 的 3 个 reference token:[右转, 直行, 左转]
-// 用于计算"固定 3 slot"位置,让单 icon 与多 icon 中同位置 icon 完全对齐
-const CORNER_REFERENCE = {
-  LT: ['TURN_DOWN_RIGHT', 'STRAIGHT_DOWN', 'TURN_DOWN_LEFT'],
-  RB: ['TURN_UP_RIGHT',   'STRAIGHT_UP',   'TURN_UP_LEFT'],
-  RT: ['TURN_LEFT_UP',    'STRAIGHT_LEFT', 'TURN_LEFT_DOWN'],
-  LB: ['TURN_RIGHT_DOWN', 'STRAIGHT_RIGHT', 'TURN_RIGHT_UP'],
-};
-
+// 参数全部从 PHASE_DIAGRAM_LAYOUT 读取, 调整请改 src/config/phaseDiagramConfig.js
 export function layoutIconsAdaptive(icons, sizeScale = 1, padScale = 1) {
+  const {
+    HALF_BLOCK, CENTER_RESERVED, ICON_MAIN_SIZE, ICON_GAP,
+    PAD_FACTOR, TURN_RATIO, ENABLE_STRAIGHT_SHRINK,
+  } = PHASE_DIAGRAM_LAYOUT;
+  const EFFECTIVE_HALF = HALF_BLOCK - CENTER_RESERVED;
+  const mainSize = ICON_MAIN_SIZE * sizeScale;
+
   // 按 corner 分组
   const byCorner = { LT: [], RT: [], LB: [], RB: [] };
   icons.forEach(token => {
@@ -111,17 +69,12 @@ export function layoutIconsAdaptive(icons, sizeScale = 1, padScale = 1) {
   const styles = {};
   const pct = n => `${(n / 30) * 100}%`;
 
-  // STRAIGHT 整体缩小 sf 让副轴与 TURN 对齐
-  const TURN_RATIO = 1.64;
-  const mainSize  = ICON_MAIN_SIZE * sizeScale;
-  const PAD_FACTOR = 0.5;
-
   const computeSize = (token, isNS) => {
     const m = POS_MAP[token];
     if (!m) return { w: 0, h: 0 };
     const isStraight = token.startsWith('STRAIGHT_');
     const straightRatio = isNS ? (m.baseH / m.baseW) : (m.baseW / m.baseH);
-    const sf = isStraight ? (TURN_RATIO / straightRatio) : 1;
+    const sf = (isStraight && ENABLE_STRAIGHT_SHRINK) ? (TURN_RATIO / straightRatio) : 1;
     let w, h;
     if (isNS) {
       w = mainSize * sf;
@@ -141,7 +94,7 @@ export function layoutIconsAdaptive(icons, sizeScale = 1, padScale = 1) {
     const sideY = corner === 'LT' || corner === 'RT' ? 'top'  : 'bottom';
 
     // 始终按 corner 的 3 个 reference token (右转/直行/左转) 计算 slot 位置
-    // 这样 1 icon 与多 icon 中同位置 icon 完全对齐(大小 + 位置)
+    // 这样 1 icon 与多 icon 中同位置 icon 完全对齐 (大小 + 位置)
     const refTokens = CORNER_REFERENCE[corner];
     const refSizes  = refTokens.map(t => computeSize(t, isNS));
 
@@ -151,7 +104,7 @@ export function layoutIconsAdaptive(icons, sizeScale = 1, padScale = 1) {
     const totalLen  = firstPad + sumMain + ICON_GAP * (refSizes.length - 1);
     const compress  = totalLen > EFFECTIVE_HALF ? EFFECTIVE_HALF / totalLen : 1;
 
-    // 计算每个 slot 的位置(基于 reference)
+    // 计算每个 slot 的位置 (基于 reference)
     let cumOffset = firstPad * compress;
     const slotPositions = refSizes.map(size => {
       const w = size.w * compress;
@@ -161,14 +114,14 @@ export function layoutIconsAdaptive(icons, sizeScale = 1, padScale = 1) {
       return slot;
     });
 
-    // 渲染实际 token按 actionPriority 找对应 slot
+    // 渲染实际 token, 按 actionPriority 找对应 slot
     // priority 3 (左转掉头) 暂时归到 slot 2 (左转位置)
     tokens.forEach(actualToken => {
       const priority = actionPriority(actualToken);
       const slotIdx = Math.min(priority, refSizes.length - 1);
       const slotPos = slotPositions[slotIdx];
 
-      // 实际 token 的尺寸(按其原 baseW/baseH 算)
+      // 实际 token 的尺寸 (按其原 baseW/baseH 算)
       const actSize = computeSize(actualToken, isNS);
       const w = actSize.w * compress;
       const h = actSize.h * compress;

+ 10 - 11
src/utils/phaseSvgIcons.js

@@ -1,15 +1,14 @@
-// 自动收集 src/assets/images/svg/icon_*.svg, 导出 token -> url 映射。
+// 兼容层: 让旧代码 `import { SVG_MAP } from '@/utils/phaseSvgIcons'` 继续工作
 //
-// token 取自文件名: icon_straight_up.svg -> 'STRAIGHT_UP'
-//                  icon_turn_up_left_uturn.svg -> 'TURN_UP_LEFT_UTURN'
+// 旧实现用 require.context 自动收集 svg/ 下所有 icon_*.svg。
+// 现在改为从 phaseIconAssets 派生, 资源管理统一收敛到一处。
 //
-// 用 require.context 后续往 svg/ 里加新文件零代码自动可用。
-// 与 SignalTimingChart 的 IMAGE_MAP (PNG 版) 命名口径完全一致。
+// 新代码建议直接 import { getTokenSvg } from '@/config/phaseIconAssets'
 
-const ctx = require.context('@/assets/images/svg', false, /^\.\/icon_.*\.svg$/);
+import { TOKEN_ASSETS } from '@/config/phaseIconAssets';
 
-export const SVG_MAP = ctx.keys().reduce((acc, key) => {
-  const m = key.match(/icon_(.+)\.svg$/);
-  if (m) acc[m[1].toUpperCase()] = ctx(key);
-  return acc;
-}, {});
+export const SVG_MAP = Object.fromEntries(
+  Object.entries(TOKEN_ASSETS)
+    .filter(([, a]) => a && a.svg)
+    .map(([token, a]) => [token, a.svg])
+);