Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/components/side-panel/style-side-panel/design-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
setTheme,
staggerStationNames,
toggleLineNameBeforeDestination,
setShmetro2020BranchDistanceFactor,
setShmetro2020BranchFirstStationOffset,
setShmetro2020BranchBendType,
setShmetro2020BranchAlignEndpoints,
} from '../../../redux/param/param-slice';
import { PanelTypeGZMTR, PanelTypeShmetro, PsdLabel, RmgStyle, ShortDirection } from '../../../constants/constants';
import { MdSwapVert } from 'react-icons/md';
Expand Down Expand Up @@ -45,6 +49,17 @@ export default function DesignSection() {
info_panel_type,
stn_list,
loop,
shmetro2020_info: {
branch_distance_factor: branchDistanceFactor,
branch_first_station_offset: branchFirstStationOffset,
branch_bend_type: branchBendType,
branch_align_endpoints: branchAlignEndpoints,
} = {
branch_distance_factor: 1,
branch_first_station_offset: 0,
branch_bend_type: 'rightangle' as const,
branch_align_endpoints: false,
},
} = useRootSelector(state => state.param);

const lineServices = Math.max(...Object.values(stn_list).map(s => s.services.length));
Expand Down Expand Up @@ -226,6 +241,52 @@ export default function DesignSection() {
oneLine: true,
hidden: ![RmgStyle.SHMetro].includes(style) || lineServices > 1 || loop,
},
{
type: 'slider',
label: t('StyleSidePanel.design.branchDistanceFactor'),
value: branchDistanceFactor,
min: 1,
max: 5,
step: 0.05,
onChange: value => {
dispatch(setShmetro2020BranchDistanceFactor(value));
},
hidden: ![RmgStyle.SHMetro].includes(style) || info_panel_type !== 'sh2020' || loop,
},
{
type: 'slider',
label: t('StyleSidePanel.design.branchFirstStationOffset'),
value: branchFirstStationOffset,
min: 0,
max: 5,
step: 0.05,
onChange: value => {
dispatch(setShmetro2020BranchFirstStationOffset(value));
},
hidden: ![RmgStyle.SHMetro].includes(style) || info_panel_type !== 'sh2020' || loop,
},
{
type: 'select',
label: t('StyleSidePanel.design.branchBendType'),
value: branchBendType,
options: {
rightangle: t('StyleSidePanel.design.branchBendRightAngle'),
'45degree': t('StyleSidePanel.design.branchBendDiagonal'),
},
onChange: value => {
dispatch(setShmetro2020BranchBendType(value as 'rightangle' | '45degree'));
},
hidden: ![RmgStyle.SHMetro].includes(style) || info_panel_type !== 'sh2020' || loop,
},
{
type: 'switch',
label: t('StyleSidePanel.design.branchAlignEndpoints'),
isChecked: branchAlignEndpoints,
onChange: value => {
dispatch(setShmetro2020BranchAlignEndpoints(value));
},
hidden: ![RmgStyle.SHMetro].includes(style) || info_panel_type !== 'sh2020' || loop,
},
];

const staggerNameSelections = [
Expand Down
25 changes: 25 additions & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,31 @@ export interface RMGParam {
midpoint_station?: string;
clockwise?: boolean;
};
/**
* SHMetro 2020 style branch layout control.
*/
shmetro2020_info: {
/**
* Factor for the horizontal distance from bifurcation point to first branch station.
* Default is 1, meaning normal spacing.
*/
branch_distance_factor: number;
/**
* Horizontal offset from bifurcation point where the vertical turn begins.
* Default is 0, meaning turn starts at bifurcation point.
*/
branch_first_station_offset: number;
/**
* Type of bend at bifurcation: 'rightangle' (90°) or '45degree' (45°).
* Default is 'rightangle'.
*/
branch_bend_type: 'rightangle' | '45degree';
/**
* Whether to align shorter branch endpoints to match the longer parallel branch.
* Default is false.
*/
branch_align_endpoints: boolean;
};
Comment on lines +286 to +310
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

通常来说我们希望所有的属性都是通用的,虽然可能现在非2020及其他风格不会用到对应的数据,但正像我们在loop中实现的那样(比如bank),尽可能地通用这些属性值和名,以便后续使用

另外它也应该和branchSpacingPct一起被整合在branch键下

version?: string; // RMG version
}

Expand Down
8 changes: 7 additions & 1 deletion src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,13 @@
"legacyDestination": "Display line name on direction sign",
"overrideTerminal": "Override terminal",
"terminalZhName": "Terminal Chinese name",
"terminalEnName": "Terminal English name"
"terminalEnName": "Terminal English name",
"branchDistanceFactor": "Branch horizontal distance factor",
"branchFirstStationOffset": "Branch horizontal turn offset",
"branchBendType": "Branch bend type",
"branchBendRightAngle": "Right angle (90°)",
"branchBendDiagonal": "Diagonal (45°)",
"branchAlignEndpoints": "Align branch endpoints"
},
"note": {
"title": "Notes",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@
"legacyDestination": "終着駅表示板に路線名を表示",
"overrideTerminal": "終着駅を上書き",
"terminalZhName": "終着駅の日本語名",
"terminalEnName": "終着駅の英語名"
"terminalEnName": "終着駅の英語名",
"branchDistanceFactor": "分岐水平距離倍率",
"branchFirstStationOffset": "分岐水平曲がりオフセット",
"branchBendType": "分岐カーブ形状",
"branchBendRightAngle": "直角 (90°)",
"branchBendDiagonal": "斜め (45°)",
"branchAlignEndpoints": "分岐端点を揃える"
},
"note": {
"title": "備考",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,13 @@
"legacyDestination": "터미널에 경로 명칭 표시",
"overrideTerminal": "끝점 다시 쓰기",
"terminalZhName": "터미널 한자 명칭",
"terminalEnName": "터미널 영문 명칭"
"terminalEnName": "터미널 영문 명칭",
"branchDistanceFactor": "분기 수평 거리 배수",
"branchFirstStationOffset": "분기 수평 회전 오프셋",
"branchBendType": "분기 곡선 유형",
"branchBendRightAngle": "직각 (90°)",
"branchBendDiagonal": "대각선 (45°)",
"branchAlignEndpoints": "분기 끝점 정렬"
},
"note": {
"title": "설명",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@
"legacyDestination": "在终点站牌显示路线名称",
"overrideTerminal": "重写终点站",
"terminalZhName": "终点站中文名称",
"terminalEnName": "终点站英文名称"
"terminalEnName": "终点站英文名称",
"branchDistanceFactor": "分支水平间距倍数",
"branchFirstStationOffset": "分支水平转弯偏移量",
"branchBendType": "分支弯道类型",
"branchBendRightAngle": "直角 (90°)",
"branchBendDiagonal": "斜线 (45°)",
"branchAlignEndpoints": "对齐分支端点"
},
"note": {
"title": "备注",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@
"legacyDestination": "於終點站牌顯示路綫名稱",
"overrideTerminal": "覆寫終點站",
"terminalZhName": "終點站中文名稱",
"terminalEnName": "終點站英文名稱"
"terminalEnName": "終點站英文名稱",
"branchDistanceFactor": "分支水平間距倍數",
"branchFirstStationOffset": "分支水平轉彎偏移量",
"branchBendType": "分支彎道類型",
"branchBendRightAngle": "直角 (90°)",
"branchBendDiagonal": "斜線 (45°)",
"branchAlignEndpoints": "對齊分支端點"
},
"note": {
"title": "備註",
Expand Down
20 changes: 20 additions & 0 deletions src/redux/param/param-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ const paramSlice = createSlice({
state.loop_info.clockwise = action.payload;
},

setShmetro2020BranchDistanceFactor: (state, action: PayloadAction<number>) => {
state.shmetro2020_info!.branch_distance_factor = action.payload;
},

setShmetro2020BranchFirstStationOffset: (state, action: PayloadAction<number>) => {
state.shmetro2020_info!.branch_first_station_offset = action.payload;
},

setShmetro2020BranchBendType: (state, action: PayloadAction<'rightangle' | '45degree'>) => {
state.shmetro2020_info!.branch_bend_type = action.payload;
},

setShmetro2020BranchAlignEndpoints: (state, action: PayloadAction<boolean>) => {
state.shmetro2020_info!.branch_align_endpoints = action.payload;
},

setCurrentStation: (state, action: PayloadAction<string>) => {
state.current_stn_idx = action.payload;
},
Expand Down Expand Up @@ -211,6 +227,10 @@ export const {
setLoopBottomFactor,
setLoopMidpointStation,
setLoopClockwise,
setShmetro2020BranchDistanceFactor,
setShmetro2020BranchFirstStationOffset,
setShmetro2020BranchBendType,
setShmetro2020BranchAlignEndpoints,
setCurrentStation,
setStation,
setStations,
Expand Down
141 changes: 141 additions & 0 deletions src/svgs/methods/share.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { drawLine, getStnState } from './share';

// Diamond topology used in getStnState tests:
//
// C -- D
// / \
// A -- B E -- F
// \ /
// G -- H
//
// routes[0]: linestart → A → B → C → D → E → F → lineend
// routes[1]: linestart → A → B → G → H → E → F → lineend

const TWO_BRANCH_ROUTES: string[][] = [
['linestart', 'A', 'B', 'C', 'D', 'E', 'F', 'lineend'],
['linestart', 'A', 'B', 'G', 'H', 'E', 'F', 'lineend'],
];

// Dead-end topology:
//
// <-- a ---|
// c --- d ----
// <-- b ---|

const DEAD_END_ROUTES: string[][] = [
['linestart', 'b', 'c', 'd', 'lineend'],
['linestart', 'a', 'c', 'd', 'lineend'],
];

describe('getStnState', () => {
it('Can calculate states on simple line (direction r)', () => {
const routes = [['linestart', 'A', 'B', 'C', 'lineend']];
const states = getStnState('B', routes, 'r');
expect(states['A']).toBe(-1);
expect(states['B']).toBe(0);
expect(states['C']).toBe(1);
});

it('Can calculate states on simple line (direction l)', () => {
const routes = [['linestart', 'A', 'B', 'C', 'lineend']];
const states = getStnState('B', routes, 'l');
expect(states['A']).toBe(1);
expect(states['B']).toBe(0);
expect(states['C']).toBe(-1);
});

it('Can promote parallel branch when current is on sibling branch (direction r)', () => {
const states = getStnState('D', TWO_BRANCH_ROUTES, 'r');
expect(states['B']).toBe(-1);
expect(states['C']).toBe(-1);
expect(states['D']).toBe(0);
expect(states['E']).toBe(1);
expect(states['G']).toBe(1);
expect(states['H']).toBe(1);
});

it('Can promote parallel branch when current is on sibling branch (direction l)', () => {
const states = getStnState('D', TWO_BRANCH_ROUTES, 'l');
expect(states['B']).toBe(1);
expect(states['C']).toBe(1);
expect(states['D']).toBe(0);
expect(states['E']).toBe(-1);
expect(states['G']).toBe(1);
expect(states['H']).toBe(1);
});

it('Can calculate states at bifurcation point — both branches ahead', () => {
const states = getStnState('B', TWO_BRANCH_ROUTES, 'r');
expect(states['A']).toBe(-1);
expect(states['B']).toBe(0);
expect(states['C']).toBe(1);
expect(states['D']).toBe(1);
expect(states['E']).toBe(1);
expect(states['F']).toBe(1);
expect(states['G']).toBe(1);
expect(states['H']).toBe(1);
});

it('Can calculate states at merge point — both branches behind', () => {
const states = getStnState('E', TWO_BRANCH_ROUTES, 'r');
expect(states['A']).toBe(-1);
expect(states['B']).toBe(-1);
expect(states['C']).toBe(-1);
expect(states['D']).toBe(-1);
expect(states['E']).toBe(0);
expect(states['F']).toBe(1);
expect(states['G']).toBe(-1);
expect(states['H']).toBe(-1);
});

it('Does not promote dead-end parallel branch when junction is behind current', () => {
const states = getStnState('a', DEAD_END_ROUTES, 'l');
expect(states['a']).toBe(0);
expect(states['c']).toBe(-1);
expect(states['d']).toBe(-1);
expect(states['b']).toBe(-1);
});

it('Can promote dead-end parallel branch when junction is ahead of current', () => {
const states = getStnState('a', DEAD_END_ROUTES, 'r');
expect(states['a']).toBe(0);
expect(states['c']).toBe(1);
expect(states['d']).toBe(1);
expect(states['b']).toBe(1);
});
});

describe('drawLine', () => {
it('Can color promoted parallel branch stations', () => {
const stnStates: { [k: string]: -1 | 0 | 1 } = { B: -1, G: 1, H: 1, E: 1 };
const { main, pass } = drawLine(['linestart', 'B', 'G', 'H', 'E', 'lineend'], stnStates);
expect(main).toContain('G');
expect(main).toContain('H');
expect(main).toContain('E');
expect(pass).toContain('B');
});

it('Does not generate gray stub when entire branch is state=1', () => {
const stnStates: { [k: string]: -1 | 0 | 1 } = { X: 1, Y: 1, Z: 1 };
const { main, pass } = drawLine(['linestart', 'X', 'Y', 'Z', 'lineend'], stnStates);
expect(main).toEqual(['X', 'Y', 'Z']);
expect(pass).toHaveLength(0);
});

it('Current station (state=0) appears in both main and pass as overlap point', () => {
const stnStates: { [k: string]: -1 | 0 | 1 } = { B: -1, G: 0, H: 1, E: 1 };
const { main, pass } = drawLine(['linestart', 'B', 'G', 'H', 'E', 'lineend'], stnStates);
expect(pass).toContain('B');
expect(pass).toContain('G');
expect(main).toContain('G');
expect(main).toContain('H');
expect(main).toContain('E');
});

it('Returns empty main and full pass when entire branch is state=-1', () => {
const stnStates: { [k: string]: -1 | 0 | 1 } = { B: -1, G: -1, H: -1, E: -1 };
const { main, pass } = drawLine(['linestart', 'B', 'G', 'H', 'E', 'lineend'], stnStates);
expect(main).toHaveLength(0);
expect(pass).toEqual(['B', 'G', 'H', 'E']);
});
});
Loading