Skip to content

Commit 83c0482

Browse files
authored
Merge pull request #83 from APIOpsCycles/small-svg-export-fixes
Small svg export fixes and pdf export support to UI
2 parents 2e1f59c + dee554b commit 83c0482

14 files changed

Lines changed: 650 additions & 55 deletions

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
The repository history shows twelve merged pull requests, which introduce localization features, bug fixes, and new tests. Key updates include:
44

5-
## Version 1.7.0 - 1.7.1
5+
## Version 1.7.0 - 1.7.2
66
- 1.7.0 UI is now embeddable and configurable for Astro, Vue, React etc. use
77
- 1.7.0 fixed export bug where sticky notes were partially hidden if on top of section line
88
- 1.7.1 Fixed the responsive Help toggle so hiding the Help section no longer causes layout trembling on narrower screens.
99
- 1.7.1 Aligned the mobile toolbar by hiding the Help button at the same breakpoint where the Help panel is unavailable.
10+
- 1.7.2 Regression from 1.7.0 Fixed journeystep visibility in SVG and PNG exports
11+
- 1.7.2 Restored section descriptions for empty canvas exports while keeping them hidden whenever sticky notes are present
12+
- 1.7.2 Restored the PDF export button in the UI and wired it to the browser export flow
1013

1114
## Version 1.6.0-1.6.2
1215
Canvas package for PNG generation is now a normal dependency, because it's not working as optional, and downstream modules depend on PNG generation.
16.3 KB
Loading
-606 Bytes
Binary file not shown.

examples/Canvas_apiBusinessModelCanvas_de.svg

Lines changed: 1 addition & 1 deletion
Loading
40.5 KB
Loading
-621 Bytes
Binary file not shown.

examples/Canvas_apiBusinessModelCanvas_en.svg

Lines changed: 23 additions & 23 deletions
Loading

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "canvascreator",
3-
"version": "1.7.1",
3+
"version": "1.7.2",
44
"main": "dist/canvascreator.cjs",
55
"scripts": {
66
"test": "jest",

src/helpers.js

Lines changed: 173 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,36 @@
11
const defaultStyles = require('./defaultStyles');
2+
const localizedData = require('apiops-cycles-method-data/localizedData.json');
3+
4+
function getLocaleKey(locale) {
5+
if (!locale) return defaultStyles.defaultLocale;
6+
const lower = String(locale).toLowerCase();
7+
if (localizedData[lower]) return lower;
8+
const base = lower.split('-')[0];
9+
return localizedData[base] ? base : lower;
10+
}
11+
12+
function wrapTextApprox(text, maxWidth = defaultStyles.maxLineWidth) {
13+
const normalized = String(text || '').replace(/\n{2,}/g, '\n');
14+
const words = normalized.split(' ');
15+
const lines = [];
16+
let line = '';
17+
18+
for (const word of words) {
19+
const testLine = `${line}${word} `;
20+
if (testLine.length * 6 > maxWidth && line.trim()) {
21+
lines.push(line.trim());
22+
line = `${word} `;
23+
} else {
24+
line = testLine;
25+
}
26+
}
27+
28+
if (line.trim()) {
29+
lines.push(line.trim());
30+
}
31+
32+
return lines.join('\n');
33+
}
234

335
function sanitizeInput(text) {
436
// Remove script tags entirely
@@ -19,18 +51,25 @@ function validateInput(text) {
1951
}
2052

2153
function distributeMissingPositions(content, canvasDef, styles = defaultStyles) {
54+
const resolvedStyles = { ...defaultStyles, ...styles };
2255
const cellWidth = Math.floor(
23-
(styles.width - canvasDef.layout.columns * styles.padding) /
56+
(resolvedStyles.width - canvasDef.layout.columns * resolvedStyles.padding) /
2457
canvasDef.layout.columns,
2558
);
2659

2760
const cellHeight = Math.floor(
28-
(styles.height -
29-
styles.headerHeight -
30-
styles.footerHeight -
31-
4 * styles.padding) /
61+
(resolvedStyles.height -
62+
resolvedStyles.headerHeight -
63+
resolvedStyles.footerHeight -
64+
4 * resolvedStyles.padding) /
3265
canvasDef.layout.rows,
3366
);
67+
const locale = getLocaleKey(content.locale || resolvedStyles.defaultLocale);
68+
const localizedCanvas =
69+
(localizedData[locale] && localizedData[locale][canvasDef.id]) || {};
70+
const noteGap = Math.max(4, Math.floor(resolvedStyles.stickyNoteSpacing / 2));
71+
const borderGap = Math.max(4, Math.floor(resolvedStyles.padding / 2));
72+
const journeyNoteInsetY = Math.max(4, Math.floor(noteGap / 2));
3473

3574
content.sections.forEach((section) => {
3675
const templateSection = canvasDef.sections.find(
@@ -44,36 +83,147 @@ function distributeMissingPositions(content, canvasDef, styles = defaultStyles)
4483
if (notesToPlace.length === 0) return;
4584

4685
const startX =
47-
templateSection.gridPosition.column * cellWidth + 2 * styles.padding;
48-
const startY =
49-
templateSection.gridPosition.row * cellHeight + styles.headerHeight;
86+
templateSection.gridPosition.column * cellWidth + borderGap;
5087
const secWidth = templateSection.gridPosition.colSpan * cellWidth;
5188
const secHeight = templateSection.gridPosition.rowSpan * cellHeight;
89+
const sectionTop =
90+
templateSection.gridPosition.row * cellHeight + resolvedStyles.headerHeight;
91+
const sectionBottom = sectionTop + secHeight;
92+
const sectionTitle =
93+
(localizedCanvas.sections &&
94+
localizedCanvas.sections[section.sectionId] &&
95+
localizedCanvas.sections[section.sectionId].section) ||
96+
templateSection.id;
97+
const sectionDescription =
98+
(localizedCanvas.sections &&
99+
localizedCanvas.sections[section.sectionId] &&
100+
localizedCanvas.sections[section.sectionId].description) ||
101+
'';
102+
const titleLines = wrapTextApprox(
103+
sectionTitle,
104+
secWidth - 2 * resolvedStyles.padding - resolvedStyles.circleRadius,
105+
)
106+
.split('\n')
107+
.filter((line) => line.length > 0).length || 1;
108+
const titleTop = sectionTop + resolvedStyles.padding + resolvedStyles.circleRadius;
109+
const titleBottom =
110+
titleTop + titleLines * (resolvedStyles.fontSize + 6);
111+
const descriptionBottom =
112+
titleBottom + noteGap;
113+
const sectionBox = {
114+
x: templateSection.gridPosition.column * cellWidth + 2 * resolvedStyles.padding,
115+
y: sectionTop,
116+
width: secWidth,
117+
height: secHeight,
118+
};
119+
const journeyLayout = templateSection.journeySteps
120+
? getJourneyStepsLayout(templateSection, sectionBox, resolvedStyles)
121+
: null;
122+
const startY = templateSection.journeySteps
123+
? Math.max(
124+
descriptionBottom + noteGap,
125+
journeyLayout ? journeyLayout.boxes[0].y + noteGap : titleBottom + noteGap,
126+
)
127+
: descriptionBottom + noteGap;
128+
129+
const noteSize = resolvedStyles.stickyNoteSize;
130+
if (journeyLayout) {
131+
notesToPlace.forEach((note, index) => {
132+
const box = journeyLayout.boxes[index % journeyLayout.boxes.length];
133+
const row = Math.floor(index / journeyLayout.boxes.length);
134+
const rowOffset = row * (noteSize + noteGap);
135+
note.position = {
136+
x: box.x + Math.max(0, Math.floor((box.width - noteSize) / 2)),
137+
y: box.y + Math.max(0, Math.floor((box.height - noteSize) / 2)) +
138+
journeyNoteInsetY +
139+
rowOffset,
140+
};
141+
});
142+
return;
143+
}
52144

53-
const noteSize = styles.stickyNoteSize;
145+
const innerLeft = sectionBox.x;
146+
const innerRight = sectionBox.x + secWidth;
147+
const availableWidth = Math.max(noteSize, innerRight - innerLeft);
148+
const availableHeight = sectionBottom - startY - borderGap - (resolvedStyles.circleRadius / 2);
54149
const maxCols = Math.max(
55150
1,
56-
Math.floor(secWidth / (noteSize + styles.stickyNoteSpacing)),
151+
Math.floor((availableWidth + noteGap) / (noteSize + noteGap)),
152+
);
153+
const maxRows = Math.max(
154+
1,
155+
Math.floor((availableHeight + noteGap) / (noteSize + noteGap)),
156+
);
157+
let cols = Math.max(
158+
1,
159+
Math.min(maxCols, Math.ceil(notesToPlace.length / maxRows)),
57160
);
58-
const cols = Math.min(notesToPlace.length, maxCols);
59-
const rows = Math.ceil(notesToPlace.length / cols);
60-
61-
const spaceX = Math.max(0, (secWidth - cols * noteSize) / (cols + 1));
62-
const spaceY = Math.max(0, (secHeight - rows * noteSize) / (rows + 1));
63-
64-
notesToPlace.forEach((note, index) => {
65-
const c = index % cols;
66-
const r = Math.floor(index / cols);
67-
note.position = {
68-
x: startX + spaceX + c * (noteSize + spaceX),
69-
y: startY + spaceY + r * (noteSize + spaceY),
70-
};
161+
while (Math.ceil(notesToPlace.length / cols) > maxRows && cols < maxCols) {
162+
cols += 1;
163+
}
164+
const gridStartY = startY;
165+
const rows = Array.from({ length: Math.ceil(notesToPlace.length / cols) }, (_, rowIndex) =>
166+
notesToPlace.slice(rowIndex * cols, (rowIndex + 1) * cols),
167+
);
168+
169+
rows.forEach((rowNotes, rowIndex) => {
170+
const rowWidth = rowNotes.length * noteSize + Math.max(0, rowNotes.length - 1) * noteGap;
171+
let rowStartX;
172+
// if (rowIndex === 0) {
173+
rowStartX = innerLeft + Math.max(0, Math.floor((availableWidth - rowWidth) / 2));
174+
/* } else {
175+
rowStartX = Math.max(innerLeft, innerRight - rowWidth);
176+
} */
177+
178+
rowNotes.forEach((note, index) => {
179+
note.position = {
180+
x: rowStartX + index * (noteSize + noteGap),
181+
y: gridStartY + rowIndex * (noteSize + noteGap),
182+
};
183+
});
71184
});
72185
});
73186
}
74187

188+
function getJourneyStepsLayout(sectionDef, sectionBox, styles = defaultStyles) {
189+
const resolvedStyles = { ...defaultStyles, ...styles };
190+
if (!sectionDef || !sectionDef.journeySteps || !sectionBox) {
191+
return null;
192+
}
193+
194+
const stepCount = 5;
195+
const stepWidth = Math.max(
196+
sectionBox.width / stepCount - 2 * resolvedStyles.padding,
197+
resolvedStyles.stickyNoteSize,
198+
);
199+
const stepHeight = resolvedStyles.stickyNoteSize;
200+
const stepY = sectionBox.y + sectionBox.height - stepHeight - (resolvedStyles.padding * 2);
201+
202+
const boxes = Array.from({ length: stepCount }, (_, index) => ({
203+
x: sectionBox.x + index * (stepWidth + 2 * resolvedStyles.stickyNoteSpacing),
204+
y: stepY,
205+
width: stepWidth,
206+
height: stepHeight,
207+
}));
208+
209+
const arrows = boxes.slice(0, -1).map((box, index) => {
210+
const nextBox = boxes[index + 1];
211+
const centerY = stepY + stepHeight / 2;
212+
213+
return {
214+
x1: box.x + stepWidth,
215+
y1: centerY,
216+
x2: nextBox.x,
217+
y2: centerY,
218+
};
219+
});
220+
221+
return { boxes, arrows };
222+
}
223+
75224
module.exports = {
76225
sanitizeInput,
77226
validateInput,
78227
distributeMissingPositions,
228+
getJourneyStepsLayout,
79229
};

0 commit comments

Comments
 (0)