#6097 Next shapes, learn-right and card

This commit is contained in:
Knut Sveidqvist 2024-12-03 10:23:02 +01:00
parent 3c93e4640a
commit 8336d1cf2d
9 changed files with 409 additions and 37 deletions

View File

@ -0,0 +1,102 @@
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<style>
body {
font-family: 'Arial';
background-color: #333;
}
</style>
</head>
<body>
<div id="diagram"></div>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
mermaid.initialize({
startOnLoad: false,
//look: 'handdrawn',
// layout: 'fixed',
theme: 'dark',
//layout: 'elk',
fontFamily: 'Kalam',
logLevel: 1,
});
let shape = 'card';
// let simplified = true;
let simplified = false;
let algorithm = 'elk';
// let algorithm = 'dagre';
let code = `---
config:
layout: ${algorithm}
---
flowchart TD
A["Abrakadabra"] --> C["C"] & D["I am a circle"] & n4["Untitled Node"]
D@{ shape: diamond}
B["Bombrakadombra"] --> D & C & D
C --> E["E"] & B
D --> E & A
n4 --> C
A@{ shape: ${shape}}
B@{ shape: ${shape}}
C@{ shape: ${shape}}
D@{ shape: ${shape}}
E@{ shape: ${shape}}
n4@{ shape: ${shape}}
`;
if (simplified) {
code = `---
config:
layout: ${algorithm}
---
flowchart LR
A["Abrakadabra"] --> C["C"] & C & C & C & C
%% A["Abrakadabra"] --> C
A@{ shape: ${shape}}
C@{ shape: ${shape}}
`;
}
console.log(code);
const { svg } = await mermaid.render('the-id-of-the-svg', code, undefined, undefined);
const elem = document.querySelector('#diagram');
elem.innerHTML = svg;
</script>
</body>
</html>

View File

@ -592,9 +592,10 @@ export const render = async (
setIncludeChildrenPolicy(target, ancestorId);
}
});
// const copy = JSON.parse(JSON.stringify({ ...elkGraph }));
// console.log('APA13 layout before', copy);
const g = await elk.layout(elkGraph);
// console.log('APA13 layout', JSON.parse(JSON.stringify(g)));
// debugger;
await drawNodes(0, 0, g.children, svg, subGraphsEl, 0);
g.edges?.map(
@ -683,6 +684,18 @@ export const render = async (
}
if (startNode.calcIntersect) {
// console.log(
// 'APA13 calculating start intersection start node',
// startNode.id,
// startNode.x,
// startNode.y,
// 'w:',
// startNode.width,
// 'h:',
// startNode.height,
// '\nPos',
// edge.points[0]
// );
const intersection = startNode.calcIntersect(
{
x: startNode.offset.posX + startNode.width / 2,
@ -707,9 +720,18 @@ export const render = async (
},
edge.points[edge.points.length - 1]
);
// if (edge.id === 'L_n4_C_10_0') {
// console.log('APA14 lineData', edge.points, 'intersection:', intersection);
// console.log(
// 'APA14! calculating end intersection\ndistance:',
// distance(intersection, edge.points[edge.points.length - 1])
// );
// }
if (distance(intersection, edge.points[edge.points.length - 1]) > epsilon) {
// console.log('APA13! distance ok\nintersection:', intersection);
edge.points.push(intersection);
// console.log('APA13! distance ok\npoints:', edge.points);
}
}

View File

@ -5,7 +5,8 @@ import { createText } from '../createText.js';
import utils from '../../utils.js';
import { getLineFunctionsWithOffset } from '../../utils/lineWithOffset.js';
import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';
import { curveBasis, line, select } from 'd3';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { curveBasis, curveLinear, line, select } from 'd3';
import rough from 'roughjs';
import createLabel from './createLabel.js';
import { addEdgeMarkers } from './edgeMarker.ts';
@ -335,7 +336,7 @@ const cutPathAtIntersect = (_points, boundaryNode) => {
return points;
};
const adjustForArrowHeads = function (lineData, size = 5) {
const adjustForArrowHeads = function (lineData, size = 5, shouldLog = false) {
const newLineData = [...lineData];
const lastPoint = lineData[lineData.length - 1];
const secondLastPoint = lineData[lineData.length - 2];
@ -344,6 +345,9 @@ const adjustForArrowHeads = function (lineData, size = 5) {
(lastPoint.x - secondLastPoint.x) ** 2 + (lastPoint.y - secondLastPoint.y) ** 2
);
if (shouldLog) {
log.debug('APA14 distanceBetweenLastPoints', distanceBetweenLastPoints);
}
if (distanceBetweenLastPoints < size) {
// Calculate the direction vector from the last point to the second last point
const directionX = secondLastPoint.x - lastPoint.x;
@ -366,6 +370,195 @@ const adjustForArrowHeads = function (lineData, size = 5) {
return newLineData;
};
function extractCornerPoints(points) {
const cornerPoints = [];
const cornerPointPositions = [];
for (let i = 1; i < points.length - 1; i++) {
const prev = points[i - 1];
const curr = points[i];
const next = points[i + 1];
if (
prev.x === curr.x &&
curr.y === next.y &&
Math.abs(curr.x - next.x) > 5 &&
Math.abs(curr.y - prev.y) > 5
) {
cornerPoints.push(curr);
cornerPointPositions.push(i);
} else if (
prev.y === curr.y &&
curr.x === next.x &&
Math.abs(curr.x - prev.x) > 5 &&
Math.abs(curr.y - next.y) > 5
) {
cornerPoints.push(curr);
cornerPointPositions.push(i);
}
}
return { cornerPoints, cornerPointPositions };
}
const findAdjacentPoint = function (pointA, pointB, distance) {
const xDiff = pointB.x - pointA.x;
const yDiff = pointB.y - pointA.y;
const length = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
const ratio = distance / length;
return { x: pointB.x - ratio * xDiff, y: pointB.y - ratio * yDiff };
};
const fixCorners = function (lineData) {
const { cornerPointPositions } = extractCornerPoints(lineData);
const newLineData = [];
for (let i = 0; i < lineData.length; i++) {
if (cornerPointPositions.includes(i)) {
const prevPoint = lineData[i - 1];
const nextPoint = lineData[i + 1];
const cornerPoint = lineData[i];
const newPrevPoint = findAdjacentPoint(prevPoint, cornerPoint, 5);
const newNextPoint = findAdjacentPoint(nextPoint, cornerPoint, 5);
const xDiff = newNextPoint.x - newPrevPoint.x;
const yDiff = newNextPoint.y - newPrevPoint.y;
newLineData.push(newPrevPoint);
const a = Math.sqrt(2) * 2;
let newCornerPoint = { x: cornerPoint.x, y: cornerPoint.y };
if (Math.abs(nextPoint.x - prevPoint.x) > 10 && Math.abs(nextPoint.y - prevPoint.y) >= 10) {
log.debug(
'Corner point fixing',
Math.abs(nextPoint.x - prevPoint.x),
Math.abs(nextPoint.y - prevPoint.y)
);
const r = 5;
if (cornerPoint.x === newPrevPoint.x) {
newCornerPoint = {
x: xDiff < 0 ? newPrevPoint.x - r + a : newPrevPoint.x + r - a,
y: yDiff < 0 ? newPrevPoint.y - a : newPrevPoint.y + a,
};
} else {
newCornerPoint = {
x: xDiff < 0 ? newPrevPoint.x - a : newPrevPoint.x + a,
y: yDiff < 0 ? newPrevPoint.y - r + a : newPrevPoint.y + r - a,
};
}
} else {
log.debug(
'Corner point skipping fixing',
Math.abs(nextPoint.x - prevPoint.x),
Math.abs(nextPoint.y - prevPoint.y)
);
}
newLineData.push(newCornerPoint, newNextPoint);
} else {
newLineData.push(lineData[i]);
}
}
return newLineData;
};
export const generateRoundedPath = (points, radius, endPosition) => {
if (points.length < 2) {
return '';
}
// console.trace('here', points);
const path = [];
const startPoint = points[0];
path.push(`M ${startPoint.x},${startPoint.y}`);
for (let i = 1; i < points.length - 1; i++) {
const currPoint = points[i];
const nextPoint = points[i + 1];
const prevPoint = points[i - 1];
// Calculate vectors
const v1 = { x: currPoint.x - prevPoint.x, y: currPoint.y - prevPoint.y };
const v2 = { x: nextPoint.x - currPoint.x, y: nextPoint.y - currPoint.y };
// Normalize vectors
const v1Length = Math.hypot(v1.x, v1.y);
const v2Length = Math.hypot(v2.x, v2.y);
const v1Normalized = { x: v1.x / v1Length, y: v1.y / v1Length };
const v2Normalized = { x: v2.x / v2Length, y: v2.y / v2Length };
// Calculate tangent points
const tangentLength = Math.min(radius, v1Length / 2, v2Length / 2);
const tangent1 = {
x: currPoint.x - v1Normalized.x * tangentLength,
y: currPoint.y - v1Normalized.y * tangentLength,
};
const tangent2 = {
x: currPoint.x + v2Normalized.x * tangentLength,
y: currPoint.y + v2Normalized.y * tangentLength,
};
if (endPosition) {
const { bottomY, leftX, rightX, topY } = endPosition;
if (startPoint.pos === 'b' && tangent1.y > topY) {
tangent1.y = topY;
tangent2.y = topY;
currPoint.y = topY;
}
if (startPoint.pos === 't' && tangent1.y < bottomY) {
tangent1.y = bottomY;
tangent2.y = bottomY;
currPoint.y = bottomY;
}
if (startPoint.pos === 'l' && tangent1.x < rightX) {
tangent1.x = rightX;
tangent2.x = rightX;
currPoint.x = rightX;
}
if (startPoint.pos === 'r' && tangent1.x > leftX) {
tangent1.x = leftX;
tangent2.x = leftX;
currPoint.x = leftX;
}
if (tangent2.x && tangent2.y && tangent1.x && tangent1.y) {
path.push(
`L ${tangent1.x},${tangent1.y}`,
`Q ${currPoint.x},${currPoint.y} ${tangent2.x},${tangent2.y}`
);
}
} else {
if (tangent2.x && tangent2.y && tangent1.x && tangent1.y) {
path.push(
`L ${tangent1.x},${tangent1.y}`,
`Q ${currPoint.x},${currPoint.y} ${tangent2.x},${tangent2.y}`
);
}
}
}
// Last point
const lastPoint = points[points.length - 1];
if (endPosition) {
if (startPoint.pos === 'b') {
if (endPosition?.topY && points[1].y > endPosition?.topY && points[2].y > endPosition?.topY) {
points[1].y = endPosition?.topY;
points[2].y = endPosition?.topY;
}
path.push(`L ${lastPoint.x},${endPosition.topY}`);
}
if (startPoint.pos === 't') {
if (points[1].y < endPosition.bottomY) {
points[1].y = endPosition.bottomY;
points[2].y = endPosition.bottomY;
}
path.push(`L ${lastPoint.x},${endPosition.bottomY}`);
}
if (startPoint.pos === 'l') {
path.push(`L ${endPosition.rightX},${lastPoint.y}`);
}
if (startPoint.pos === 'r') {
path.push(`L ${endPosition.leftX},${lastPoint.y}`);
}
} else {
path.push(`L ${lastPoint.x},${lastPoint.y}`);
}
return path.join(' ');
};
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
const { handDrawnSeed } = getConfig();
@ -407,8 +600,12 @@ 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 = adjustForArrowHeads(lineData, 4, edge.id === 'L_n4_C_10_0');
lineData = fixCorners(lineData);
// if (edge.id === 'L_n4_C_10_0') {
// console.log('APA14 lineData', lineData);
// }
// lineData = adjustForArrowHeads(lineData);
let curve = curveBasis;
// let curve = curveLinear;
if (edge.curve) {
@ -417,6 +614,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
const { x, y } = getLineFunctionsWithOffset(edge);
const lineFunction = line().x(x).y(y).curve(curve);
// const lineFunction = line().curve(curve);
let strokeClasses;
switch (edge.thickness) {
@ -447,6 +645,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
}
let svgPath;
let linePath = lineFunction(lineData);
// let linePath = generateRoundedPath(lineData, 5);
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
if (edge.look === 'handDrawn') {
const rc = rough.svg(elem);
@ -478,16 +677,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
// DEBUG code, DO NOT REMOVE
// adds a red circle at each edge coordinate
// cornerPoints.forEach((point) => {
// elem
// .append('circle')
// .style('stroke', 'blue')
// .style('fill', 'blue')
// .attr('r', 3)
// .attr('cx', point.x)
// .attr('cy', point.y);
// });
// lineData.forEach((point) => {
// points.forEach((point) => {
// elem
// .append('circle')
// .style('stroke', 'red')

View File

@ -57,7 +57,7 @@ function intersectLine(p1, p2, q1, q2) {
num = a2 * c1 - a1 * c2;
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom;
// console.log(
// 'APA30 intersectLine intersection',
// 'APA13 intersectLine intersection',
// '\np1: (',
// p1.x,
// p1.y,

View File

@ -7,7 +7,7 @@ import intersectLine from './intersect-line.js';
function intersectPolygon(node, polyPoints, point) {
let x1 = node.x;
let y1 = node.y;
// console.trace('APA14 intersectPolygon', x1, y1, polyPoints, point);
let intersections = [];
let minX = Number.POSITIVE_INFINITY;
@ -24,7 +24,7 @@ function intersectPolygon(node, polyPoints, point) {
let left = x1 - node.width / 2 - minX;
let top = y1 - node.height / 2 - minY;
// console.log('APA13 intersectPolygon2 ', left, y1);
for (let i = 0; i < polyPoints.length; i++) {
let p1 = polyPoints[i];
let p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
@ -34,7 +34,9 @@ function intersectPolygon(node, polyPoints, point) {
{ x: left + p1.x, y: top + p1.y },
{ x: left + p2.x, y: top + p2.y }
);
// console.log('APA13 intersectPolygon3 ', intersect);
if (intersect) {
// console.log('APA13 intersectPolygon4 ', intersect);
intersections.push(intersect);
}
}
@ -42,6 +44,7 @@ function intersectPolygon(node, polyPoints, point) {
if (!intersections.length) {
return node;
}
// console.log('APA12 intersectPolygon5 ');
if (intersections.length > 1) {
// More intersections, find the one nearest to edge end point
@ -54,6 +57,8 @@ function intersectPolygon(node, polyPoints, point) {
let qdy = q.y - point.y;
let distq = Math.sqrt(qdx * qdx + qdy * qdy);
// console.log('APA12 intersectPolygon6 ');
return distp < distq ? -1 : distp === distq ? 0 : 1;
});
}

View File

@ -66,7 +66,9 @@ export async function card<T extends SVGGraphicsElement>(parent: D3Selection<T>,
const w = bounds.width;
const points = getPoints(w, h, padding);
return intersect.polygon(bounds, points, point);
const res = intersect.polygon(bounds, points, point);
return { x: res.x - 0.5, y: res.y - 0.5 };
};
node.intersect = function (point) {

View File

@ -61,9 +61,7 @@ export function forkJoin<T extends SVGGraphicsElement>(
node.height += padding / 2 || 0;
}
node.calcIntersect = function (bounds: Bounds, point: Point) {
// TODO: Implement intersect for this shape
const radius = bounds.width / 2;
return intersect.circle(bounds, radius, point);
return intersect.rect(bounds, point);
};
node.intersect = function (point) {

View File

@ -51,13 +51,58 @@ export async function lean_right<T extends SVGGraphicsElement>(parent: D3Selecti
updateNodeBounds(node, polygon);
node.calcIntersect = function (bounds: Bounds, point: Point) {
// TODO: Implement intersect for this shape
const radius = bounds.width / 2;
return intersect.circle(bounds, radius, point);
const w = bounds.width;
const h = bounds.height;
const dx = h / 2;
const z = w - h;
// (w = dx+z+dx)
const points = [
{ x: -dx, y: 0 },
{ x: z, y: 0 },
{ x: z + dx, y: -h },
{ x: 0, y: -h },
];
const res = intersect.polygon(bounds, points, point);
// if (node.id === 'C') {
// console.log(
// 'APA14!',
// bounds.x,
// bounds.x,
// bounds.width,
// '\nw:',
// w,
// points,
// '\nExternal point: ',
// '(',
// point.x,
// point.y,
// ')\nIntersection:',
// res
// );
// }
return { x: res.x - 0.5, y: res.y - 0.5 };
};
node.intersect = function (point) {
return intersect.polygon(node, points, point);
node.intersect = function (point: Point) {
const res = intersect.polygon(node, points, point);
// if (node.id === 'C') {
// console.log(
// 'APA14!!',
// node.x,
// node.y,
// '\nw:',
// node.width,
// points,
// '\nExternal point: ',
// '(',
// point.x,
// point.y,
// ')\nIntersection:',
// res
// );
// }
return res;
};
return shapeSvg;

View File

@ -12,6 +12,14 @@ import rough from 'roughjs';
import type { D3Selection } from '../../../types.js';
import type { Bounds, Point } from '../../../types.js';
function getPoints(w: number, finalH: number, waveAmplitude: number) {
return [
{ x: -w / 2, y: finalH / 2 },
...generateFullSineWavePoints(-w / 2, finalH / 2, w / 2, finalH / 2, waveAmplitude, 1),
{ x: w / 2, y: -finalH / 2 },
...generateFullSineWavePoints(w / 2, -finalH / 2, -w / 2, -finalH / 2, waveAmplitude, -1),
];
}
export async function waveRectangle<T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node
@ -53,12 +61,7 @@ export async function waveRectangle<T extends SVGGraphicsElement>(
options.fillStyle = 'solid';
}
const points = [
{ x: -w / 2, y: finalH / 2 },
...generateFullSineWavePoints(-w / 2, finalH / 2, w / 2, finalH / 2, waveAmplitude, 1),
{ x: w / 2, y: -finalH / 2 },
...generateFullSineWavePoints(w / 2, -finalH / 2, -w / 2, -finalH / 2, waveAmplitude, -1),
];
const points = getPoints(w, finalH, waveAmplitude);
const waveRectPath = createPathFromPoints(points);
const waveRectNode = rc.path(waveRectPath, options);
@ -78,9 +81,14 @@ export async function waveRectangle<T extends SVGGraphicsElement>(
updateNodeBounds(node, waveRect);
node.calcIntersect = function (bounds: Bounds, point: Point) {
// TODO: Implement intersect for this shape
const radius = bounds.width / 2;
return intersect.circle(bounds, radius, point);
const w = bounds.width;
const h = bounds.height;
const waveAmplitude = Math.min(h * 0.2, h / 4);
const finalH = h + waveAmplitude * 2;
const points = getPoints(w, finalH, waveAmplitude);
return intersect.polygon(node, points, point);
};
node.intersect = function (point) {