Skip to content

Commit 989567b

Browse files
committed
improve svg quality
1 parent 0c56e6d commit 989567b

1 file changed

Lines changed: 43 additions & 29 deletions

File tree

apps/probe-viewer/src/utils/exportUtils.ts

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -347,13 +347,20 @@ function generateProbeSvgString(
347347

348348
const elements: string[] = [];
349349

350+
// Round emitted coordinates to 2 decimals: sub-pixel precision is invisible
351+
// but keeps the markup compact and readable.
352+
const r2 = (n: number) => Math.round(n * 100) / 100;
353+
350354
// Probe contour: technical line-art — a faint cool wash so the shank reads as
351355
// a region, with a thin precise outline. No fill gradient or shadow.
352356
if (probe.probe_planar_contour && probe.probe_planar_contour.length > 1) {
353357
const points = probe.probe_planar_contour
354-
.map((p) => projectPoint(p).join(","))
358+
.map((p) => {
359+
const [px, py] = projectPoint(p);
360+
return `${r2(px)},${r2(py)}`;
361+
})
355362
.join(" ");
356-
const strokeWidth = Math.max(1, Math.min(1.6, 2 * (scale / 120)));
363+
const strokeWidth = r2(Math.max(1, Math.min(1.6, 2 * (scale / 120))));
357364
elements.push(
358365
`<polygon points="${points}" fill="rgba(51, 65, 85, 0.05)" stroke="rgb(51, 65, 85)" stroke-opacity="0.9" stroke-width="${strokeWidth}" stroke-linejoin="round"/>`
359366
);
@@ -363,36 +370,35 @@ function generateProbeSvgString(
363370
const contactShapes = probe.contact_shapes ?? [];
364371
const contactShapeParams = probe.contact_shape_params ?? [];
365372

366-
// Helper to generate one contact SVG element: flat gold (the electrode
367-
// convention) with a defined bronze outline, no gradient or shadow.
368-
const contactStrokeWidth = Math.max(1, Math.min(1.8, 2.5 * (scale / 150)));
373+
// Helper to generate one contact's geometry. The flat-gold style (fill,
374+
// bronze outline) is applied once on the wrapping <g>, not per element.
375+
// Rectangular pads get lightly rounded corners.
369376
const generateContactSvg = (
370377
x: number,
371378
y: number,
372379
shape: string,
373380
params: ContactShapeParams
374381
): string => {
375-
const common = `fill="rgb(212, 175, 55)" stroke="rgb(110, 80, 25)" stroke-opacity="0.9" stroke-width="${contactStrokeWidth}"`;
376382
switch (shape) {
377383
case "circle": {
378384
const radius = (params.radius ?? 5) * scale;
379-
return `<circle cx="${x}" cy="${y}" r="${radius}" ${common}/>`;
385+
return `<circle cx="${r2(x)}" cy="${r2(y)}" r="${r2(radius)}"/>`;
380386
}
381387
case "square": {
382388
const side = (params.width ?? 10) * scale;
383-
const r = side * 0.12;
384-
return `<rect x="${x - side / 2}" y="${y - side / 2}" width="${side}" height="${side}" rx="${r}" ry="${r}" ${common}/>`;
389+
const rr = r2(side * 0.12);
390+
return `<rect x="${r2(x - side / 2)}" y="${r2(y - side / 2)}" width="${r2(side)}" height="${r2(side)}" rx="${rr}" ry="${rr}"/>`;
385391
}
386392
case "rect": {
387393
const w = (params.width ?? 10) * scale;
388394
const h = (params.height ?? 15) * scale;
389-
const r = Math.min(w, h) * 0.12;
390-
return `<rect x="${x - w / 2}" y="${y - h / 2}" width="${w}" height="${h}" rx="${r}" ry="${r}" ${common}/>`;
395+
const rr = r2(Math.min(w, h) * 0.12);
396+
return `<rect x="${r2(x - w / 2)}" y="${r2(y - h / 2)}" width="${r2(w)}" height="${r2(h)}" rx="${rr}" ry="${rr}"/>`;
391397
}
392398
default: {
393-
// Unknown shape: a small plain gold dot.
399+
// Unknown shape: a small plain dot.
394400
const markerSize = Math.max(3, Math.min(10, 7 * (scale / 100)));
395-
return `<circle cx="${x}" cy="${y}" r="${markerSize * 0.4}" ${common}/>`;
401+
return `<circle cx="${r2(x)}" cy="${r2(y)}" r="${r2(markerSize * 0.4)}"/>`;
396402
}
397403
}
398404
};
@@ -410,13 +416,21 @@ function generateProbeSvgString(
410416
y >= -frameMargin &&
411417
y <= heightPx + frameMargin;
412418

419+
// All contacts share one flat-gold style, set once on a group wrapper.
420+
const contactEls: string[] = [];
413421
contactPositions.forEach((position, index) => {
414422
const [x, y] = projectPoint(position);
415423
if (!isContactInFrame(x, y)) return;
416424
const shape = contactShapes[index] ?? "";
417425
const params = contactShapeParams[index] ?? {};
418-
elements.push(generateContactSvg(x, y, shape, params));
426+
contactEls.push(generateContactSvg(x, y, shape, params));
419427
});
428+
if (contactEls.length > 0) {
429+
const contactStrokeWidth = r2(Math.max(1, Math.min(1.8, 2.5 * (scale / 150))));
430+
elements.push(
431+
`<g fill="rgb(212, 175, 55)" stroke="rgb(110, 80, 25)" stroke-opacity="0.9" stroke-width="${contactStrokeWidth}">\n${contactEls.join("\n")}\n</g>`
432+
);
433+
}
420434

421435
// Scale bar (L-shaped, bottom-left corner)
422436
if (showScaleBar) {
@@ -434,26 +448,26 @@ function generateProbeSvgString(
434448
const tickSize = 4;
435449

436450
const label = scaleBarUm >= 1000 ? `${scaleBarUm / 1000} mm` : `${scaleBarUm} μm`;
437-
const strokeStyle = "rgba(15, 23, 42, 0.9)";
438-
439-
// L shape path
440-
elements.push(
441-
`<path d="M${cornerX},${cornerY} L${cornerX},${cornerY - scaleBarPixels} M${cornerX},${cornerY} L${cornerX + scaleBarPixels},${cornerY}" stroke="${strokeStyle}" stroke-width="2" stroke-linecap="square" fill="none"/>`
442-
);
443-
444-
// End ticks
445-
elements.push(
446-
`<path d="M${cornerX - tickSize},${cornerY - scaleBarPixels} L${cornerX + tickSize},${cornerY - scaleBarPixels} M${cornerX + scaleBarPixels},${cornerY - tickSize} L${cornerX + scaleBarPixels},${cornerY + tickSize}" stroke="${strokeStyle}" stroke-width="2" fill="none"/>`
447-
);
451+
const col = "rgba(15, 23, 42, 0.9)";
452+
const x0 = r2(cornerX);
453+
const y0 = r2(cornerY);
454+
const xEnd = r2(cornerX + scaleBarPixels);
455+
const yTop = r2(cornerY - scaleBarPixels);
448456

449-
// X label (below horizontal arm)
457+
// L shape + end ticks, sharing one stroke style on a group.
450458
elements.push(
451-
`<text x="${cornerX + scaleBarPixels / 2}" y="${cornerY + 16}" text-anchor="middle" font-family="Inter, sans-serif" font-size="11" fill="${strokeStyle}">${label}</text>`
459+
`<g stroke="${col}" stroke-width="2" fill="none">` +
460+
`<path d="M${x0},${y0} L${x0},${yTop} M${x0},${y0} L${xEnd},${y0}" stroke-linecap="square"/>` +
461+
`<path d="M${r2(cornerX - tickSize)},${yTop} L${r2(cornerX + tickSize)},${yTop} M${xEnd},${r2(cornerY - tickSize)} L${xEnd},${r2(cornerY + tickSize)}"/>` +
462+
`</g>`
452463
);
453464

454-
// Y label (rotated, to the left of vertical arm)
465+
// Both labels share one text style on a group.
455466
elements.push(
456-
`<text x="${cornerX - 6}" y="${cornerY - scaleBarPixels / 2}" text-anchor="middle" font-family="Inter, sans-serif" font-size="11" fill="${strokeStyle}" transform="rotate(-90, ${cornerX - 6}, ${cornerY - scaleBarPixels / 2})">${label}</text>`
467+
`<g fill="${col}" font-family="Inter, sans-serif" font-size="11" text-anchor="middle">` +
468+
`<text x="${r2(cornerX + scaleBarPixels / 2)}" y="${r2(cornerY + 16)}">${label}</text>` +
469+
`<text x="${r2(cornerX - 6)}" y="${r2(cornerY - scaleBarPixels / 2)}" transform="rotate(-90, ${r2(cornerX - 6)}, ${r2(cornerY - scaleBarPixels / 2)})">${label}</text>` +
470+
`</g>`
457471
);
458472
}
459473

0 commit comments

Comments
 (0)