Fix SVG export: group shape+text, preserve rotation, stroke-width 1px

- Wrap each conveyance shape + its label text in a <g> element so
  rotation/mirror transforms apply to both shape and text together
- Change stroke-width from 0.5 to 1 on all conveyance and PE shapes
  (both canvas renderer and SVG export)
- Text is now inside the group, inheriting the transform — no more
  floating unrotated labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 21:06:49 +04:00
parent e3a0e422e6
commit ea367df42a
3 changed files with 29 additions and 19 deletions

View File

@ -60,7 +60,7 @@ export const THEME = {
induction: {
fillColor: '#ffffff',
strokeColor: '#000000',
lineWidth: 0.5,
lineWidth: 1,
},
canvas: {
maxRenderScale: 4,

View File

@ -438,7 +438,7 @@ function drawPhotoeyeSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
}
@ -456,7 +456,7 @@ function drawCurvedSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
}
@ -479,7 +479,7 @@ function drawSymbolBody(ctx: CanvasRenderingContext2D, sym: PlacedSymbol): boole
ctx.closePath();
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
} else if (isExtendoType(sym.symbolId)) {
@ -503,13 +503,13 @@ function drawSymbolBody(ctx: CanvasRenderingContext2D, sym: PlacedSymbol): boole
ctx.closePath();
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
} else if (isRectConveyanceType(sym.symbolId)) {
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.lineWidth = 1;
ctx.fillRect(sym.x, sym.y, sym.w, sym.h);
ctx.strokeRect(sym.x, sym.y, sym.w, sym.h);
} else if (isPhotoeyeType(sym.symbolId)) {

View File

@ -17,8 +17,8 @@ function parseConveyanceLabel(label: string): { lines: string[]; stripped: strin
return { lines: [core], stripped: [core] };
}
/** Emit conveyance label text — absolute coordinates, no transforms (Ignition compatible) */
function emitConveyanceLabel(lines: string[], sym: PlacedSymbol, _outerTransform: string) {
/** Emit conveyance label text inside a <g> — no transform needed (inherits from group) */
function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) {
if (!sym.label) return;
const { lines: textLines } = parseConveyanceLabel(sym.label);
@ -134,8 +134,10 @@ export async function exportSVG() {
const stripBottomY = sym.y + sym.h * INDUCTION_CONFIG.stripBottomFrac;
const pts = INDUCTION_CONFIG.arrowPoints.map(([xf, yf]) => [sym.x + xf * hw, sym.y + yf * sym.h] as const);
const d = `M ${sym.x + sym.w},${stripTopY} L ${pts[0][0]},${stripTopY} ${pts.map(([px, py]) => `L ${px},${py}`).join(' ')} L ${pts[5][0]},${stripBottomY} L ${sym.x + sym.w},${stripBottomY} Z`;
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
lines.push(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
lines.push(` </g>`);
} else if (isCurvedType(sym.symbolId)) {
const angle = sym.curveAngle || 90;
const { arcCx, arcCy, outerR, innerR } = getCurveGeometry(sym.symbolId, sym.x, sym.y, sym.w, sym.h);
@ -152,16 +154,22 @@ export async function exportSVG() {
`A ${innerR},${innerR} 0 ${largeArc},1 ${arcCx + innerR},${arcCy}`,
'Z',
].join(' ');
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
lines.push(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
lines.push(` </g>`);
} else if (isSpurType(sym.symbolId)) {
const w2 = sym.w2 ?? sym.w;
const d = `M ${sym.x},${sym.y} L ${sym.x + w2},${sym.y} L ${sym.x + sym.w},${sym.y + sym.h} L ${sym.x},${sym.y + sym.h} Z`;
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
lines.push(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
lines.push(` </g>`);
} else if (isRectConveyanceType(sym.symbolId)) {
lines.push(` <rect ${idAttr} x="${sym.x}" y="${sym.y}" width="${sym.w}" height="${sym.h}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
lines.push(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
lines.push(` <rect x="${sym.x}" y="${sym.y}" width="${sym.w}" height="${sym.h}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
lines.push(` </g>`);
} else if (isExtendoType(sym.symbolId)) {
const bracketW = 10.6 / 31.07 * 73;
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
@ -180,8 +188,10 @@ export async function exportSVG() {
[x + bracketW * 0.34, y + h * 0.016],
];
const d = `M ${pts[0][0]},${pts[0][1]} ` + pts.slice(1).map(p => `L ${p[0]},${p[1]}`).join(' ') + ' Z';
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
lines.push(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
lines.push(` </g>`);
} else if (isPhotoeyeType(sym.symbolId)) {
const { leftCap, rightCap } = PHOTOEYE_CONFIG;
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
@ -200,7 +210,7 @@ export async function exportSVG() {
[x + w - rightCap, y + h * 0.42],
];
const d = `M ${pts[0][0]},${pts[0][1]} ` + pts.slice(1).map(p => `L ${p[0]},${p[1]}`).join(' ') + ' Z';
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
} else {
// Regular SVG symbol
try {