diff --git a/src/components/svgs/lines/paths/diagonal.tsx b/src/components/svgs/lines/paths/diagonal.tsx index 6c9a5dd3d..665fbd231 100644 --- a/src/components/svgs/lines/paths/diagonal.tsx +++ b/src/components/svgs/lines/paths/diagonal.tsx @@ -1,7 +1,7 @@ -import { Button, Text } from '@chakra-ui/react'; +import { Button } from '@chakra-ui/react'; import { RmgFields, RmgFieldsField } from '@railmapgen/rmg-components'; import { useTranslation } from 'react-i18next'; -import { LineId, MiscNodeId, StnId } from '../../../../constants/constants'; +import { LineId } from '../../../../constants/constants'; import { LinePath, LinePathAttributes, @@ -9,9 +9,9 @@ import { LinePathType, PathGenerator, } from '../../../../constants/lines'; -import { useRootDispatch, useRootSelector } from '../../../../redux'; +import { useRootDispatch } from '../../../../redux'; import { setSelected } from '../../../../redux/runtime/runtime-slice'; -import { getBaseParallelLineID, makeParallelIndex } from '../../../../util/parallel'; +import { getBaseParallelLineID } from '../../../../util/parallel'; import { roundPathCorners } from '../../../../util/pathRounding'; const generateDiagonalPath: PathGenerator = ( diff --git a/src/components/svgs/lines/paths/perpendicular.tsx b/src/components/svgs/lines/paths/perpendicular.tsx index 86a60168d..e754065fa 100644 --- a/src/components/svgs/lines/paths/perpendicular.tsx +++ b/src/components/svgs/lines/paths/perpendicular.tsx @@ -1,7 +1,7 @@ import { Button } from '@chakra-ui/react'; import { RmgFields, RmgFieldsField } from '@railmapgen/rmg-components'; import { useTranslation } from 'react-i18next'; -import { LineId, MiscNodeId, StnId } from '../../../../constants/constants'; +import { LineId } from '../../../../constants/constants'; import { LinePath, LinePathAttributes, @@ -9,9 +9,9 @@ import { LinePathType, PathGenerator, } from '../../../../constants/lines'; -import { useRootDispatch, useRootSelector } from '../../../../redux'; +import { useRootDispatch } from '../../../../redux'; import { setSelected } from '../../../../redux/runtime/runtime-slice'; -import { getBaseParallelLineID, makeParallelIndex } from '../../../../util/parallel'; +import { getBaseParallelLineID } from '../../../../util/parallel'; import { roundPathCorners } from '../../../../util/pathRounding'; const generatePerpendicularPath: PathGenerator = ( diff --git a/src/util/auto-simple.ts b/src/util/auto-simple.ts index f20a3b149..425cbdd58 100644 --- a/src/util/auto-simple.ts +++ b/src/util/auto-simple.ts @@ -1,4 +1,3 @@ -import { LineId } from '../constants/constants'; import { ExternalLinePathAttributes, LinePathType } from '../constants/lines'; /** @@ -16,8 +15,7 @@ export const checkSimplePathAvailability = ( y1: number, x2: number, y2: number, - attrs: NonNullable, - parallelIndex: number + attrs: NonNullable ): { x1: number; y1: number; x2: number; y2: number; offset: number } | undefined => { // Check if offsetFrom and offsetTo are defined and are numbers. if (!('offsetFrom' in attrs) || !('offsetTo' in attrs)) return; @@ -27,24 +25,7 @@ export const checkSimplePathAvailability = ( // It is just parallel to the line from (x1,y1) to (x2,y2). if (attrs['offsetFrom'] === attrs['offsetTo']) { if (checkKAndType(type, x1, y1, x2, y2)) { - if (parallelIndex < 0) { - return { x1, y1, x2, y2, offset: attrs['offsetFrom'] }; - } - - // auto parallel instead of manual offset tweaks when parallelIndex >= 0 (enabled) - if (x1 === x2) { - return { x1: x1 + 5 * parallelIndex, y1, x2: x2 + 5 * parallelIndex, y2, offset: attrs['offsetFrom'] }; - } - if (y1 === y2) { - return { x1, y1: y1 + 5 * parallelIndex, x2, y2: y2 + 5 * parallelIndex, offset: attrs['offsetFrom'] }; - } - return { - x1: x1 + 5 * Math.SQRT1_2 * parallelIndex, - y1: y1 + 5 * Math.SQRT1_2 * parallelIndex, - x2: x2 + 5 * Math.SQRT1_2 * parallelIndex, - y2: y2 + 5 * Math.SQRT1_2 * parallelIndex, - offset: attrs['offsetFrom'], - }; + return { x1, y1, x2, y2, offset: attrs['offsetFrom'] }; } return; } @@ -74,6 +55,32 @@ export const checkSimplePathAvailability = ( } }; +/** + * Auto parallel instead of manual offset tweaks when parallelIndex > 0 (non base parallel line). + */ +export const reconcileSimplePathWithParallel = ( + x1: number, + y1: number, + x2: number, + y2: number, + offset: number, + parallelIndex: number +) => { + if (x1 === x2) { + return { x1: x1 + 5 * parallelIndex, y1, x2: x2 + 5 * parallelIndex, y2, offset }; + } + if (y1 === y2) { + return { x1, y1: y1 + 5 * parallelIndex, x2, y2: y2 + 5 * parallelIndex, offset }; + } + return { + x1: x1 + 5 * Math.SQRT1_2 * parallelIndex, + y1: y1 + 5 * Math.SQRT1_2 * parallelIndex, + x2: x2 + 5 * Math.SQRT1_2 * parallelIndex, + y2: y2 + 5 * Math.SQRT1_2 * parallelIndex, + offset, + }; +}; + /** * Check k and type to see if this combination matches the simple path rule for this line path type. * 1. k = 0 or ∞(parallel and vertical) for Diagonal and Perpendicular. diff --git a/src/util/parallel.ts b/src/util/parallel.ts index de7027005..1685276db 100644 --- a/src/util/parallel.ts +++ b/src/util/parallel.ts @@ -12,21 +12,26 @@ export type NonSimpleLinePathAttributes = NonNullable< const MIN_ROUND_CORNER_FACTOR = 1; -export const extractParallelLines = ( +/** + * Classify all the lines between source and target of the provided line + * into lines that should be parallel to the provided line and lines that should not. + * Based on parallelIndex, type, and startFrom of the provided line. + * @param graph The graph. + * @param lineEntry The line entry. + * @returns An object containing normal and parallel lines. + */ +export const classifyParallelLines = ( graph: MultiDirectedGraph, lineEntry: EdgeEntry ) => { - const lineID = lineEntry.edge as LineId; - const type = lineEntry.attributes.type; - const attr = lineEntry.attributes[type]; - const parallelIndex = lineEntry.attributes.parallelIndex; - + const { type, parallelIndex } = lineEntry.attributes; + // TODO: might be redundant to check this if (type === LinePathType.Simple || parallelIndex < 0) { return { normal: [lineEntry], parallel: [] }; } + const { source, target } = lineEntry; const normal: EdgeEntry[] = []; - const [source, target] = graph.extremities(lineID); const parallelLines: EdgeEntry[] = []; for (const lineEntry of graph.edgeEntries(source, target)) { if (lineEntry.attributes.parallelIndex < 0) { @@ -34,20 +39,8 @@ export const extractParallelLines = ( continue; } - // edgeEntries will also return edges from target to source - if ( - lineEntry.attributes.type === type && - source === lineEntry.source && - (lineEntry.attributes[type] as NonSimpleLinePathAttributes).startFrom === - (attr as NonSimpleLinePathAttributes).startFrom - ) { - parallelLines.push(lineEntry); - } else if ( - lineEntry.attributes.type === type && - source === lineEntry.target && - (lineEntry.attributes[type] as NonSimpleLinePathAttributes).startFrom !== - (attr as NonSimpleLinePathAttributes).startFrom - ) { + const { startFrom } = lineEntry.attributes[type] as NonSimpleLinePathAttributes; + if (checkParallels(type, source as StnId | MiscNodeId, startFrom, lineEntry)) { parallelLines.push(lineEntry); } } @@ -135,6 +128,40 @@ export const makeParallelPaths = (parallelLines: EdgeEntry +) => { + const lineEntryType = lineEntry.attributes.type; + if ( + type === lineEntry.attributes.type && + source === lineEntry.source && + startFrom === (lineEntry.attributes[lineEntryType] as NonSimpleLinePathAttributes).startFrom + ) { + return true; + } else if ( + // edgeEntries will also return edges from target to source + type === lineEntry.attributes.type && + source === lineEntry.target && + startFrom !== (lineEntry.attributes[lineEntryType] as NonSimpleLinePathAttributes).startFrom + ) { + return true; + } + return false; +}; + export const makeParallelIndex = ( graph: MultiDirectedGraph, type: LinePathType, @@ -147,19 +174,7 @@ export const makeParallelIndex = ( // find all parallel lines that are either (source, target, from) or (target, source, to) const existingParallelIndex: number[] = []; for (const lineEntry of graph.edgeEntries(source, target)) { - const attr = lineEntry.attributes; - if ( - type === attr.type && - source === lineEntry.source && - (attr[type] as NonSimpleLinePathAttributes).startFrom === startFrom - ) { - existingParallelIndex.push(lineEntry.attributes.parallelIndex); - } else if ( - type === attr.type && - // edgeEntries will also return edges from target to source - source === lineEntry.target && - (attr[type] as NonSimpleLinePathAttributes).startFrom !== startFrom - ) { + if (checkParallels(type, source, startFrom, lineEntry)) { existingParallelIndex.push(lineEntry.attributes.parallelIndex); } } @@ -187,16 +202,16 @@ export const getBaseParallelLineID = ( graph: MultiDirectedGraph, type: LinePathType, lineID: LineId -) => { +): LineId => { if (type === LinePathType.Simple) return lineID; const parallelIndex = graph.getEdgeAttribute(lineID, 'parallelIndex'); if (parallelIndex < 0) return lineID; - const startFrom = graph.getEdgeAttribute(lineID, type)!['startFrom']; + const { startFrom } = graph.getEdgeAttribute(lineID, type)!; const [source, target] = graph.extremities(lineID); - let minParallelIndex = Number.MAX_VALUE; + let minParallelIndex = parallelIndex; let minLineID = lineID; for (const lineEntry of graph.edgeEntries(source, target)) { const attr = lineEntry.attributes; @@ -222,7 +237,7 @@ export const getBaseParallelLineID = ( minLineID = lineEntry.edge as LineId; } } - return minParallelIndex == parallelIndex ? lineID : minLineID; + return minLineID; }; export const MAX_PARALLEL_LINES_FREE = 5; diff --git a/src/util/process-elements.ts b/src/util/process-elements.ts index 210ad3a8a..6522f06e7 100644 --- a/src/util/process-elements.ts +++ b/src/util/process-elements.ts @@ -3,8 +3,8 @@ import { EdgeEntry } from 'graphology-types'; import { linePaths } from '../components/svgs/lines/lines'; import { EdgeAttributes, GraphAttributes, Id, LineId, MiscNodeId, NodeAttributes, StnId } from '../constants/constants'; import { ExternalLinePathAttributes, LinePathType, Path } from '../constants/lines'; -import { checkSimplePathAvailability } from './auto-simple'; -import { extractParallelLines, makeParallelPaths } from './parallel'; +import { checkSimplePathAvailability, reconcileSimplePathWithParallel } from './auto-simple'; +import { classifyParallelLines, getBaseParallelLineID, makeParallelPaths } from './parallel'; import { makeReconciledPath, reconcileLines } from './reconcile'; /** @@ -29,7 +29,6 @@ export const getNodes = (graph: MultiDirectedGraph): Element[] => { const resolvedLines: { [k in LineId]: LinePathElement } = {}; + const cachedSimplePathAvailability: { [k in LineId]: ReturnType } = {}; const parallelLines: EdgeEntry[] = []; const lineGroupsToReconcile: { [reconcileId: string]: EdgeEntry[] } = {}; - // const reconciledLines: EdgeEntry[] = []; const normalLines: EdgeEntry[] = []; + + // Check and cache all the lines if they can be a simple path. for (const lineEntry of graph.edgeEntries()) { const [x1, y1, x2, y2] = [ lineEntry.sourceAttributes.x, @@ -49,28 +50,36 @@ export const getLines = (graph: MultiDirectedGraph= 0) { - parallelLines.push(lineEntry); - continue; + const attr = lineEntry.attributes[lineEntry.attributes.type] as NonNullableExternalLinePathAttribute; + const simplePathAvailability = checkSimplePathAvailability(lineEntry.attributes.type, x1, y1, x2, y2, attr); + cachedSimplePathAvailability[lineEntry.edge as LineId] = simplePathAvailability; + } + + // Generalize all the lines into parallel, reconcile, simple, and normal lines. + for (const lineEntry of graph.edgeEntries()) { + let simplePathAvailability = cachedSimplePathAvailability[lineEntry.edge as LineId]; + + const { parallelIndex } = lineEntry.attributes; + if (parallelIndex >= 0) { + // only find the base parallel line and see if it is a simple path + const baseLineId = getBaseParallelLineID(graph, lineEntry.attributes.type, lineEntry.edge as LineId); + const baseSimplePathAvailability = cachedSimplePathAvailability[baseLineId]; + if (!baseSimplePathAvailability) { + parallelLines.push(lineEntry); + continue; + } + // here is the line that should enable auto simple + // no parallel involved, just add some offset to the simple path + // based on the parallelIndex and make it looks like parallel + if (parallelIndex > 0) { + const { x1, y1, x2, y2, offset } = baseSimplePathAvailability; + simplePathAvailability = reconcileSimplePathWithParallel(x1, y1, x2, y2, offset, parallelIndex); + } } if (lineEntry.attributes.reconcileId !== '') { const reconcileId = lineEntry.attributes.reconcileId; if (reconcileId in lineGroupsToReconcile) lineGroupsToReconcile[reconcileId].push(lineEntry); else lineGroupsToReconcile[reconcileId] = [lineEntry]; - // reconciledLines.push(lineEntry); continue; } if (simplePathAvailability) { @@ -79,7 +88,6 @@ export const getLines = (graph: MultiDirectedGraph = new Set(); - + // Handle parallel lines. + const resolvedParallelLinesID: Set = new Set(); while (parallelLines.length) { const lineEntry = parallelLines.pop()!; - if (resolvedLinesID.has(lineEntry.edge as LineId)) continue; + if (resolvedParallelLinesID.has(lineEntry.edge as LineId)) continue; - const { normal, parallel } = extractParallelLines(graph, lineEntry); - if (!parallel.length) continue; - parallel.forEach(_ => resolvedLinesID.add(_.edge as LineId)); - normalLines.push(...normal); + // find all the parallel lines between source and target from lineEntry + // `normal` are dropped as they are already handled in normalLines + const { parallel: parallels } = classifyParallelLines(graph, lineEntry); + if (!parallels.length) continue; + parallels.forEach(_ => resolvedParallelLinesID.add(_.edge as LineId)); - const parallelPaths = makeParallelPaths(parallel); - for (const lineEntry of parallel) { - const lineID = lineEntry.edge as LineId; + const parallelPaths = makeParallelPaths(parallels); + for (const parallel of parallels) { + const lineID = parallel.edge as LineId; resolvedLines[lineID] = { - id: lineEntry.edge as LineId, - attr: lineEntry.attributes, + attr: parallel.attributes, path: parallelPaths[lineID], }; } } + // Handle reconcile lines. const { allReconciledLines, danglingLines } = reconcileLines(graph, lineGroupsToReconcile); for (const reconciledLine of allReconciledLines) { const path = makeReconciledPath(graph, reconciledLine); if (!path) continue; const lineID = reconciledLine[0]; - resolvedLines[lineID] = { id: lineID, attr: graph.getEdgeAttributes(lineID), path }; + resolvedLines[lineID] = { attr: graph.getEdgeAttributes(lineID), path }; } for (const danglingLine of danglingLines) { const attr = graph.getEdgeAttributes(danglingLine); @@ -123,7 +132,6 @@ export const getLines = (graph: MultiDirectedGraph ({ id: _.id, type: 'line', line: _ })); + return Object.entries(resolvedLines).map(([id, line]) => ({ id: id as LineId, type: 'line', line })); }; diff --git a/src/util/reconcile.ts b/src/util/reconcile.ts index a10bec83d..d93b7f9a3 100644 --- a/src/util/reconcile.ts +++ b/src/util/reconcile.ts @@ -3,7 +3,7 @@ import { EdgeEntry } from 'graphology-types'; import { linePaths } from '../components/svgs/lines/lines'; import { EdgeAttributes, GraphAttributes, LineId, NodeAttributes } from '../constants/constants'; import { LinePathType, Path } from '../constants/lines'; -import { checkSimplePathAvailability } from './auto-simple'; +import { checkSimplePathAvailability, reconcileSimplePathWithParallel } from './auto-simple'; /** * Only lines have a reconcileId will be considered. @@ -124,17 +124,17 @@ export const makeReconciledPath = ( const [source, target] = graph.extremities(line); const sourceAttr = graph.getNodeAttributes(source); const targetAttr = graph.getNodeAttributes(target); - const { type, parallelIndex } = graph.getEdgeAttributes(line); + const { type } = graph.getEdgeAttributes(line); const attr = graph.getEdgeAttribute(line, type) ?? linePaths[type].defaultAttrs; + // TODO: disable parallel on reconciled lines, use offsetFrom to offsetTo instead const simplePathAvailability = checkSimplePathAvailability( type, sourceAttr.x, sourceAttr.y, targetAttr.x, targetAttr.y, - attr, - parallelIndex + attr ); if (simplePathAvailability) { // simple path hook on matched situation