5432 WIP, rendering sections

This commit is contained in:
Knut Sveidqvist
2024-10-06 18:34:24 +02:00
parent fabdfd9ae8
commit fb44e769f2
7 changed files with 186 additions and 147 deletions

View File

@@ -90,9 +90,13 @@ kanban
id3[Update DB function] id3[Update DB function]
id4[Create parsing tests] id4[Create parsing tests]
id5[define getData] id5[define getData]
id6[Create renderer] id6[Create renderer so that it works in all cases]
id7[In progress] id7[In progress]
id8[Design grammar] id8[Design grammar]
id9[Ready for deploy]
id10[Ready for test]
id11[Done]
id12[Can't reproduce]
</pre> </pre>
<script type="module"> <script type="module">
import mermaid from './mermaid.esm.mjs'; import mermaid from './mermaid.esm.mjs';

View File

@@ -50,7 +50,6 @@ const getSection = function (level: number) {
}; };
const getSections = function () { const getSections = function () {
console.log('sections', sections);
return sections; return sections;
}; };
@@ -58,13 +57,29 @@ const getData = function () {
const edges = [] as Edge[]; const edges = [] as Edge[];
const nodes: Node[] = []; const nodes: Node[] = [];
const sections = getSections();
const conf = getConfig();
// const id: string = sanitizeText(id, conf) || 'identifier' + cnt++; // const id: string = sanitizeText(id, conf) || 'identifier' + cnt++;
// const node = { for (const section of sections) {
// id, const node = {
// label: sanitizeText(descr, conf), id: section.nodeId,
// isGroup, label: sanitizeText(section.descr, conf),
// } satisfies Node; isGroup: true,
shape: 'kanbanSection',
} satisfies Node;
nodes.push(node);
for (const item of section.children) {
const childNode = {
id: item.nodeId,
parentId: section.nodeId,
label: sanitizeText(item.descr, conf),
isGroup: false,
shape: 'kanbanItem',
} satisfies Node;
nodes.push(childNode);
}
}
return { nodes, edges, other: {}, config: getConfig() }; return { nodes, edges, other: {}, config: getConfig() };
}; };

View File

@@ -1,4 +1,4 @@
import cytoscape from 'cytoscape'; import type cytoscape from 'cytoscape';
// @ts-expect-error No types available // @ts-expect-error No types available
import coseBilkent from 'cytoscape-cose-bilkent'; import coseBilkent from 'cytoscape-cose-bilkent';
import { select } from 'd3'; import { select } from 'd3';
@@ -9,13 +9,10 @@ import { log } from '../../logger.js';
import type { D3Element } from '../../types.js'; import type { D3Element } from '../../types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import type { FilledKanbanNode, KanbanDB, KanbanNode } from './kanbanTypes.js'; import type { KanbanDB, KanbanNode } from './kanbanTypes.js';
import { drawNode, positionNode } from './svgDraw.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
import { insertCluster, positionCluster } from '../../rendering-util/rendering-elements/clusters';
// Inject the layout algorithm into cytoscape import { insertNode, positionNode } from '../../rendering-util/rendering-elements/nodes';
async function drawSection(section: FilledKanbanNode, svg: D3Element, conf: MermaidConfig) {}
declare module 'cytoscape' { declare module 'cytoscape' {
interface EdgeSingular { interface EdgeSingular {
@@ -85,72 +82,11 @@ function addNodes(mindmap: KanbanNode, cy: cytoscape.Core, conf: MermaidConfig,
} }
} }
function layoutMindmap(node: KanbanNode, conf: MermaidConfig): Promise<cytoscape.Core> {
return new Promise((resolve) => {
// Add temporary render element
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
const cy = cytoscape({
container: document.getElementById('cy'), // container to render in
style: [
{
selector: 'edge',
style: {
'curve-style': 'bezier',
},
},
],
});
// Remove element after layout
renderEl.remove();
addNodes(node, cy, conf, 0);
// Make cytoscape care about the dimensions of the nodes
cy.nodes().forEach(function (n) {
n.layoutDimensions = () => {
const data = n.data();
return { w: data.width, h: data.height };
};
});
cy.layout({
name: 'cose-bilkent',
// @ts-ignore Types for cose-bilkent are not correct?
quality: 'proof',
styleEnabled: false,
animate: false,
}).run();
cy.ready((e) => {
log.info('Ready', e);
resolve(cy);
});
});
}
function positionNodes(db: KanbanDB, cy: cytoscape.Core) {
cy.nodes().map((node, id) => {
const data = node.data();
data.x = node.position().x;
data.y = node.position().y;
positionNode(db, data);
const el = db.getElementById(data.nodeId);
log.info('Id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
el.attr(
'transform',
`translate(${node.position().x - data.width / 2}, ${node.position().y - data.height / 2})`
);
el.attr('attr', `apa-${id})`);
});
}
export const draw: DrawDefinition = async (text, id, _version, diagObj) => { export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
log.debug('Rendering mindmap diagram\n' + text); log.debug('Rendering mindmap diagram\n' + text);
const db = diagObj.db as KanbanDB; const db = diagObj.db as KanbanDB;
const sections = db.getSections(); const data4Layout = db.getData();
// const sections = db.getData();
if (!sections) {
return;
}
const conf = getConfig(); const conf = getConfig();
conf.htmlLabels = false; conf.htmlLabels = false;
@@ -160,19 +96,43 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
// Draw the graph and start with drawing the nodes without proper position // Draw the graph and start with drawing the nodes without proper position
// this gives us the size of the nodes and we can set the positions later // this gives us the size of the nodes and we can set the positions later
const edgesElem = svg.append('g'); const sectionsElem = svg.append('g');
edgesElem.attr('class', 'sections'); sectionsElem.attr('class', 'sections');
const nodesElem = svg.append('g'); const nodesElem = svg.append('g');
nodesElem.attr('class', 'items'); nodesElem.attr('class', 'items');
await drawSections(db, nodesElem, sections as FilledKanbanNode, -1, conf); const sections = data4Layout.nodes.filter((node) => node.isGroup);
let cnt = 0;
// TODO set padding
const padding = 10;
// Next step is to layout the mindmap, giving each node a position for (const section of sections) {
let y = 0;
cnt = cnt + 1;
const WIDTH = 300;
section.x = WIDTH * cnt + ((cnt - 1) * padding) / 2;
section.width = WIDTH;
section.y = 0;
section.height = WIDTH;
section.rx = 5;
section.ry = 5;
const cy = await layoutMindmap(sections, conf); // Todo, use theme variable THEME_COLOR_LIMIT instead of 10
section.cssClasses = section.cssClasses + ' section-' + cnt;
// After this we can draw, first the edges and the then nodes with the correct position const cluster = await insertCluster(sectionsElem, section);
drawEdges(edgesElem, cy); const sectionItems = data4Layout.nodes.filter((node) => node.parentId === section.id);
positionNodes(db, cy); // positionCluster(section);
for (const item of sectionItems) {
item.x = section.x;
item.width = WIDTH - padding * 2;
// item.height = 100;
const nodeEl = await insertNode(nodesElem, item);
console.log('ITEM', item, 'bbox=', nodeEl.node().getBBox());
item.y = y;
item.height = 150;
await positionNode(item);
y = y + 1.5 * nodeEl.node().getBBox().height + padding / 2;
}
}
// Setup the view box and size of the svg element // Setup the view box and size of the svg element
setupGraphViewbox( setupGraphViewbox(

View File

@@ -46,6 +46,16 @@ const genSections: DiagramStylesProvider = (options) => {
.disabled text { .disabled text {
fill: #efefef; fill: #efefef;
} }
.node rect,
.node circle,
.node ellipse,
.node polygon,
.node path {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
`; `;
} }
return sections; return sections;

View File

@@ -281,30 +281,30 @@ const roundedWithTitle = async (parent, node) => {
return { cluster: shapeSvg, labelBBox: bbox }; return { cluster: shapeSvg, labelBBox: bbox };
}; };
const kanbanSection = async (parent, node) => { const kanbanSection = async (parent, node) => {
log.info('Creating subgraph rect for ', node.id, node);
const siteConfig = getConfig(); const siteConfig = getConfig();
const { themeVariables, handDrawnSeed } = siteConfig; const { themeVariables, handDrawnSeed } = siteConfig;
const { altBackground, compositeBackground, compositeTitleBackground, nodeBorder } = const { clusterBkg, clusterBorder } = themeVariables;
themeVariables;
const { labelStyles, nodeStyles, borderStyles, backgroundStyles } = styles2String(node);
// Add outer g element // Add outer g element
const shapeSvg = parent const shapeSvg = parent
.insert('g') .insert('g')
.attr('class', node.cssClasses) .attr('class', 'cluster ' + node.cssClasses)
.attr('id', node.id) .attr('id', node.id)
.attr('data-id', node.id)
.attr('data-look', node.look); .attr('data-look', node.look);
// add the rect const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
const outerRectG = shapeSvg.insert('g', ':first-child');
// Create the label and insert it after the rect // Create the label and insert it after the rect
const label = shapeSvg.insert('g').attr('class', 'cluster-label'); const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');
let innerRect = shapeSvg.append('rect');
const text = label const text = await createText(labelEl, node.label, {
.node() style: node.labelStyle,
.appendChild(await createLabel(node.label, node.labelStyle, undefined, true)); useHtmlLabels,
isNode: true,
});
// Get the size of the label // Get the size of the label
let bbox = text.getBBox(); let bbox = text.getBBox();
@@ -317,83 +317,72 @@ const kanbanSection = async (parent, node) => {
dv.attr('height', bbox.height); dv.attr('height', bbox.height);
} }
// Rounded With Title const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
const padding = 0 * node.padding;
const halfPadding = padding / 2;
const width =
(node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width) + padding;
if (node.width <= bbox.width + node.padding) { if (node.width <= bbox.width + node.padding) {
node.diff = (width - node.width) / 2 - node.padding; node.diff = (width - node.width) / 2 - node.padding;
} else { } else {
node.diff = -node.padding; node.diff = -node.padding;
} }
const height = node.height + padding; const height = node.height;
// const height = node.height + padding;
const innerHeight = node.height + padding - bbox.height - 6;
const x = node.x - width / 2; const x = node.x - width / 2;
const y = node.y - height / 2; const y = node.y;
node.width = width;
const innerY = node.y - node.height / 2 - halfPadding + bbox.height + 2;
// add the rect log.trace('Data ', node, JSON.stringify(node));
let rect; let rect;
if (node.look === 'handDrawn') { if (node.look === 'handDrawn') {
const isAlt = node.cssClasses.includes('statediagram-cluster-alt'); // @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg); const rc = rough.svg(shapeSvg);
const roughOuterNode = const options = userNodeOverrides(node, {
node.rx || node.ry
? rc.path(createRoundedRectPathD(x, y, width, height, 10), {
roughness: 0.7, roughness: 0.7,
fill: compositeTitleBackground, fill: clusterBkg,
fillStyle: 'solid', // fill: 'red',
stroke: nodeBorder, stroke: clusterBorder,
seed: handDrawnSeed, fillWeight: 3,
})
: rc.rectangle(x, y, width, height, { seed: handDrawnSeed });
rect = shapeSvg.insert(() => roughOuterNode, ':first-child');
const roughInnerNode = rc.rectangle(x, innerY, width, innerHeight, {
fill: isAlt ? altBackground : compositeBackground,
fillStyle: isAlt ? 'hachure' : 'solid',
stroke: nodeBorder,
seed: handDrawnSeed, seed: handDrawnSeed,
}); });
const roughNode = rc.path(createRoundedRectPathD(x, y, width, height, node.rx), options);
rect = shapeSvg.insert(() => roughOuterNode, ':first-child'); rect = shapeSvg.insert(() => {
innerRect = shapeSvg.insert(() => roughInnerNode); log.debug('Rough node insert CXC', roughNode);
return roughNode;
}, ':first-child');
// Should we affect the options instead of doing this?
rect.select('path:nth-child(2)').attr('style', borderStyles.join(';'));
rect.select('path').attr('style', backgroundStyles.join(';').replace('fill', 'stroke'));
} else { } else {
rect = outerRectG.insert('rect', ':first-child'); // add the rect
const outerRectClass = 'outer'; rect = shapeSvg.insert('rect', ':first-child');
// center the rect around its coordinate // center the rect around its coordinate
rect rect
.attr('class', outerRectClass) .attr('style', nodeStyles)
.attr('rx', node.rx)
.attr('ry', node.ry)
.attr('x', x) .attr('x', x)
.attr('y', y) .attr('y', y)
.attr('width', width) .attr('width', width)
.attr('height', height) .attr('height', height);
.attr('data-look', node.look);
innerRect
.attr('class', 'inner')
.attr('x', x)
.attr('y', innerY)
.attr('width', width)
.attr('height', innerHeight);
} }
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
label.attr( labelEl.attr(
'transform', 'transform',
`translate(${node.x - bbox.width / 2}, ${y + 1 - (evaluate(siteConfig.flowchart.htmlLabels) ? 0 : 3)})` // This puts the label on top of the box instead of inside it
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
); );
if (labelStyles) {
const span = labelEl.select('span');
if (span) {
span.attr('style', labelStyles);
}
}
// Center the label
const rectBox = rect.node().getBBox(); const rectBox = rect.node().getBBox();
node.height = rectBox.height;
node.offsetX = 0; node.offsetX = 0;
node.width = rectBox.width;
node.height = rectBox.height;
// Used by layout engine to position subgraph in parent // Used by layout engine to position subgraph in parent
node.offsetY = bbox.height - node.padding / 2; node.offsetY = bbox.height - node.padding / 2;
node.labelBBox = bbox;
node.intersect = function (point) { node.intersect = function (point) {
return intersectRect(node, point); return intersectRect(node, point);

View File

@@ -55,6 +55,7 @@ import { iconCircle } from './shapes/iconCircle.js';
import { icon } from './shapes/icon.js'; import { icon } from './shapes/icon.js';
import { imageSquare } from './shapes/imageSquare.js'; import { imageSquare } from './shapes/imageSquare.js';
import { iconRounded } from './shapes/iconRounded.js'; import { iconRounded } from './shapes/iconRounded.js';
import { kanbanItem } from './shapes/kanbanItem.js';
//Use these names as the left side to render shapes. //Use these names as the left side to render shapes.
export const shapes = { export const shapes = {
@@ -299,6 +300,7 @@ export const shapes = {
icon, icon,
iconRounded, iconRounded,
imageSquare, imageSquare,
kanbanItem,
}; };
const nodeElems = new Map(); const nodeElems = new Map();

View File

@@ -0,0 +1,59 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { createRoundedRectPathD } from './roundedRectPath.js';
import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const kanbanItem = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
// console.log('IPI labelStyles:', labelStyles);
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const labelPaddingX = 10;
const labelPaddingY = 10;
const totalWidth = Math.max(bbox.width + labelPaddingX * 2, node?.width || 0);
const totalHeight = Math.max(bbox.height + labelPaddingY * 2, node?.height || 0);
const x = -totalWidth / 2;
const y = -totalHeight / 2;
// log.info('IPI node = ', node);
let rect;
const { rx, ry } = node;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const roughNode =
rx || ry
? rc.path(createRoundedRectPathD(x, y, totalWidth, totalHeight, rx || 0), options)
: rc.rectangle(x, y, totalWidth, totalHeight, options);
rect = shapeSvg.insert(() => roughNode, ':first-child');
rect.attr('class', 'basic label-container').attr('style', cssStyles);
} else {
rect = shapeSvg.insert('rect', ':first-child');
rect
.attr('class', 'basic label-container __APA__')
.attr('style', nodeStyles)
.attr('rx', rx)
.attr('ry', ry)
.attr('x', x)
.attr('y', y)
.attr('width', totalWidth)
.attr('height', totalHeight);
}
updateNodeBounds(node, rect);
node.intersect = function (point) {
return intersect.rect(node, point);
};
return shapeSvg;
};