diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index ec39aa3a9..27b925fa7 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -157,275 +157,93 @@
--- config: - layout: tidy-tree + layout: elk + flowchart: + curve: linear --- - mindmap - root((mindmap is a long thing)) - A - B - C - D --
- --- - config: - layout: tidy-tree - --- - mindmap - root((mindmap)) - A - B --
- --- - config: - layout: tidy-tree - --- - mindmap - root((mindmap)) - A - a - apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - b - c - d - B - apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - D - apa5[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] + flowchart LR + A[A] -- Mermaid js --> B[B] + A[A] -- Mermaid js --- B[B] + A@{ shape: diamond} + B@{ shape: diamond}-
-treemap -"Section 1" - "Leaf 1.1": 12 - "Section 1.2":::class1 - "Leaf 1.2.1": 12 -"Section 2" - "Leaf 2.1": 20:::class1 - "Leaf 2.2": 25 - "Leaf 2.3": 12 - -classDef class1 fill:red,color:blue,stroke:#FFD600; - - --
+ --- + config: + layout: elk + flowchart: + curve: rounded + --- + flowchart LR + D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"] + I --> D & D + D@{ shape: question} + I@{ shape: question} ++
+ --- + config: + layout: elk + flowchart: + curve: rounded + elk: + nodePlacementStrategy: NETWORK_SIMPLEX + --- + flowchart LR + D["Use the editor"] -- Mermaid js --> I["fa:fa-code Text"] + D --> I & I + a["a"] + D@{ shape: trap-b} + I@{ shape: lean-l} ++
--- config: - treemap: - valueFormat: '$0,0' + layout: elk + --- -treemap -"Budget" - "Operations" - "Salaries": 7000 - "Equipment": 2000 - "Supplies": 1000 - "Marketing" - "Advertising": 4000 - "Events": 1000 +flowchart LR + %% subgraph s1["Untitled subgraph"] + C["Evaluate"] + %% end -+ B --> C +
- treemap - title Accessible Treemap Title - "Category A" - "Item A1": 10 - "Item A2": 20 - "Category B" - "Item B1": 15 - "Item B2": 25 --
- --- - config: - layout: tidy-tree - --- - mindmap - root((mindmap)) - a - apa[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - apa2[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - b - apa3[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - apa4[I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on] - +--- +config: + layout: elk + flowchart: + //curve: linear +--- +flowchart LR +%% A ==> B +%% A2 --> B2 +A{A} --> B((Bo boo)) & B & B & B-
- --- - config: - layout: tidy-tree - --- - flowchart TB - A --> n0["1"] - A --> n1["2"] - A --> n2["3"] - A --> n3["4"] --> Q & R & S & T --
+--- config: layout: elk - --- - flowchart TB - A --> n0["1"] - A --> n1["2"] - A --> n2["3"] - A --> n3["4"] --> Q & R & S & T --- --- - config: - layout: dagre - --- - mindmap - root((mindmap is a long thing)) - Origins - Long history - ::icon(fa fa-book) - Popularisation - British popular psychology author Tony Buzan - Research - On effectiveness<br/>and features - On Automatic creation - Uses - Creative techniques - Strategic planning - Argument mapping - Tools - Pen and paper - Mermaid --- --- - config: - layout: cose-bilkent - --- - mindmap - root((mindmap)) - Origins - Long history - ::icon(fa fa-book) - Popularisation - British popular psychology author Tony Buzan - Research - On effectiveness<br/>and features - On Automatic creation - Uses - Creative techniques - Strategic planning - Argument mapping - Tools - Pen and paper - Mermaid --- --- - config: - layout: elk - --- - mindmap - root((mindmap)) - Origins - Long history - ::icon(fa fa-book) - Popularisation - British popular psychology author Tony Buzan - Research - On effectiveness<br/>and features - On Automatic creation - Uses - Creative techniques - Strategic planning - Argument mapping - Tools - Pen and paper - Mermaid --- --- - config: - layout: cose-bilkent + theme: default + look: classic --- flowchart LR - root{mindmap} --- Origins --- Europe - Origins --> Asia - root --- Background --- Rich - Background --- Poor - subgraph apa - Background - Poor - end - - - - + subgraph s1["APA"] + D{"Use the editor"} + end + subgraph S2["S2"] + s1 + I>"fa:fa-code Text"] + E["E"] + end + D -- Mermaid js --> I + D --> I & E + E --> I-- --- - config: - layout: elk - --- - flowchart LR - root{mindmap} --- Origins --- Europe - Origins --> Asia - root --- Background --- Rich - Background --- Poor - - - - --- flowchart - D(("for D")) --- flowchart LR - A e1@==> B - e1@{ animate: true} ---flowchart LR - A e1@--> B - classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round - class e1 animate --infinite
--flowchart LR - A e1@--> B - classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite; - class e1 animate --Mermaid - edge-animation-slow
--flowchart LR - A e1@--> B -e1@{ animation: fast} --Mermaid - edge-animation-fast
--flowchart LR - A e1@--> B - classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear; - class e1 edge-animation-fast -- -- -info-+--- config: layout: elk @@ -463,45 +281,7 @@ config: D-->I D-->I----- -config: - layout: elk ---- - flowchart LR - a - subgraph s0["APA"] - subgraph s8["APA"] - subgraph s1["APA"] - D{"X"} - E[Q] - end - subgraph s3["BAPA"] - F[Q] - I - end - D --> I - D --> I - D --> I - - I{"X"} - end - end ------ -config: - layout: elk ---- - flowchart LR - a - D{"Use the editor"} - - D -- Mermaid js --> I{"fa:fa-code Text"} - D-->I - D-->I --+--- config: layout: elk @@ -577,7 +357,7 @@ flowchart LR end-+--- config: layout: elk @@ -759,7 +539,7 @@ kanban // look: 'handDrawn', // 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX', // layout: 'dagre', - // layout: 'elk', + layout: 'elk', // layout: 'fixed', // htmlLabels: false, flowchart: { titleTopMargin: 10 }, diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts index 4a121f8a4..ff5bc19fc 100644 --- a/packages/mermaid-layout-elk/src/render.ts +++ b/packages/mermaid-layout-elk/src/render.ts @@ -1,11 +1,17 @@ +import type { + InternalHelpers, + LayoutData, + RenderOptions, + SVG, + SVGGroup, +} from '@mermaid-chart/mermaid'; +// @ts-ignore TODO: Investigate D3 issue import { curveLinear } from 'd3'; import ELK from 'elkjs/lib/elk.bundled.js'; -import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid'; import { type TreeData, findCommonAncestor } from './find-common-ancestor.js'; type Node = LayoutData['nodes'][number]; -// Used to calculate distances in order to avoid floating number rounding issues when comparing floating numbers -const epsilon = 0.0001; + interface LabelData { width: number; height: number; @@ -18,16 +24,7 @@ interface NodeWithVertex extends Omit{ labelData?: LabelData; domId?: Node['domId'] | SVGGroup | d3.Selection ; } -interface Point { - x: number; - y: number; -} -function distance(p1?: Point, p2?: Point): number { - if (!p1 || !p2) { - return 0; - } - return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); -} + export const render = async ( data4Layout: LayoutData, svg: SVG, @@ -61,30 +58,14 @@ export const render = async ( // Add the element to the DOM if (!node.isGroup) { - // Create a clean node object for ELK with only the properties it expects - const child: NodeWithVertex = { - id: node.id, - width: node.width, - height: node.height, - // Store the original node data for later use - label: node.label, - isGroup: node.isGroup, - shape: node.shape, - padding: node.padding, - cssClasses: node.cssClasses, - cssStyles: node.cssStyles, - look: node.look, - // Include parentId for subgraph processing - parentId: node.parentId, - }; + const child = node as NodeWithVertex; graph.children.push(child); - nodeDb[node.id] = child; + nodeDb[node.id] = node; const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir }); const boundingBox = childNodeEl.node()!.getBBox(); // Store the domId separately for rendering, not in the ELK graph child.domId = childNodeEl; - child.calcIntersect = node.calcIntersect; child.width = boundingBox.width; child.height = boundingBox.height; } else { @@ -93,7 +74,9 @@ export const render = async ( ...node, children: [], }; + // Let elk render with the copy graph.children.push(child); + // Save the original containing the intersection function nodeDb[node.id] = child; await addVertices(nodeEl, nodeArr, child, node.id); @@ -168,7 +151,7 @@ export const render = async ( height: node.height, }; if (node.isGroup) { - log.debug('id abc88 subgraph = ', node.id, node.x, node.y, node.labelData); + log.debug('Id abc88 subgraph = ', node.id, node.x, node.y, node.labelData); const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph'); // TODO use faster way of cloning const clusterNode = JSON.parse(JSON.stringify(node)); @@ -177,10 +160,10 @@ export const render = async ( clusterNode.width = Math.max(clusterNode.width, node.labelData.width); await insertCluster(subgraphEl, clusterNode); - log.debug('id (UIO)= ', node.id, node.width, node.shape, node.labels); + log.debug('Id (UIO)= ', node.id, node.width, node.shape, node.labels); } else { log.info( - 'id NODE = ', + 'Id NODE = ', node.id, node.x, node.y, @@ -284,7 +267,6 @@ export const render = async ( const edges = dataForLayout.edges; const labelsEl = svg.insert('g').attr('class', 'edgeLabels'); const linkIdCnt: any = {}; - const dir = dataForLayout.direction || 'DOWN'; let defaultStyle: string | undefined; let defaultLabelStyle: string | undefined; @@ -314,7 +296,7 @@ export const render = async ( linkIdCnt[linkIdBase]++; log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); } - const linkId = linkIdBase + '_' + linkIdCnt[linkIdBase]; + const linkId = linkIdBase; // + '_' + linkIdCnt[linkIdBase]; edge.id = linkId; log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); const linkNameStart = 'LS_' + edge.start; @@ -421,13 +403,11 @@ export const render = async ( // calculate start and end points of the edge, note that the source and target // can be modified for shapes that have ports - // @ts-ignore TODO: fix this - const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir); + + const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge); log.debug('abc78 source and target', source, target); // Add the edge to the graph graph.edges.push({ - // @ts-ignore TODO: fix this - id: 'e' + edge.start + edge.end, ...edge, sources: [source], targets: [target], @@ -484,6 +464,391 @@ export const render = async ( } } + const intersection = ( + node: { x: any; y: any; width: number; height: number }, + outsidePoint: { x: number; y: number }, + insidePoint: { x: number; y: number } + ) => { + log.debug(`intersection calc abc89: + outsidePoint: ${JSON.stringify(outsidePoint)} + insidePoint : ${JSON.stringify(insidePoint)} + node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`); + const x = node.x; + const y = node.y; + + const dx = Math.abs(x - insidePoint.x); + // const dy = Math.abs(y - insidePoint.y); + const w = node.width / 2; + let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx; + const h = node.height / 2; + + const Q = Math.abs(outsidePoint.y - insidePoint.y); + const R = Math.abs(outsidePoint.x - insidePoint.x); + + if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) { + // Intersection is top or bottom of rect. + const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y; + r = (R * q) / Q; + const res = { + x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r, + y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q, + }; + + if (r === 0) { + res.x = outsidePoint.x; + res.y = outsidePoint.y; + } + if (R === 0) { + res.x = outsidePoint.x; + } + if (Q === 0) { + res.y = outsidePoint.y; + } + + log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line + + return res; + } else { + // Intersection onn sides of rect + if (insidePoint.x < outsidePoint.x) { + r = outsidePoint.x - w - x; + } else { + // r = outsidePoint.x - w - x; + r = x - w - outsidePoint.x; + } + const q = (Q * r) / R; + // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x + dx - w; + // OK let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; + let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r; + // let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r; + let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q; + log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y }); + if (r === 0) { + _x = outsidePoint.x; + _y = outsidePoint.y; + } + if (R === 0) { + _x = outsidePoint.x; + } + if (Q === 0) { + _y = outsidePoint.y; + } + + return { x: _x, y: _y }; + } + }; + const outsideNode = ( + node: { x: any; y: any; width: number; height: number }, + point: { x: number; y: number } + ) => { + const x = node.x; + const y = node.y; + const dx = Math.abs(point.x - x); + const dy = Math.abs(point.y - y); + const w = node.width / 2; + const h = node.height / 2; + if (dx >= w || dy >= h) { + return true; + } + return false; + }; + + const cutter2 = (startNode: any, endNode: any, _points: any[]) => { + const startBounds = { + x: startNode.offset.posX + startNode.width / 2, + y: startNode.offset.posY + startNode.height / 2, + width: startNode.width, + height: startNode.height, + padding: startNode.padding, + }; + const endBounds = { + x: endNode.offset.posX + endNode.width / 2, + y: endNode.offset.posY + endNode.height / 2, + width: endNode.width, + height: endNode.height, + padding: endNode.padding, + }; + + if (_points.length === 0) { + return []; + } + + // Copy the original points array + const points = [..._points]; + + // The first point is the center of sNode, the last point is the center of eNode + const startCenter = points[0]; + const endCenter = points[points.length - 1]; + + log.debug('UIO cutter2: startCenter:', startCenter); + log.debug('UIO cutter2: endCenter:', endCenter); + + let firstOutsideStartIndex = -1; + let lastOutsideEndIndex = -1; + + // Single iteration through the array + for (let i = 0; i < points.length; i++) { + const point = points[i]; + + // Check if this is the first point outside the start node + if (firstOutsideStartIndex === -1 && outsideNode(startBounds, point)) { + firstOutsideStartIndex = i; + log.debug('UIO cutter2: First point outside start node at index', i, point); + } + + // Check if this point is outside the end node (keep updating to find the last one) + if (outsideNode(endBounds, point)) { + lastOutsideEndIndex = i; + log.debug('UIO cutter2: Point outside end node at index', i, point); + } + } + + log.debug( + 'UIO cutter2: firstOutsideStartIndex:', + firstOutsideStartIndex, + 'lastOutsideEndIndex:', + lastOutsideEndIndex + ); + log.debug('UIO cutter2: startBounds:', startBounds); + log.debug('UIO cutter2: endBounds:', endBounds); + log.debug('UIO cutter2: original points:', _points); + + // Calculate intersection with start node if we found a point outside it + if (firstOutsideStartIndex !== -1) { + const outsidePoint = points[firstOutsideStartIndex]; + let startIntersection; + + // Try using the node's intersect method first + if (startNode.intersect) { + startIntersection = startNode.intersect(outsidePoint); + + // Check if the intersection is valid (distance > 1) + const distance = Math.sqrt( + (startCenter.x - startIntersection.x) ** 2 + (startCenter.y - startIntersection.y) ** 2 + ); + if (distance <= 1) { + startIntersection = null; + } + } + + // Fallback to intersection function + if (!startIntersection) { + startIntersection = intersection(startBounds, startCenter, outsidePoint); + } + + // Replace the first point with the intersection + if (startIntersection) { + // Check if the intersection is the same as any existing point + const isDuplicate = points.some( + (p, index) => + index > 0 && + Math.abs(p.x - startIntersection.x) < 0.1 && + Math.abs(p.y - startIntersection.y) < 0.1 + ); + + if (isDuplicate) { + log.debug( + 'UIO cutter2: Start intersection is duplicate of existing point, removing first point instead' + ); + points.shift(); // Remove the first point instead of replacing it + } else { + log.debug( + 'UIO cutter2: Replacing first point', + points[0], + 'with intersection', + startIntersection + ); + points[0] = startIntersection; + } + } + } + + // Calculate intersection with end node + // Need to recalculate indices since we may have removed the first point + let outsidePointForEnd = null; + let outsideIndexForEnd = -1; + + // Find the last point that's outside the end node in the current points array + for (let i = points.length - 1; i >= 0; i--) { + if (outsideNode(endBounds, points[i])) { + outsidePointForEnd = points[i]; + outsideIndexForEnd = i; + log.debug('UIO cutter2: Found point outside end node at current index:', i, points[i]); + break; + } + } + + if (!outsidePointForEnd && points.length > 1) { + // No points outside end node, try using the second-to-last point + log.debug('UIO cutter2: No points outside end node, trying second-to-last point'); + outsidePointForEnd = points[points.length - 2]; + outsideIndexForEnd = points.length - 2; + } + + if (outsidePointForEnd) { + // Check if the outside point is actually on the boundary (distance = 0 from intersection) + // If so, we need to create a truly outside point + let actualOutsidePoint = outsidePointForEnd; + + // Quick check: if the point is very close to the node boundary, move it further out + const dx = Math.abs(outsidePointForEnd.x - endBounds.x); + const dy = Math.abs(outsidePointForEnd.y - endBounds.y); + const w = endBounds.width / 2; + const h = endBounds.height / 2; + + log.debug('UIO cutter2: Checking if outside point is truly outside:', { + outsidePoint: outsidePointForEnd, + dx, + dy, + w, + h, + isOnBoundary: Math.abs(dx - w) < 1 || Math.abs(dy - h) < 1, + }); + + // If the point is on or very close to the boundary, move it further out + if (Math.abs(dx - w) < 1 || Math.abs(dy - h) < 1) { + log.debug('UIO cutter2: Outside point is on boundary, creating truly outside point'); + // Move the point further away from the node center + const directionX = outsidePointForEnd.x - endBounds.x; + const directionY = outsidePointForEnd.y - endBounds.y; + const length = Math.sqrt(directionX * directionX + directionY * directionY); + + if (length > 0) { + // Move the point 10 pixels further out in the same direction + actualOutsidePoint = { + x: endBounds.x + (directionX / length) * (length + 10), + y: endBounds.y + (directionY / length) * (length + 10), + }; + log.debug('UIO cutter2: Created truly outside point:', actualOutsidePoint); + } + } + + let endIntersection; + + // Try using the node's intersect method first + if (endNode.intersect) { + endIntersection = endNode.intersect(actualOutsidePoint); + log.debug('UIO cutter2: endNode.intersect result:', endIntersection); + + // Check if the intersection is on the wrong side of the node + const isWrongSide = + (actualOutsidePoint.x < endBounds.x && endIntersection.x > endBounds.x) || + (actualOutsidePoint.x > endBounds.x && endIntersection.x < endBounds.x); + + if (isWrongSide) { + log.debug('UIO cutter2: endNode.intersect returned wrong side, setting to null'); + endIntersection = null; + } else { + // Check if the intersection is valid (distance > 1) + const distance = Math.sqrt( + (actualOutsidePoint.x - endIntersection.x) ** 2 + + (actualOutsidePoint.y - endIntersection.y) ** 2 + ); + log.debug('UIO cutter2: Distance from outside point to intersection:', distance); + if (distance <= 1) { + log.debug('UIO cutter2: endNode.intersect distance too small, setting to null'); + endIntersection = null; + } + } + } else { + log.debug('UIO cutter2: endNode.intersect method not available'); + } + + // Fallback to intersection function + if (!endIntersection) { + // Create a proper inside point that's on the correct side of the node + // The inside point should be between the outside point and the far edge + const insidePoint = { + x: + actualOutsidePoint.x < endBounds.x + ? endBounds.x - endBounds.width / 4 + : endBounds.x + endBounds.width / 4, + y: endCenter.y, + }; + + log.debug('UIO cutter2: Using fallback intersection function with:', { + endBounds, + actualOutsidePoint, + insidePoint, + endCenter, + }); + endIntersection = intersection(endBounds, actualOutsidePoint, insidePoint); + log.debug('UIO cutter2: Fallback intersection result:', endIntersection); + } + + // Replace the last point with the intersection + if (endIntersection) { + // Check if the intersection is the same as any existing point + const isDuplicate = points.some( + (p, index) => + index < points.length - 1 && + Math.abs(p.x - endIntersection.x) < 0.1 && + Math.abs(p.y - endIntersection.y) < 0.1 + ); + + if (isDuplicate) { + log.debug( + 'UIO cutter2: End intersection is duplicate of existing point, removing last point instead' + ); + points.pop(); // Remove the last point instead of replacing it + } else { + log.debug( + 'UIO cutter2: Replacing last point', + points[points.length - 1], + 'with intersection', + endIntersection, + 'using outside point at index', + outsideIndexForEnd + ); + points[points.length - 1] = endIntersection; + } + } + } else { + log.debug('UIO cutter2: No suitable outside point found for end node intersection'); + } + + // Final cleanup: Check if the last point is too close to the previous point + if (points.length > 1) { + const lastPoint = points[points.length - 1]; + const secondLastPoint = points[points.length - 2]; + const distance = Math.sqrt( + (lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2 + ); + + // If the distance is very small (less than 2 pixels), remove the last point + if (distance < 2) { + log.debug( + 'UIO cutter2: Last point too close to previous point, removing it. Distance:', + distance + ); + log.debug('UIO cutter2: Removing last point:', lastPoint, 'keeping:', secondLastPoint); + points.pop(); + } + } + + log.debug('UIO cutter2: Final points:', points); + + // Debug: Check which side of the end node we're ending at + if (points.length > 0) { + const finalPoint = points[points.length - 1]; + const endNodeCenter = endBounds.x; + const endNodeLeftEdge = endNodeCenter - endBounds.width / 2; + const endNodeRightEdge = endNodeCenter + endBounds.width / 2; + + log.debug('UIO cutter2: End node analysis:', { + finalPoint, + endNodeCenter, + endNodeLeftEdge, + endNodeRightEdge, + endingSide: finalPoint.x < endNodeCenter ? 'LEFT' : 'RIGHT', + distanceFromLeftEdge: Math.abs(finalPoint.x - endNodeLeftEdge), + distanceFromRightEdge: Math.abs(finalPoint.x - endNodeRightEdge), + }); + } + + return points; + }; + // @ts-ignore - ELK is not typed const elk = new ELK(); const element = svg.select('g'); @@ -495,7 +860,6 @@ export const render = async ( id: 'root', layoutOptions: { 'elk.hierarchyHandling': 'INCLUDE_CHILDREN', - 'elk.layered.crossingMinimization.forceNodeModelOrder': true, 'elk.algorithm': algorithm, 'nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy, 'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges, @@ -510,6 +874,7 @@ export const render = async ( // 'spacing.edgeEdge': 10, // 'spacing.edgeEdgeBetweenLayers': 20, // 'spacing.nodeSelfLoop': 20, + // Tweaking options // 'elk.layered.nodePlacement.favorStraightEdges': true, // 'nodePlacement.feedbackEdges': true, @@ -730,38 +1095,26 @@ export const render = async ( startNode.innerHTML ); } - - if (startNode.calcIntersect) { - const intersection = startNode.calcIntersect( - { - x: startNode.offset.posX + startNode.width / 2, - y: startNode.offset.posY + startNode.height / 2, - width: startNode.width, - height: startNode.height, - }, - edge.points[0] - ); - - if (distance(intersection, edge.points[0]) > epsilon) { - edge.points.unshift(intersection); - } + startNode.x = startNode.offset.posX + startNode.width / 2; + startNode.y = startNode.offset.posY + startNode.height / 2; + endNode.x = endNode.offset.posX + endNode.width / 2; + endNode.y = endNode.offset.posY + endNode.height / 2; + if (startNode.shape !== 'rect33') { + edge.points.unshift({ + x: startNode.x, + y: startNode.y, + }); } - if (endNode.calcIntersect) { - const intersection = endNode.calcIntersect( - { - x: endNode.offset.posX + endNode.width / 2, - y: endNode.offset.posY + endNode.height / 2, - width: endNode.width, - height: endNode.height, - }, - edge.points[edge.points.length - 1] - ); - - if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) { - edge.points.push(intersection); - } + if (endNode.shape !== 'rect33') { + edge.points.push({ + x: endNode.x, + y: endNode.y, + }); } + log.debug('UIO cutter2: Points before cutter2:', edge.points); + edge.points = cutter2(startNode, endNode, edge.points); + log.debug('UIO cutter2: Points after cutter2:', edge.points); const paths = insertEdge( edgesEl, edge, @@ -769,8 +1122,10 @@ export const render = async ( data4Layout.type, startNode, endNode, - data4Layout.diagramId + data4Layout.diagramId, + true ); + log.info('APA12 edge points after insert', JSON.stringify(edge.points)); edge.x = edge.labels[0].x + offset.x + edge.labels[0].width / 2; edge.y = edge.labels[0].y + offset.y + edge.labels[0].height / 2; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 4bcbf6d80..11713d6da 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -1,9 +1,13 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; -import { evaluate, getUrl } from '../../diagrams/common/common.js'; +import { evaluate } from '../../diagrams/common/common.js'; import { log } from '../../logger.js'; import { createText } from '../createText.js'; import utils from '../../utils.js'; -import { getLineFunctionsWithOffset } from '../../utils/lineWithOffset.js'; +import { + getLineFunctionsWithOffset, + markerOffsets, + markerOffsets2, +} from '../../utils/lineWithOffset.js'; import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js'; import { @@ -27,8 +31,8 @@ import createLabel from './createLabel.js'; import { addEdgeMarkers } from './edgeMarker.ts'; import { isLabelStyle } from './shapes/handDrawnShapeStyles.js'; -const edgeLabels = new Map(); -const terminalLabels = new Map(); +export const edgeLabels = new Map(); +export const terminalLabels = new Map(); export const clear = () => { edgeLabels.clear(); @@ -55,7 +59,7 @@ export const insertEdgeLabel = async (elem, edge) => { const edgeLabel = elem.insert('g').attr('class', 'edgeLabel'); // Create inner g, label, this will be positioned now for centering the text - const label = edgeLabel.insert('g').attr('class', 'label'); + const label = edgeLabel.insert('g').attr('class', 'label').attr('data-id', edge.id); label.node().appendChild(labelElement); // Center the label @@ -352,6 +356,25 @@ const cutPathAtIntersect = (_points, boundaryNode) => { return points; }; + +const generateDashArray = (len, oValueS, oValueE) => { + const middleLength = len - oValueS - oValueE; + const dashLength = 2; // Length of each dash + const gapLength = 2; // Length of each gap + const dashGapPairLength = dashLength + gapLength; + + // Calculate number of complete dash-gap pairs that can fit + const numberOfPairs = Math.floor(middleLength / dashGapPairLength); + + // Generate the middle pattern array + const middlePattern = Array(numberOfPairs).fill(`${dashLength} ${gapLength}`).join(' '); + + // Combine all parts + const dashArray = `0 ${oValueS} ${middlePattern} ${oValueE}`; + + return dashArray; +}; + const adjustForArrowHeads = function (lineData, size = 5) { if (!Array.isArray(lineData) || lineData.length < 2) { return lineData; @@ -360,34 +383,27 @@ const adjustForArrowHeads = function (lineData, size = 5) { const lastPoint = lineData[lineData.length - 1]; const secondLastPoint = lineData[lineData.length - 2]; - const distanceBetweenLastPoints = Math.sqrt( - (lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2 - ); + // Calculate number of complete dash-gap pairs that can fit + const numberOfPairs = Math.floor(middleLength / dashGapPairLength); - if (distanceBetweenLastPoints < size) { - // Calculate the direction vector from the last point to the second last point - const directionX = secondLastPoint.x - lastPoint.x; - const directionY = secondLastPoint.y - lastPoint.y; + // Generate the middle pattern array + const middlePattern = Array(numberOfPairs).fill(`${dashLength} ${gapLength}`).join(' '); - // Normalize the direction vector - const magnitude = Math.sqrt(directionX ** 2 + directionY ** 2); - const normalizedX = directionX / magnitude; - const normalizedY = directionY / magnitude; + // Combine all parts + const dashArray = `0 ${oValueS} ${middlePattern} ${oValueE}`; - // Calculate the new position for the second last point - const adjustedSecondLastPoint = { - x: lastPoint.x + normalizedX * size, - y: lastPoint.y + normalizedY * size, - }; - - // Replace the second last point in the new line data - newLineData[newLineData.length - 2] = adjustedSecondLastPoint; - } - - return newLineData; + return dashArray; }; - -export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) { +export const insertEdge = function ( + elem, + edge, + clusterDb, + diagramType, + startNode, + endNode, + id, + skipIntersect = false +) { const { handDrawnSeed } = getConfig(); let points = edge.points; let pointsHasChanged = false; @@ -401,11 +417,12 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod edgeClassStyles.push(edge.cssCompiledStyles[key]); } - if (head.intersect && tail.intersect) { + log.debug('UIO intersect check', edge.points, head.x, tail.x); + if (head.intersect && tail.intersect && !skipIntersect) { points = points.slice(1, edge.points.length - 1); points.unshift(tail.intersect(points[0])); log.debug( - 'Last point APA12', + 'Last point UIO', edge.start, '-->', edge.end, @@ -415,6 +432,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod ); points.push(head.intersect(points[points.length - 1])); } + const pointsStr = btoa(JSON.stringify(points)); if (edge.toCluster) { log.info('to cluster abc88', clusterDb.get(edge.toCluster)); points = cutPathAtIntersect(edge.points, clusterDb.get(edge.toCluster).node); @@ -434,8 +452,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod } let lineData = points.filter((p) => !Number.isNaN(p.y)); - lineData = adjustForArrowHeads(lineData); - // lineData = fixCorners(lineData); + //lineData = fixCorners(lineData); let curve = curveBasis; curve = curveLinear; switch (edge.curve) { @@ -479,6 +496,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod curve = curveBasis; } + // if (edge.curve) { + // curve = edge.curve; + // } + const { x, y } = getLineFunctionsWithOffset(edge); const lineFunction = line().x(x).y(y).curve(curve); @@ -510,10 +531,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod strokeClasses += ' edge-pattern-solid'; } let svgPath; - let linePath = lineFunction(lineData); - const edgeStyles = Array.isArray(edge.style) ? edge.style : edge.style ? [edge.style] : []; + let linePath = + edge.curve === 'rounded' + ? generateRoundedPath(applyMarkerOffsetsToPoints(lineData, edge), 5) + : lineFunction(lineData); + const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style]; let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:')); + let animatedEdge = false; if (edge.look === 'handDrawn') { const rc = rough.svg(elem); Object.assign([], lineData); @@ -544,7 +569,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod animationClass = ' edge-animation-' + edge.animation; } - const pathStyle = stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles; + const pathStyle = + (stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles) + + ';' + + (edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : ''); svgPath = elem .append('path') .attr('d', linePath) @@ -554,11 +582,38 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '') ) .attr('style', pathStyle); + + //eslint-disable-next-line @typescript-eslint/prefer-regexp-exec strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1]; + + // Possible fix to remove eslint-disable-next-line + //strokeColor = /stroke:([^;]+)/.exec(pathStyle)?.[1]; + + animatedEdge = + edge.animate === true || !!edge.animation || stylesFromClasses.includes('animation'); + const len = svgPath.node().getTotalLength(); + const oValueS = markerOffsets2[edge.arrowTypeStart] || 0; + const oValueE = markerOffsets2[edge.arrowTypeEnd] || 0; + + if (edge.look === 'neo' && !animatedEdge) { + const dashArray = + edge.pattern === 'dotted' || edge.pattern === 'dashed' + ? generateDashArray(len, oValueS, oValueE) + : `0 ${oValueS} ${len - oValueS - oValueE} ${oValueE}`; + + // No offset needed because we already start with a zero-length dash that effectively sets us up for a gap at the start. + const mOffset = `stroke-dasharray: ${dashArray}; stroke-dashoffset: 0;`; + svgPath.attr('style', mOffset + svgPath.attr('style')); + } } - // DEBUG code, DO NOT REMOVE - // adds a red circle at each edge coordinate + // MC Special + svgPath.attr('data-edge', true); + svgPath.attr('data-et', 'edge'); + svgPath.attr('data-id', edge.id); + svgPath.attr('data-points', pointsStr); + + // DEBUG code, adds a red circle at each edge coordinate // cornerPoints.forEach((point) => { // elem // .append('circle') @@ -568,24 +623,33 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod // .attr('cx', point.x) // .attr('cy', point.y); // }); - // lineData.forEach((point) => { - // elem - // .append('circle') - // .style('stroke', 'red') - // .style('fill', 'red') - // .attr('r', 1) - // .attr('cx', point.x) - // .attr('cy', point.y); - // }); + if (edge.showPoints) { + lineData.forEach((point) => { + elem + .append('circle') + .style('stroke', 'red') + .style('fill', 'red') + .attr('r', 1) + .attr('cx', point.x) + .attr('cy', point.y); + }); + } let url = ''; if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) { - url = getUrl(true); + url = + window.location.protocol + + '//' + + window.location.host + + window.location.pathname + + window.location.search; + url = url.replace(/\(/g, '\\(').replace(/\)/g, '\\)'); } log.info('arrowTypeStart', edge.arrowTypeStart); log.info('arrowTypeEnd', edge.arrowTypeEnd); - addEdgeMarkers(svgPath, edge, url, id, diagramType, strokeColor); + const useMargin = !animatedEdge && edge?.look === 'neo'; + addEdgeMarkers(svgPath, edge, url, id, diagramType, useMargin, strokeColor); let paths = {}; if (pointsHasChanged) { @@ -594,3 +658,134 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod paths.originalPath = edge.points; return paths; }; + +/** + * Generates SVG path data with rounded corners from an array of points. + * @param {Array} points - Array of points in the format [{x: Number, y: Number}, ...] + * @param {Number} radius - The radius of the rounded corners + * @returns {String} - SVG path data string + */ +function generateRoundedPath(points, radius) { + if (points.length < 2) { + return ''; + } + + let path = ''; + const size = points.length; + const epsilon = 1e-5; + + for (let i = 0; i < size; i++) { + const currPoint = points[i]; + const prevPoint = points[i - 1]; + const nextPoint = points[i + 1]; + + if (i === 0) { + // Move to the first point + path += `M${currPoint.x},${currPoint.y}`; + } else if (i === size - 1) { + // Last point, draw a straight line to the final point + path += `L${currPoint.x},${currPoint.y}`; + } else { + // Calculate vectors for incoming and outgoing segments + const dx1 = currPoint.x - prevPoint.x; + const dy1 = currPoint.y - prevPoint.y; + const dx2 = nextPoint.x - currPoint.x; + const dy2 = nextPoint.y - currPoint.y; + + const len1 = Math.hypot(dx1, dy1); + const len2 = Math.hypot(dx2, dy2); + + // Prevent division by zero + if (len1 < epsilon || len2 < epsilon) { + path += `L${currPoint.x},${currPoint.y}`; + continue; + } + + // Normalize the vectors + const nx1 = dx1 / len1; + const ny1 = dy1 / len1; + const nx2 = dx2 / len2; + const ny2 = dy2 / len2; + + // Calculate the angle between the vectors + const dot = nx1 * nx2 + ny1 * ny2; + // Clamp the dot product to avoid numerical issues with acos + const clampedDot = Math.max(-1, Math.min(1, dot)); + const angle = Math.acos(clampedDot); + + // Skip rounding if the angle is too small or too close to 180 degrees + if (angle < epsilon || Math.abs(Math.PI - angle) < epsilon) { + path += `L${currPoint.x},${currPoint.y}`; + continue; + } + + // Calculate the distance to offset the control point + const cutLen = Math.min(radius / Math.sin(angle / 2), len1 / 2, len2 / 2); + + // Calculate the start and end points of the curve + const startX = currPoint.x - nx1 * cutLen; + const startY = currPoint.y - ny1 * cutLen; + const endX = currPoint.x + nx2 * cutLen; + const endY = currPoint.y + ny2 * cutLen; + + // Draw the line to the start of the curve + path += `L${startX},${startY}`; + + // Draw the quadratic Bezier curve + path += `Q${currPoint.x},${currPoint.y} ${endX},${endY}`; + } + } + + return path; +} +// Helper function to calculate delta and angle between two points +function calculateDeltaAndAngle(point1, point2) { + if (!point1 || !point2) { + return { angle: 0, deltaX: 0, deltaY: 0 }; + } + const deltaX = point2.x - point1.x; + const deltaY = point2.y - point1.y; + const angle = Math.atan2(deltaY, deltaX); + return { angle, deltaX, deltaY }; +} + +// Function to adjust the first and last points of the points array +function applyMarkerOffsetsToPoints(points, edge) { + // Copy the points array to avoid mutating the original data + const newPoints = points.map((point) => ({ ...point })); + + // Handle the first point (start of the edge) + if (points.length >= 2 && markerOffsets[edge.arrowTypeStart]) { + const offsetValue = markerOffsets[edge.arrowTypeStart]; + + const point1 = points[0]; + const point2 = points[1]; + + const { angle } = calculateDeltaAndAngle(point1, point2); + + const offsetX = offsetValue * Math.cos(angle); + const offsetY = offsetValue * Math.sin(angle); + + newPoints[0].x = point1.x + offsetX; + newPoints[0].y = point1.y + offsetY; + } + + // Handle the last point (end of the edge) + const n = points.length; + if (n >= 2 && markerOffsets[edge.arrowTypeEnd]) { + const offsetValue = markerOffsets[edge.arrowTypeEnd]; + + const point1 = points[n - 1]; + const point2 = points[n - 2]; + + const { angle } = calculateDeltaAndAngle(point2, point1); + + const offsetX = offsetValue * Math.cos(angle); + const offsetY = offsetValue * Math.sin(angle); + + newPoints[n - 1].x = point1.x - offsetX; + newPoints[n - 1].y = point1.y - offsetY; + } + + return newPoints; +} diff --git a/packages/mermaid/src/utils/lineWithOffset.ts b/packages/mermaid/src/utils/lineWithOffset.ts index 057944325..e5d9b41f4 100644 --- a/packages/mermaid/src/utils/lineWithOffset.ts +++ b/packages/mermaid/src/utils/lineWithOffset.ts @@ -4,12 +4,22 @@ import type { EdgeData, Point } from '../types.js'; // under any transparent markers. // The offsets are calculated from the markers' dimensions. export const markerOffsets = { - aggregation: 18, - extension: 18, - composition: 18, + aggregation: 17.25, + extension: 17.25, + composition: 17.25, dependency: 6, lollipop: 13.5, arrow_point: 4, + //arrow_cross: 24, +} as const; + +// We need to draw the lines a bit shorter to avoid drawing +// under any transparent markers. +// The offsets are calculated from the markers' dimensions. +export const markerOffsets2 = { + arrow_point: 9, + arrow_cross: 12.5, + arrow_circle: 12.5, } as const; /** @@ -104,6 +114,7 @@ export const getLineFunctionsWithOffset = ( adjustment *= DIRECTION === 'right' ? -1 : 1; offset += adjustment; } + return pointTransformer(d).x + offset; }, y: function ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63d843360..df17cdc4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -533,6 +533,67 @@ importers: specifier: ^7.3.0 version: 7.3.0 + packages/mermaid/src/vitepress: + dependencies: + '@mdi/font': + specifier: ^7.4.47 + version: 7.4.47 + '@vueuse/core': + specifier: ^12.7.0 + version: 12.7.0(typescript@5.7.3) + font-awesome: + specifier: ^4.7.0 + version: 4.7.0 + jiti: + specifier: ^2.4.2 + version: 2.4.2 + mermaid: + specifier: workspace:^ + version: link:../.. + vue: + specifier: ^3.4.38 + version: 3.5.13(typescript@5.7.3) + devDependencies: + '@iconify-json/carbon': + specifier: ^1.1.37 + version: 1.2.1 + '@unocss/reset': + specifier: ^66.0.0 + version: 66.0.0 + '@vite-pwa/vitepress': + specifier: ^0.5.3 + version: 0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)) + '@vitejs/plugin-vue': + specifier: ^5.0.5 + version: 5.2.1(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3)) + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 + https-localhost: + specifier: ^4.7.1 + version: 4.7.1 + pathe: + specifier: ^2.0.3 + version: 2.0.3 + unocss: + specifier: ^66.0.0 + version: 66.0.0(postcss@8.5.3)(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3)) + unplugin-vue-components: + specifier: ^28.4.0 + version: 28.4.0(@babel/parser@7.27.2)(vue@3.5.13(typescript@5.7.3)) + vite: + specifier: ^6.1.1 + version: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1) + vite-plugin-pwa: + specifier: ^0.21.1 + version: 0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0) + vitepress: + specifier: 1.6.3 + version: 1.6.3(@algolia/client-search@5.20.3)(@types/node@22.13.5)(axios@1.8.4)(postcss@8.5.3)(search-insights@2.17.2)(terser@5.39.0)(typescript@5.7.3) + workbox-window: + specifier: ^7.3.0 + version: 7.3.0 + packages/parser: dependencies: langium: @@ -8091,10 +8152,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -19020,8 +19077,6 @@ snapshots: dependencies: picomatch: 2.3.1 - readdirp@4.1.2: {} - real-require@0.2.0: {} rechoir@0.7.1: @@ -20360,6 +20415,33 @@ snapshots: - supports-color - vue + unocss@66.0.0(postcss@8.5.3)(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@unocss/astro': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3)) + '@unocss/cli': 66.0.0 + '@unocss/core': 66.0.0 + '@unocss/postcss': 66.0.0(postcss@8.5.3) + '@unocss/preset-attributify': 66.0.0 + '@unocss/preset-icons': 66.0.0 + '@unocss/preset-mini': 66.0.0 + '@unocss/preset-tagify': 66.0.0 + '@unocss/preset-typography': 66.0.0 + '@unocss/preset-uno': 66.0.0 + '@unocss/preset-web-fonts': 66.0.0 + '@unocss/preset-wind': 66.0.0 + '@unocss/preset-wind3': 66.0.0 + '@unocss/transformer-attributify-jsx': 66.0.0 + '@unocss/transformer-compile-class': 66.0.0 + '@unocss/transformer-directives': 66.0.0 + '@unocss/transformer-variant-group': 66.0.0 + '@unocss/vite': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3)) + optionalDependencies: + vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1) + transitivePeerDependencies: + - postcss + - supports-color + - vue + unpipe@1.0.0: {} unplugin-utils@0.2.4: