Make conveyors/chutes/inductions/extendos/spurs white; fix PE stroke and size

- Change all conveyance SVGs from black fill to white fill with black stroke
- Update programmatic rendering (curves, induction, spur) to white fill
- Replace PE 3-slice rendering with programmatic canvas paths for
  consistent stroke width at any size
- Reduce PE default size from 56x20 to 30x14 to fit around conveyor devices
- Update SVG export to match new white fills

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 17:17:09 +04:00
parent 20b9547578
commit 775c6e2e99
10 changed files with 51 additions and 31 deletions

View File

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

View File

@ -186,7 +186,6 @@ function drawEpcSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
ctx.fillStyle = THEME.epcBody.rightBoxFill;
ctx.strokeStyle = THEME.epcBody.rightBoxStroke;
ctx.lineWidth = THEME.epcBody.rightBoxStrokeWidth;
ctx.rotate(-Math.PI / 2);
ctx.fillRect(-rb.w, -rb.h / 2, rb.w, rb.h);
ctx.strokeRect(-rb.w, -rb.h / 2, rb.w, rb.h);
ctx.restore();
@ -269,7 +268,7 @@ function traceEpcOutlinePath(ctx: CanvasRenderingContext2D, sym: PlacedSymbol, p
ctx.save();
ctx.translate(plx, ply);
ctx.rotate(rAngle - Math.PI / 2);
ctx.rotate(rAngle);
ctx.beginPath();
ctx.rect(-rb.w - pad, -rb.h / 2 - pad, rb.w + pad * 2, rb.h + pad * 2);
ctx.stroke();
@ -376,26 +375,47 @@ function drawInductionSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
ctx.closePath();
ctx.fillStyle = THEME.induction.fillColor;
ctx.strokeStyle = THEME.induction.strokeColor;
ctx.lineWidth = THEME.induction.lineWidth;
ctx.fill();
ctx.stroke();
}
/** Draw photoeye with 3-slice: fixed left cap, stretched middle beam, fixed right cap */
function drawPhotoeye3Slice(ctx: CanvasRenderingContext2D, sym: PlacedSymbol, img: HTMLImageElement) {
const { leftCap, rightCap, defaultWidth } = PHOTOEYE_CONFIG;
const srcW = img.naturalWidth;
const srcH = img.naturalHeight;
const scale = srcW / defaultWidth;
const srcLeftW = leftCap * scale;
const srcRightW = rightCap * scale;
const srcMiddleW = srcW - srcLeftW - srcRightW;
const dstMiddleW = sym.w - leftCap - rightCap;
/** Draw photoeye programmatically for consistent stroke at any size */
function drawPhotoeyeSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
const { leftCap, rightCap } = PHOTOEYE_CONFIG;
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
// Left cap (fixed)
ctx.drawImage(img, 0, 0, srcLeftW, srcH, sym.x, sym.y, leftCap, sym.h);
// Middle beam (stretched)
ctx.drawImage(img, srcLeftW, 0, srcMiddleW, srcH, sym.x + leftCap, sym.y, dstMiddleW, sym.h);
// Right cap (fixed)
ctx.drawImage(img, srcW - srcRightW, 0, srcRightW, srcH, sym.x + sym.w - rightCap, sym.y, rightCap, sym.h);
// Y positions as fractions of height (derived from original SVG path)
const beamTop = y + h * 0.42;
const beamBottom = y + h * 0.585;
const arrowInnerTop = y + h * 0.248;
const arrowInnerBottom = y + h * 0.744;
const recvTop = y + h * 0.181;
const recvBottom = y + h * 0.826;
const arrowTipTop = y + h * 0.05;
const arrowTipBottom = y + h * 0.948;
ctx.beginPath();
ctx.moveTo(x + leftCap, beamTop);
ctx.lineTo(x + leftCap, arrowInnerTop);
ctx.lineTo(x, arrowTipTop);
ctx.lineTo(x, arrowTipBottom);
ctx.lineTo(x + leftCap, arrowInnerBottom);
ctx.lineTo(x + leftCap, beamBottom);
ctx.lineTo(x + w - rightCap, beamBottom);
ctx.lineTo(x + w - rightCap, recvBottom);
ctx.lineTo(x + w, recvBottom);
ctx.lineTo(x + w, recvTop);
ctx.lineTo(x + w - rightCap, recvTop);
ctx.lineTo(x + w - rightCap, beamTop);
ctx.closePath();
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.fill();
ctx.stroke();
}
/** Draw curved conveyor/chute programmatically with fixed band width */
@ -409,7 +429,7 @@ function drawCurvedSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
ctx.arc(arcCx, arcCy, innerR, -sweepRad, 0, false);
ctx.closePath();
ctx.fillStyle = '#000000';
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.fill();
@ -432,20 +452,18 @@ function drawSymbolBody(ctx: CanvasRenderingContext2D, sym: PlacedSymbol): boole
ctx.lineTo(sym.x + sym.w, sym.y + sym.h);
ctx.lineTo(sym.x, sym.y + sym.h);
ctx.closePath();
ctx.fillStyle = '#000000';
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.5;
ctx.fill();
ctx.stroke();
} else if (isPhotoeyeType(sym.symbolId)) {
drawPhotoeyeSymbol(ctx, sym);
} else {
const img = getSymbolImage(sym.file);
if (!img) return false;
if (isPhotoeyeType(sym.symbolId)) {
drawPhotoeye3Slice(ctx, sym, img);
} else {
ctx.drawImage(img, sym.x, sym.y, sym.w, sym.h);
}
}
return true;
}

View File

@ -52,7 +52,7 @@ 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="#000000"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
} 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);
@ -69,11 +69,11 @@ export async function exportSVG() {
`A ${innerR},${innerR} 0 ${largeArc},1 ${arcCx + innerR},${arcCy}`,
'Z',
].join(' ');
lines.push(` <path ${idAttr} d="${d}" fill="#000000" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
} 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="#000000" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
} else {
// Regular SVG symbol
try {

View File

@ -36,8 +36,8 @@ export const SYMBOLS: SymbolDef[] = [
{ id: 'fio_sio_fioh_v', name: 'FIO/SIO/FIOH (V)', file: '/symbols/fio_sio_fioh.svg', w: 14, h: 20, defaultRotation: 90, group: 'I/O Modules' },
// --- Sensors ---
{ id: 'photoeye', name: 'Photoeye', file: '/symbols/photoeye.svg', w: 56, h: 20, group: 'Sensors' },
{ id: 'photoeye_v', name: 'Photoeye (V)', file: '/symbols/photoeye.svg', w: 56, h: 20, defaultRotation: 90, group: 'Sensors' },
{ id: 'photoeye', name: 'Photoeye', file: '/symbols/photoeye.svg', w: 30, h: 14, group: 'Sensors' },
{ id: 'photoeye_v', name: 'Photoeye (V)', file: '/symbols/photoeye.svg', w: 30, h: 14, defaultRotation: 90, group: 'Sensors' },
{ id: 'pressure_sensor', name: 'Pressure Sensor', file: '/symbols/pressure_sensor.svg', w: 20, h: 20, group: 'Sensors' },
{ id: 'pressure_sensor_v', name: 'Pressure Sensor (V)', file: '/symbols/pressure_sensor.svg', w: 20, h: 20, defaultRotation: 90, group: 'Sensors' },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 367 B