From 701ed61a824df64b61ddbab96444b48087c421fe Mon Sep 17 00:00:00 2001 From: Ilya Ashikhmin Date: Thu, 2 Jul 2026 23:43:04 +0200 Subject: [PATCH] fix(icons): emit valid single-fill SVG so node icons render as image resources Icon markup had duplicate 'fill' attributes (white body + stroke's fill:none on one element). Inline SVG parses leniently, but as an / data-URI the SVG is parsed as strict XML where duplicate attributes are fatal, showing broken icons on the canvas. Split fill into FILL/NONE/SOLID so each element declares exactly one fill, and add width/height for intrinsic sizing. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/canvas/shapes.ts | 5 ++++- src/model/svgIcons.ts | 46 +++++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/canvas/shapes.ts b/src/canvas/shapes.ts index 2c5b43e..9654c5d 100644 --- a/src/canvas/shapes.ts +++ b/src/canvas/shapes.ts @@ -117,7 +117,10 @@ export function createNode(opts: CreateNodeOptions): dia.Element { size: { ...type.size }, }); - node.attr("icon/xlinkHref", iconDataUri(type.icon)); + // Set both href (SVG2) and xlink:href (legacy) for maximum renderer support. + const iconUri = iconDataUri(type.icon); + node.attr("icon/xlinkHref", iconUri); + node.attr("icon/href", iconUri); const props = { ...buildDefaults(type.properties), ...(opts.props ?? {}) }; node.attr("label/text", (props.title as string) || type.label); node.attr("accent/fill", (props.color as string) || "#2f80ed"); diff --git a/src/model/svgIcons.ts b/src/model/svgIcons.ts index b68f452..b07a0b4 100644 --- a/src/model/svgIcons.ts +++ b/src/model/svgIcons.ts @@ -4,65 +4,73 @@ * an icon by key; the canvas renders it as an via a data URI, keeping * node geometry and the vector art decoupled — exactly the "nodes are defined * by an SVG set with port configuration" model. + * + * IMPORTANT: markup must be valid XML. When an SVG is loaded as an image + * resource (data URI) it is parsed strictly, so every element may carry at most + * ONE `fill` attribute. Use FILL (white body), NONE (open line paths), or SOLID. */ -const S = 'stroke="#2b2f36" stroke-width="2.5" fill="none" stroke-linejoin="round" stroke-linecap="round"'; +// Stroke styling WITHOUT a fill, so each shape declares its own fill exactly once. +const S = 'stroke="#2b2f36" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"'; const FILL = 'fill="#ffffff"'; +const NONE = 'fill="none"'; +const SOLID = 'fill="#2b2f36"'; function svg(inner: string): string { - return `${inner}`; + // Explicit width/height give the SVG an intrinsic size so it renders when + // referenced from an SVG element (not just an HTML ). + return `${inner}`; } export const SVG_ICONS: Record = { source: svg( `` + `` + - ``, + ``, ), sink: svg( `` + - ``, + ``, ), tank: svg( `` + - `` + - ``, + `` + + ``, ), pump: svg( `` + - `` + - ``, + `` + + ``, ), valve: svg( `` + `` + - ``, + ``, ), junction: svg( - `` + - ``, + `` + + ``, ), filter: svg( `` + - ``, + ``, ), meter: svg( `` + - `` + - `` + - ``, + `` + + `` + + ``, ), heater: svg( `` + - ``, + ``, ), elbow: svg( - `` + - ``, + ``, ), compressor: svg( `` + - ``, + ``, ), };