Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@reduxjs/toolkit": "^2.9.0",
"bezier-js": "^6.1.4",
"canvas-size": "^2.0.0",
"expr-eval": "^2.0.2",
"graphology": "^0.26.0",
"graphology-utils": "^2.5.2",
"mime-types": "^3.0.1",
Expand Down
13 changes: 13 additions & 0 deletions src/components/page-header/master-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { isSupportedMasterVersion } from './master-import';

describe('isSupportedMasterVersion', () => {
it('accepts v2, v3 and v4 masters only', () => {
expect(isSupportedMasterVersion(2)).toBe(true);
expect(isSupportedMasterVersion(3)).toBe(true);
expect(isSupportedMasterVersion(4)).toBe(true);
expect(isSupportedMasterVersion(undefined)).toBe(false);
expect(isSupportedMasterVersion(1)).toBe(false);
expect(isSupportedMasterVersion(5)).toBe(false);
});
});
6 changes: 5 additions & 1 deletion src/components/page-header/master-import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const defaultMasterSelected: MasterTypeList = {
fg: MonoColour.white,
};

export const isSupportedMasterVersion = (version: unknown) => {
return typeof version === 'number' && version >= 2 && version <= 4;
};

const styles: SystemStyleObject = {
h: '80%',
w: '80%',
Expand Down Expand Up @@ -111,7 +115,7 @@ export const MasterImport = (props: {
core: p.core,
version: p.version,
};
if (!param.version || param.version < 2) {
if (!isSupportedMasterVersion(param.version)) {
toast({
title: 'Outdated configuration!',
status: 'error' as const,
Comment thread
langonginc marked this conversation as resolved.
Expand Down
28 changes: 15 additions & 13 deletions src/components/page-header/master-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,40 +54,42 @@ export const MasterManager = (props: { isOpen: boolean; onClose: () => void }) =
const attrs = structuredClone(nodeAttrs[MiscNodeType.Master]!);

const getComponentValue = (query: string) => {
attrs.components.forEach(c => {
if (c.id === query) {
return c.value ?? c.defaultValue;
}
});
return undefined;
const component = attrs.components.find(c => c.id === query);
return component ? (component.value ?? component.defaultValue) : undefined;
};

newParam.components.forEach((c, i) => {
newParam.components[i].value = getComponentValue(c.id) ?? c.defaultValue;
});
if (newParam.color !== undefined)
newParam.color.value = attrs.color ? newParam.color.value : newParam.color.defaultValue;
newParam.color.value = attrs.color
? (attrs.color.value ?? attrs.color.defaultValue)
: newParam.color.defaultValue;
graph.current.mergeNodeAttributes(node, { [MiscNodeType.Master]: newParam });
});
dispatch(saveGraph(graph.current.export()));
dispatch(refreshNodesThunk());
};

const handleDownload = (p: MasterParam) => {
const param = {
const components = structuredClone(p.components);
components.forEach(c => {
c.value = c.defaultValue;
});

const param: Record<string, unknown> = {
id: p.randomId,
type: p.nodeType,
label: p.label,
svgs: p.svgs,
components: p.components,
color: p.color,
components,
core: p.core,
transform: p.transform,
version: p.version,
};
param.components.forEach((c, i) => {
param.components[i].value = c.defaultValue;
});
if (p.version !== 4 && p.color) {
param.color = { ...p.color, value: p.color.defaultValue };
}
downloadAs(`RMP_Master_Node_${new Date().valueOf()}.json`, 'application/json', JSON.stringify(param));
};

Expand Down
28 changes: 28 additions & 0 deletions src/components/svg-canvas-graph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { findConnectableTarget } from './svg-canvas-graph';

describe('findConnectableTarget', () => {
it('resolves a connectable target from an ancestor element', () => {
const core = document.createElementNS('http://www.w3.org/2000/svg', 'g');
core.setAttribute('id', 'stn_core_misc_node_target');

const child = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
core.appendChild(child);

expect(findConnectableTarget([child])).toEqual({
id: 'stn_core_misc_node_target',
matchedPrefix: 'stn_core_',
});
});

it('continues scanning later elements when the first element is not connectable', () => {
const nonConnectable = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const connectable = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
connectable.setAttribute('id', 'virtual_circle_misc_node_target');

expect(findConnectableTarget([nonConnectable, connectable])).toEqual({
id: 'virtual_circle_misc_node_target',
matchedPrefix: 'virtual_circle_',
});
});
});
41 changes: 30 additions & 11 deletions src/components/svg-canvas-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ const connectableNodesType = [
MiscNodeType.ChengduRTLineBadge,
MiscNodeType.GzmtrLineBadge,
];
const connectableTargetPrefixes = ['stn_core_', 'virtual_circle_', 'misc_node_connectable_'] as const;

export const findConnectableTarget = (elements: Element[]) => {
for (const element of elements) {
let current: Element | null = element;

while (current) {
const id = current.getAttribute('id');
const matchedPrefix = connectableTargetPrefixes.find(prefix => id?.startsWith(prefix));

if (id && matchedPrefix) {
return { id, matchedPrefix };
}

if (id === 'canvas') break;
current = current.parentElement;
}
}
};

const SvgCanvas = () => {
const dispatch = useRootDispatch();
Expand Down Expand Up @@ -325,27 +344,27 @@ const SvgCanvas = () => {
graph.current.hasNode(active) &&
connectableNodesType.includes(graph.current.getNodeAttribute(active, 'type'));

const prefixes = ['stn_core_', 'virtual_circle_', 'misc_node_connectable_'];
const elems = document.elementsFromPoint(e.clientX, e.clientY);
const id = elems.at(0)?.attributes?.getNamedItem('id')?.value;
// all connectable nodes have prefixes in their mask/event elements' ids
// also known as couldTargetBeConnected
const matchedPrefix = prefixes.find(prefix => id?.startsWith(prefix));
const target = findConnectableTarget(document.elementsFromPoint(e.clientX, e.clientY));

if (couldSourceBeConnected && matchedPrefix) {
if (couldSourceBeConnected && target) {
const { path, style: style_ } = getLinePathAndStyle(mode);
const [type, style] = [path!, style_!]; // assured by startsWith('line') check
const newLineId: LineId = `line_${nanoid(10)}`;
const [source, target] = [active! as NodeId, id!.slice(matchedPrefix.length) as NodeId];
if (source !== target) {
const [source, targetNode] = [
active! as NodeId,
target.id.slice(target.matchedPrefix.length) as NodeId,
];
if (source !== targetNode) {
const styleAttr = structuredClone(lineStyles[style].defaultAttrs);
// TODO: there should be some way for a style to disable auto theme injection
if ('color' in styleAttr && style !== LineStyleType.River) styleAttr.color = theme;
const parallelIndex =
autoParallel && supportsParallelLinePath(type)
? makeParallelIndex(graph.current, type, source, target, 'from')
? makeParallelIndex(graph.current, type, source, targetNode, 'from')
: -1;
graph.current.addDirectedEdgeWithKey(newLineId, source, target, {
graph.current.addDirectedEdgeWithKey(newLineId, source, targetNode, {
visible: true,
zIndex: 0,
type,
Expand All @@ -360,8 +379,8 @@ const SvgCanvas = () => {
if (autoChangeStationType && source.startsWith('stn')) {
checkAndChangeStationIntType(graph.current, source as StnId);
}
if (autoChangeStationType && target.startsWith('stn')) {
checkAndChangeStationIntType(graph.current, target as StnId);
if (autoChangeStationType && targetNode.startsWith('stn')) {
checkAndChangeStationIntType(graph.current, targetNode as StnId);
}

dispatch(setSelected(new Set([newLineId])));
Expand Down
Loading