diff --git a/projectes/CDW5/pdf/CDW5_SYSDL_MCM09 Non Con PH1-SYSDL.pdf b/projectes/CDW5/pdf/CDW5_SYSDL_MCM09 Non Con PH1-SYSDL.pdf new file mode 100644 index 0000000..a69e53b Binary files /dev/null and b/projectes/CDW5/pdf/CDW5_SYSDL_MCM09 Non Con PH1-SYSDL.pdf differ diff --git a/svelte-app/src/lib/canvas/collision.ts b/svelte-app/src/lib/canvas/collision.ts index e7d2ffd..b9e6534 100644 --- a/svelte-app/src/lib/canvas/collision.ts +++ b/svelte-app/src/lib/canvas/collision.ts @@ -1,5 +1,5 @@ import type { AABB, EpcWaypoint } from '../types.js'; -import { SPACING_EXEMPT, isCurvedType, isSpurType, isEpcType, getCurveGeometry, EPC_CONFIG } from '../symbols.js'; +import { SPACING_EXEMPT, isCurvedType, isSpurType, isEpcType, isInductionType, getCurveGeometry, EPC_CONFIG, INDUCTION_CONFIG } from '../symbols.js'; import { layout } from '../stores/layout.svelte.js'; import { orientedBoxCorners, createCurveTransforms } from './geometry.js'; import { getAABB, distPointToSegment, distToAnnularSector, pointToOBBDist, pointInConvexPolygon, pointToConvexPolygonDist, edgeDistance } from './distance.js'; @@ -63,6 +63,34 @@ function getSpurVertices( }); } +/** Get the convex hull vertices of an induction shape (arrow + strip) in world space */ +function getInductionVertices( + x: number, y: number, w: number, h: number, rotation: number +): [number, number][] { + const hw = INDUCTION_CONFIG.headWidth; + const stripTopY = y + h * INDUCTION_CONFIG.stripTopFrac; + const stripBottomY = y + h * INDUCTION_CONFIG.stripBottomFrac; + const arrowPts = INDUCTION_CONFIG.arrowPoints; + // Convex hull: top-right strip, arrow top, arrow left, arrow bottom, bottom-right strip + const local: [number, number][] = [ + [x + w, stripTopY], // strip top-right + [x + arrowPts[0][0] * hw, y + arrowPts[0][1] * h], // arrow top junction + [x + arrowPts[1][0] * hw, y + arrowPts[1][1] * h], // arrow top + [x + arrowPts[2][0] * hw, y + arrowPts[2][1] * h], // arrow left point + [x + arrowPts[3][0] * hw, y + arrowPts[3][1] * h], // arrow bottom + [x + arrowPts[5][0] * hw, y + arrowPts[5][1] * h], // arrow bottom junction + [x + w, stripBottomY], // strip bottom-right + ]; + if (!rotation) return local; + const cx = x + w / 2, cy = y + h / 2; + const rad = rotation * Math.PI / 180; + const cos = Math.cos(rad), sin = Math.sin(rad); + return local.map(([px, py]) => { + const dx = px - cx, dy = py - cy; + return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos] as [number, number]; + }); +} + /** Get the 4 vertices of an OBB in world space */ function getOBBVertices( x: number, y: number, w: number, h: number, rotation: number @@ -406,6 +434,16 @@ function anyQuadVsCurved( return false; } +/** Get collision vertices for any non-curved, non-EPC symbol based on its actual shape */ +function getShapeVertices( + x: number, y: number, w: number, h: number, rotation: number, + symbolId: string, w2?: number +): [number, number][] { + if (isSpurType(symbolId)) return getSpurVertices(x, y, w, h, w2 ?? w, rotation); + if (isInductionType(symbolId)) return getInductionVertices(x, y, w, h, rotation); + return getOBBVertices(x, y, w, h, rotation); +} + export function checkSpacingViolation( id: number, x: number, y: number, w: number, h: number, rotation: number, symbolId?: string, curveAngle?: number, w2?: number @@ -416,6 +454,7 @@ export function checkSpacingViolation( const isCurved = !!(symbolId && isCurvedType(symbolId)); const isSpur = !!(symbolId && isSpurType(symbolId)); const isEpc = !!(symbolId && isEpcType(symbolId)); + const isInduction = !!(symbolId && isInductionType(symbolId)); // Get EPC quads for "this" symbol if it's an EPC let epcQuads: [number, number][][] | null = null; @@ -431,6 +470,7 @@ export function checkSpacingViolation( const symIsCurved = isCurvedType(sym.symbolId); const symIsSpur = isSpurType(sym.symbolId); const symIsEpc = isEpcType(sym.symbolId); + const symIsInduction = isInductionType(sym.symbolId); // Get EPC quads for "other" symbol if it's EPC let symEpcQuads: [number, number][][] | null = null; @@ -455,68 +495,32 @@ export function checkSpacingViolation( continue; } - // ── EPC vs Spur ── - if (isEpc && symIsSpur && epcQuads) { - const spurVerts = getSpurVertices(sym.x, sym.y, sym.w, sym.h, sym.w2 ?? sym.w, sym.rotation); - if (anyQuadVsPolygon(epcQuads, spurVerts, spacing)) return true; - continue; - } - if (isSpur && symIsEpc && symEpcQuads) { - const spurVerts = getSpurVertices(x, y, w, h, w2 ?? w, rotation); - if (anyQuadVsPolygon(symEpcQuads, spurVerts, spacing)) return true; - continue; - } - - // ── EPC vs OBB (regular symbol) ── - if (isEpc && epcQuads && !symIsCurved && !symIsSpur && !symIsEpc) { - const bVerts = getOBBVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation); + // ── EPC vs non-curved/non-EPC ── + if (isEpc && epcQuads && !symIsCurved && !symIsEpc) { + const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2); if (anyQuadVsPolygon(epcQuads, bVerts, spacing)) return true; continue; } - if (symIsEpc && symEpcQuads && !isCurved && !isSpur && !isEpc) { - const aVerts = getOBBVertices(x, y, w, h, rotation); + if (symIsEpc && symEpcQuads && !isCurved && !isEpc) { + const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2); if (anyQuadVsPolygon(symEpcQuads, aVerts, spacing)) return true; continue; } - // ── Non-EPC logic (existing) ── + // ── Non-EPC, non-curved: shape polygon vs shape polygon ── if (!isCurved && !symIsCurved) { - if (isSpur || symIsSpur) { - const aVerts = isSpur - ? getSpurVertices(x, y, w, h, w2 ?? w, rotation) - : getOBBVertices(x, y, w, h, rotation); - const bVerts = symIsSpur - ? getSpurVertices(sym.x, sym.y, sym.w, sym.h, sym.w2 ?? sym.w, sym.rotation) - : getOBBVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation); - if (polygonSATWithSpacing(aVerts, bVerts, spacing)) return true; - } else { - if (obbOverlapWithSpacing(x, y, w, h, rotation, sym.x, sym.y, sym.w, sym.h, sym.rotation, spacing)) { - return true; - } - } + const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2); + const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2); + if (polygonSATWithSpacing(aVerts, bVerts, spacing)) return true; } else if (isCurved && !symIsCurved) { - if (symIsSpur) { - const spurVerts = getSpurVertices(sym.x, sym.y, sym.w, sym.h, sym.w2 ?? sym.w, sym.rotation); - if (checkCurvedVsSpur( - { x, y, w, h, rotation, symbolId: symbolId!, curveAngle }, - spurVerts, spacing - )) return true; - } else { - if (checkCurvedVsOBB( - { x, y, w, h, rotation, symbolId: symbolId!, curveAngle }, - sym.x, sym.y, sym.w, sym.h, sym.rotation, spacing - )) return true; - } + const bVerts = getShapeVertices(sym.x, sym.y, sym.w, sym.h, sym.rotation, sym.symbolId, sym.w2); + if (checkCurvedVsSpur( + { x, y, w, h, rotation, symbolId: symbolId!, curveAngle }, + bVerts, spacing + )) return true; } else if (!isCurved && symIsCurved) { - if (isSpur) { - const spurVerts = getSpurVertices(x, y, w, h, w2 ?? w, rotation); - if (checkCurvedVsSpur(sym, spurVerts, spacing)) return true; - } else { - if (checkCurvedVsOBB( - sym, - x, y, w, h, rotation, spacing - )) return true; - } + const aVerts = getShapeVertices(x, y, w, h, rotation, symbolId!, w2); + if (checkCurvedVsSpur(sym, aVerts, spacing)) return true; } else { // Both curved: use band AABBs as fallback const aBoxes = getCurvedBandAABBs({ x, y, w, h, rotation, symbolId: symbolId!, curveAngle }); diff --git a/svelte-app/src/lib/canvas/hit-testing.ts b/svelte-app/src/lib/canvas/hit-testing.ts index ae41b50..32c875b 100644 --- a/svelte-app/src/lib/canvas/hit-testing.ts +++ b/svelte-app/src/lib/canvas/hit-testing.ts @@ -120,6 +120,39 @@ export function hitResizeHandle(cx: number, cy: number, sym: PlacedSymbol): 'lef return null; } +/** Check if a point is inside an arc band (annular sector) */ +function pointInArcBand(px: number, py: number, sym: PlacedSymbol): boolean { + const angle = sym.curveAngle || 90; + const { arcCx, arcCy, outerR, innerR } = getCurveGeometry(sym.symbolId, sym.x, sym.y, sym.w, sym.h); + const dx = px - arcCx, dy = -(py - arcCy); // flip Y to math convention (Y up) + const r = Math.sqrt(dx * dx + dy * dy); + if (r < innerR || r > outerR) return false; + let theta = Math.atan2(dy, dx); + if (theta < -0.01) theta += 2 * Math.PI; + const sweepRad = (angle * Math.PI) / 180; + return theta >= -0.01 && theta <= sweepRad + 0.01; +} + +/** Check if a point is inside the induction shape (arrow head + strip) */ +function pointInInduction(px: number, py: number, sym: PlacedSymbol): boolean { + const hw = INDUCTION_CONFIG.headWidth; + const stripTopY = sym.y + sym.h * INDUCTION_CONFIG.stripTopFrac; + const stripBottomY = sym.y + sym.h * INDUCTION_CONFIG.stripBottomFrac; + // Check strip rect + if (px >= sym.x + hw * 0.55 && px <= sym.x + sym.w && py >= stripTopY && py <= stripBottomY) return true; + // Check arrow head (approximate as bounding rect of arrow points) + if (px >= sym.x && px <= sym.x + hw && py >= sym.y && py <= sym.y + sym.h) return true; + return false; +} + +/** Hit test a single symbol against its actual shape */ +function pointInSymbol(px: number, py: number, sym: PlacedSymbol): boolean { + if (isCurvedType(sym.symbolId)) return pointInArcBand(px, py, sym); + if (isSpurType(sym.symbolId)) return pointInTrapezoid(px, py, sym); + if (isInductionType(sym.symbolId)) return pointInInduction(px, py, sym); + return pointInRect(px, py, sym.x, sym.y, sym.w, sym.h); +} + /** Hit test all symbols, returning the id of the topmost hit, or null */ export function hitTestSymbols( cx: number, cy: number, @@ -128,11 +161,7 @@ export function hitTestSymbols( for (let i = symbols.length - 1; i >= 0; i--) { const sym = symbols[i]; const local = toSymbolLocal(cx, cy, sym); - if (isSpurType(sym.symbolId)) { - if (pointInTrapezoid(local.x, local.y, sym)) return sym.id; - } else if (pointInRect(local.x, local.y, sym.x, sym.y, sym.w, sym.h)) { - return sym.id; - } + if (pointInSymbol(local.x, local.y, sym)) return sym.id; } return null; } diff --git a/svelte-app/src/lib/export.ts b/svelte-app/src/lib/export.ts index 0a517a0..445357a 100644 --- a/svelte-app/src/lib/export.ts +++ b/svelte-app/src/lib/export.ts @@ -3,19 +3,45 @@ import { isEpcType, isInductionType, isSpurType, isCurvedType, EPC_CONFIG, INDUC import { deserializeSymbol } from './serialization.js'; import type { PlacedSymbol } from './types.js'; -/** If the SVG root has a single child (with only a transform attr), unwrap it. */ -function unwrapSingleGroup(svgEl: Element): { content: string; extraTransform: string } { - const children = Array.from(svgEl.children); - if (children.length === 1 && children[0].tagName === 'g') { - const innerG = children[0]; - if (Array.from(innerG.attributes).every(a => a.name === 'transform')) { - return { - content: innerG.innerHTML, - extraTransform: innerG.getAttribute('transform') || '' - }; +/** Recursively collect all leaf (non-) elements from an SVG element tree, + * composing transforms as we descend through nodes. */ +function collectLeaves(el: Element, parentTransform: string): { el: Element; transform: string }[] { + const results: { el: Element; transform: string }[] = []; + for (const child of Array.from(el.children)) { + const childTransform = child.getAttribute('transform') || ''; + const composed = [parentTransform, childTransform].filter(Boolean).join(' '); + if (child.tagName === 'g') { + // Recurse into groups, composing their transform + results.push(...collectLeaves(child, composed)); + } else { + results.push({ el: child, transform: composed }); } } - return { content: svgEl.innerHTML, extraTransform: '' }; + return results; +} + +/** Serialize an element to string, optionally overriding its transform, + * and stripping the default xmlns that XMLSerializer adds. */ +function serializeLeaf(el: Element, transform: string, id?: string, inkscapeLabel?: string): string { + const clone = el.cloneNode(true) as Element; + if (transform) { + clone.setAttribute('transform', transform); + } else { + clone.removeAttribute('transform'); + } + if (id) { + clone.setAttribute('id', id); + } + // inkscape:label via setAttributeNS won't serialize correctly with XMLSerializer, + // so we inject it as a string afterwards + let s = new XMLSerializer().serializeToString(clone) + .replace(/ xmlns="http:\/\/www\.w3\.org\/2000\/svg"/g, ''); + if (inkscapeLabel) { + // Inject inkscape:label right after the tag name + const firstSpace = s.indexOf(' '); + s = s.slice(0, firstSpace) + ` inkscape:label="${inkscapeLabel}"` + s.slice(firstSpace); + } + return s; } function downloadBlob(blob: Blob, filename: string) { @@ -27,57 +53,6 @@ function downloadBlob(blob: Blob, filename: string) { URL.revokeObjectURL(url); } -async function buildEpcSvgElements(sym: PlacedSymbol): Promise { - const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints; - const ox = sym.x; - const oy = sym.y; - const parts: string[] = []; - - // Polyline - if (waypoints.length >= 2) { - const points = waypoints.map(wp => `${ox + wp.x},${oy + wp.y}`).join(' '); - parts.push(` `); - } - - if (waypoints.length >= 2) { - // Left icon — embed actual SVG, oriented along first segment - const lb = EPC_CONFIG.leftBox; - const p0x = ox + waypoints[0].x, p0y = oy + waypoints[0].y; - const p1x = ox + waypoints[1].x, p1y = oy + waypoints[1].y; - const lAngle = Math.atan2(p1y - p0y, p1x - p0x) * 180 / Math.PI; - - try { - const svgText = await (await fetch(EPC_CONFIG.iconFile)).text(); - const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); - const svgEl = doc.documentElement; - const vb = svgEl.getAttribute('viewBox'); - const [vbX, vbY, vbW, vbH] = vb ? vb.split(/[\s,]+/).map(Number) : [0, 0, lb.w, lb.h]; - const sx = lb.w / vbW; - const sy = lb.h / vbH; - const { content, extraTransform } = unwrapSingleGroup(svgEl); - let transform = `translate(${p0x},${p0y}) rotate(${lAngle.toFixed(2)}) translate(${-lb.w},${-lb.h / 2}) scale(${sx.toFixed(6)},${sy.toFixed(6)}) translate(${-vbX},${-vbY})`; - if (extraTransform) transform += ` ${extraTransform}`; - parts.push(` `); - parts.push(` ${content}`); - parts.push(` `); - } catch { - // Fallback: plain rect - parts.push(` `); - } - - // Right box — oriented along last segment, left-center at wp[last] - const last = waypoints[waypoints.length - 1]; - const prev = waypoints[waypoints.length - 2]; - const plx = ox + last.x, ply = oy + last.y; - const ppx = ox + prev.x, ppy = oy + prev.y; - const rAngle = Math.atan2(ply - ppy, plx - ppx) * 180 / Math.PI; - const rb = EPC_CONFIG.rightBox; - parts.push(` `); - } - - return parts.join('\n'); -} - export async function exportSVG() { const lines: string[] = [ '', @@ -92,31 +67,29 @@ export async function exportSVG() { const mirrored = sym.mirrored || false; const cx = sym.x + sym.w / 2; const cy = sym.y + sym.h / 2; - let outerTransformParts: string[] = []; - if (rot) outerTransformParts.push(`rotate(${rot},${cx},${cy})`); - if (mirrored) outerTransformParts.push(`translate(${cx},0) scale(-1,1) translate(${-cx},0)`); - const rotAttr = outerTransformParts.length ? ` transform="${outerTransformParts.join(' ')}"` : ''; const label = sym.label || sym.name; - const idAttr = ` id="${label}" inkscape:label="${label}"`; + + // Build outer transform (rotation + mirror) + const outerParts: string[] = []; + if (rot) outerParts.push(`rotate(${rot},${cx},${cy})`); + if (mirrored) outerParts.push(`translate(${cx},0) scale(-1,1) translate(${-cx},0)`); + const outerTransform = outerParts.join(' '); if (isEpcType(sym.symbolId)) { - lines.push(` `); - lines.push(await buildEpcSvgElements(sym as PlacedSymbol)); - lines.push(' '); + // EPC: polyline + icon + right box — emit flat elements + await emitEpcFlat(lines, sym as PlacedSymbol, label, outerTransform); } else if (isInductionType(sym.symbolId)) { const hw = INDUCTION_CONFIG.headWidth; const stripTopY = sym.y + sym.h * INDUCTION_CONFIG.stripTopFrac; 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(` `); - lines.push(` `); - lines.push(' '); + const t = outerTransform || undefined; + lines.push(` `); } 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); const sweepRad = (angle * Math.PI) / 180; - // Outer arc: from angle=0 to angle=-sweep (CCW in SVG = large-arc with sweep-flag 0) const outerEndX = arcCx + outerR * Math.cos(sweepRad); const outerEndY = arcCy - outerR * Math.sin(sweepRad); const innerEndX = arcCx + innerR * Math.cos(sweepRad); @@ -129,21 +102,19 @@ export async function exportSVG() { `A ${innerR},${innerR} 0 ${largeArc},1 ${arcCx + innerR},${arcCy}`, 'Z', ].join(' '); - lines.push(` `); - lines.push(` `); - lines.push(' '); + const t = outerTransform || undefined; + lines.push(` `); } else if (isSpurType(sym.symbolId)) { const w2 = sym.w2 ?? sym.w; - const points = `${sym.x},${sym.y} ${sym.x + w2},${sym.y} ${sym.x + sym.w},${sym.y + sym.h} ${sym.x},${sym.y + sym.h}`; - lines.push(` `); - lines.push(` `); - lines.push(' '); + 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`; + const t = outerTransform || undefined; + lines.push(` `); } else { + // Regular SVG symbol — flatten all children try { const svgText = await (await fetch(sym.file)).text(); const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); const svgEl = doc.documentElement; - // Skip if DOMParser returned an error (e.g., 404 or invalid SVG) if (svgEl.querySelector('parsererror')) { console.error('SVG parse error for:', sym.file); continue; @@ -155,19 +126,26 @@ export async function exportSVG() { const sx = sym.w / vbW; const sy = sym.h / vbH; - const { content, extraTransform } = unwrapSingleGroup(svgEl); - let transform = `translate(${sym.x},${sym.y}) scale(${sx.toFixed(6)},${sy.toFixed(6)}) translate(${-vbX},${-vbY})`; - if (extraTransform) transform += ` ${extraTransform}`; + // Base transform: position + scale from viewBox + let baseTransform = `translate(${sym.x},${sym.y}) scale(${sx.toFixed(6)},${sy.toFixed(6)}) translate(${-vbX},${-vbY})`; if (mirrored) { - transform = `translate(${cx},0) scale(-1,1) translate(${-cx},0) ${transform}`; + baseTransform = `translate(${cx},0) scale(-1,1) translate(${-cx},0) ${baseTransform}`; } if (rot) { - transform = `rotate(${rot},${cx},${cy}) ${transform}`; + baseTransform = `rotate(${rot},${cx},${cy}) ${baseTransform}`; } - lines.push(` `); - lines.push(` ${content}`); - lines.push(' '); + // Collect all leaf elements with composed transforms + const leaves = collectLeaves(svgEl, baseTransform); + + for (let i = 0; i < leaves.length; i++) { + const leaf = leaves[i]; + if (i === 0) { + lines.push(` ${serializeLeaf(leaf.el, leaf.transform, label, label)}`); + } else { + lines.push(` ${serializeLeaf(leaf.el, leaf.transform)}`); + } + } } catch (err) { console.error('Failed to embed symbol:', sym.name, err); } @@ -178,6 +156,74 @@ export async function exportSVG() { downloadBlob(new Blob([lines.join('\n')], { type: 'image/svg+xml' }), 'test_view.svg'); } +/** Emit EPC symbol elements flat (no group wrapper) */ +async function emitEpcFlat(lines: string[], sym: PlacedSymbol, label: string, outerTransform: string) { + const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints; + const ox = sym.x; + const oy = sym.y; + let isFirst = true; + + const idAttrs = (addId: boolean) => { + if (!addId) return ''; + return ` id="${label}" inkscape:label="${label}"`; + }; + const tAttr = (t: string) => t ? ` transform="${t}"` : ''; + + // Polyline as path + if (waypoints.length >= 2) { + const d = 'M ' + waypoints.map(wp => `${ox + wp.x},${oy + wp.y}`).join(' L '); + lines.push(` `); + isFirst = false; + } + + if (waypoints.length >= 2) { + // Left icon + const lb = EPC_CONFIG.leftBox; + const p0x = ox + waypoints[0].x, p0y = oy + waypoints[0].y; + const p1x = ox + waypoints[1].x, p1y = oy + waypoints[1].y; + const lAngle = Math.atan2(p1y - p0y, p1x - p0x) * 180 / Math.PI; + + try { + const svgText = await (await fetch(EPC_CONFIG.iconFile)).text(); + const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); + const svgEl = doc.documentElement; + const vb = svgEl.getAttribute('viewBox'); + const [vbX, vbY, vbW, vbH] = vb ? vb.split(/[\s,]+/).map(Number) : [0, 0, lb.w, lb.h]; + const sx = lb.w / vbW; + const sy = lb.h / vbH; + + let iconBase = `translate(${p0x},${p0y}) rotate(${lAngle.toFixed(2)}) translate(${-lb.w},${-lb.h / 2}) scale(${sx.toFixed(6)},${sy.toFixed(6)}) translate(${-vbX},${-vbY})`; + if (outerTransform) iconBase = `${outerTransform} ${iconBase}`; + + const leaves = collectLeaves(svgEl, iconBase); + for (const leaf of leaves) { + lines.push(` ${serializeLeaf(leaf.el, leaf.transform, isFirst ? label : undefined, isFirst ? label : undefined)}`); + isFirst = false; + } + } catch { + const t = outerTransform + ? `${outerTransform} translate(${p0x},${p0y}) rotate(${lAngle.toFixed(2)})` + : `translate(${p0x},${p0y}) rotate(${lAngle.toFixed(2)})`; + const d = `M ${-lb.w},${-lb.h / 2} L 0,${-lb.h / 2} L 0,${lb.h / 2} L ${-lb.w},${lb.h / 2} Z`; + lines.push(` `); + isFirst = false; + } + + // Right box + const last = waypoints[waypoints.length - 1]; + const prev = waypoints[waypoints.length - 2]; + const plx = ox + last.x, ply = oy + last.y; + const ppx = ox + prev.x, ppy = oy + prev.y; + const rAngle = Math.atan2(ply - ppy, plx - ppx) * 180 / Math.PI; + const rb = EPC_CONFIG.rightBox; + const rTransform = outerTransform + ? `${outerTransform} translate(${plx},${ply}) rotate(${rAngle.toFixed(2)})` + : `translate(${plx},${ply}) rotate(${rAngle.toFixed(2)})`; + const d = `M 0,${-rb.h / 2} L ${rb.w},${-rb.h / 2} L ${rb.w},${rb.h / 2} L 0,${rb.h / 2} Z`; + lines.push(` `); + } +} + export function loadLayoutJSON(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); diff --git a/svelte-app/src/lib/symbols.ts b/svelte-app/src/lib/symbols.ts index 7559134..053e480 100644 --- a/svelte-app/src/lib/symbols.ts +++ b/svelte-app/src/lib/symbols.ts @@ -82,9 +82,10 @@ export const PRIORITY_TYPES = new Set([ 'photoeye', 'photoeye_v', ]); -// Photoeyes are exempt from spacing — can be placed freely on top of anything +// Overlay types: exempt from spacing — can be placed freely on top of anything export const SPACING_EXEMPT = new Set([ 'photoeye', 'photoeye_v', + 'fio_sio_fioh', 'fio_sio_fioh_v', ]);