diff --git a/src/lib/components/icons/BlockIcon.svelte b/src/lib/components/icons/BlockIcon.svelte index 29e4aebc..e862718f 100644 --- a/src/lib/components/icons/BlockIcon.svelte +++ b/src/lib/components/icons/BlockIcon.svelte @@ -24,6 +24,7 @@ import IconGlyph from './blocks/IconGlyph.svelte'; import IconScope from './blocks/IconScope.svelte'; import IconSurface from './blocks/IconSurface.svelte'; + import IconPoleZero from './blocks/IconPoleZero.svelte'; interface Props { blockClass: string | undefined; @@ -46,7 +47,12 @@ axes={def.axes} markers={def.markers} decoration={def.decoration} + asymptotes={def.asymptotes} + badge={def.badge} + stems={def.stems} /> + {:else if def.kind === 'pz'} + {:else if def.kind === 'scope'} {:else if def.kind === 'surface'} diff --git a/src/lib/components/icons/blocks/IconPlot.svelte b/src/lib/components/icons/blocks/IconPlot.svelte index 75cc69f8..76f421b8 100644 --- a/src/lib/components/icons/blocks/IconPlot.svelte +++ b/src/lib/components/icons/blocks/IconPlot.svelte @@ -1,7 +1,8 @@ - {#if axes === 'baseline' || axes === 'cross'} - - {/if} - {#if axes === 'cross'} - + stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"> + + {#if axes !== 'none'} + + {#if axes === 'baseline' || axes === 'cross'} + + {/if} + {#if axes === 'yaxis' || axes === 'cross'} + + {/if} + {/if} + {#each asymptoteX as ax} + + {/each} {#if pathDashed} - + + {/if} + {#if stems} + {#each finiteSamples as [x, v]} + {@const sx = mapX(x, xRange[0], xRange[1])} + + + {/each} + {:else} + {/if} - {#if markers} {#each finiteSamples as [x, v]} {/each} {/if} {#if decoration === 'arrow-up'} - + {:else if decoration === 'arrow-down'} - + + {/if} + {#if badge} + + {badge} {/if} @@ -77,4 +124,20 @@ height: 100%; display: block; } + + /* Axes carry the same weight as the trace — at canvas scale anything + * lighter disappears. */ + .axis { + stroke-width: 1.6; + } + + .asymptote { + stroke-width: 1.2; + opacity: 0.55; + stroke-dasharray: 3 3; + } + + .ghost { + opacity: 0.5; + } diff --git a/src/lib/components/icons/blocks/IconPoleZero.svelte b/src/lib/components/icons/blocks/IconPoleZero.svelte new file mode 100644 index 00000000..b4a670ce --- /dev/null +++ b/src/lib/components/icons/blocks/IconPoleZero.svelte @@ -0,0 +1,61 @@ + + + + + + + + {#each poles as [re, im]} + {@const x = px(re)} + {@const y = py(im)} + + {/each} + {#each zeros as [re, im]} + + {/each} + + + diff --git a/src/lib/components/icons/blocks/IconScope.svelte b/src/lib/components/icons/blocks/IconScope.svelte index 16f353fe..77b55a7f 100644 --- a/src/lib/components/icons/blocks/IconScope.svelte +++ b/src/lib/components/icons/blocks/IconScope.svelte @@ -1,29 +1,43 @@ - - - - + + + {#each gridXLines as gx} - + {/each} {#each gridYLines as gy} - + {/each} - - - {#if path2} - + {#if bars} + {#each samples as [t, v]} + + {/each} + + {:else} + + {#if path2} + + {/if} {/if} @@ -75,4 +96,19 @@ height: 100%; display: block; } + + .grid { + stroke-width: 0.8; + opacity: 0.35; + } + + .bar { + stroke-width: 3.2; + stroke-linecap: butt; + } + + .baseline { + stroke-width: 1.2; + opacity: 0.5; + } diff --git a/src/lib/components/icons/blocks/curves.ts b/src/lib/components/icons/blocks/curves.ts index fc7b37b7..8fa23ff7 100644 --- a/src/lib/components/icons/blocks/curves.ts +++ b/src/lib/components/icons/blocks/curves.ts @@ -7,23 +7,16 @@ export type Sample = [number, number]; -/** Box used to draw axes — the visible "frame" of the plot. */ -export const AXIS_BOX = { - x0: 12, - x1: 88, - y0: 8, - y1: 56 -} as const; - -/** Inset between axes box and the actual signal area, gives axes headroom. */ -const SIGNAL_INSET = 8; - -/** Box used to map sample values into pixels — strictly inside AXIS_BOX. */ +/** + * Geometry. The signal area (PLOT_BOX) is centred in the 96×64 viewBox; the + * axes overshoot it by AXIS_OVERSHOOT on every side, so an icon stays visually + * balanced no matter which axes it draws. + */ export const PLOT_BOX = { - x0: AXIS_BOX.x0 + SIGNAL_INSET / 2, - x1: AXIS_BOX.x1 - SIGNAL_INSET, - y0: AXIS_BOX.y0 + SIGNAL_INSET, - y1: AXIS_BOX.y1 - SIGNAL_INSET / 2, + x0: 14, + x1: 82, + y0: 14, + y1: 50, get width() { return this.x1 - this.x0; }, @@ -32,6 +25,17 @@ export const PLOT_BOX = { } } as const; +/** How far the axes extend past the signal area. */ +const AXIS_OVERSHOOT = 4; + +/** Box used to draw axes — the visible "frame" of the plot. */ +export const AXIS_BOX = { + x0: PLOT_BOX.x0 - AXIS_OVERSHOOT, + x1: PLOT_BOX.x1 + AXIS_OVERSHOOT, + y0: PLOT_BOX.y0 - AXIS_OVERSHOOT, + y1: PLOT_BOX.y1 + AXIS_OVERSHOOT +} as const; + export function mapX(x: number, xMin = 0, xMax = 1): number { const t = (x - xMin) / (xMax - xMin); return PLOT_BOX.x0 + t * PLOT_BOX.width; @@ -76,32 +80,29 @@ export function sineSamples(cycles = 1.5, n = 64): Sample[] { return out; } -export function squareSamples(cycles = 1.5): Sample[] { +/** Square wave over whole periods, so the trace closes cleanly at both edges. */ +export function squareSamples(cycles = 2): Sample[] { const out: Sample[] = []; const period = 1 / cycles; - let t = 0; - out.push([0, 1]); - while (t < 1) { - const tHigh = Math.min(1, t + period / 2); - out.push([tHigh, 1]); - out.push([tHigh, -1]); - const tLow = Math.min(1, t + period); - out.push([tLow, -1]); - if (tLow < 1) { - out.push([tLow, 1]); - } - t += period; + for (let c = 0; c < cycles; c++) { + const t0 = c * period; + out.push([t0, 1]); + out.push([t0 + period / 2, 1]); + out.push([t0 + period / 2, -1]); + out.push([t0 + period, -1]); + if (c < cycles - 1) out.push([t0 + period, 1]); } return out; } -export function triangleSamples(cycles = 1.5, n = 80): Sample[] { +/** Triangle wave over whole periods, starting and ending at the zero crossing. */ +export function triangleSamples(cycles = 2, n = 81): Sample[] { const out: Sample[] = []; for (let i = 0; i < n; i++) { const t = i / (n - 1); - const phase = (t * cycles * 2) % 2; - const v = phase < 1 ? phase : 2 - phase; - out.push([t, v * 2 - 1]); + const phase = (t * cycles * 4) % 4; + const v = phase < 1 ? phase : phase < 3 ? 2 - phase : phase - 4; + out.push([t, v]); } return out; } @@ -419,6 +420,17 @@ export function cosFunctionSamples(n = 80): Sample[] { return out; } +/** atan2 characteristic — the arctan S-curve saturating towards ±π/2, + * normalised so ±π/2 maps to ±1. */ +export function atan2Samples(gain = 4, n = 80): Sample[] { + const out: Sample[] = []; + for (let i = 0; i < n; i++) { + const x = -1 + (2 * i) / (n - 1); + out.push([x, Math.atan(x * gain) / (Math.PI / 2)]); + } + return out; +} + export function powSamples(exp = 2, n = 60): Sample[] { const out: Sample[] = []; for (let i = 0; i < n; i++) { @@ -471,18 +483,16 @@ export function pidStepSamples(t0 = 0.12, n = 100): Sample[] { return pt2StepSamples(0.4, 18, t0, n); } -/** Modulo / sawtooth: y = x mod period over x ∈ [0, 1] */ -export function modSamples(period = 0.3): Sample[] { +/** Modulo / sawtooth: y = x mod period, over a whole number of periods so no + * tooth is cut off at the right edge. */ +export function modSamples(teeth = 3): Sample[] { const out: Sample[] = []; - let t = 0; - while (t < 1) { - out.push([t, 0]); - const tEnd = Math.min(1, t + period); - out.push([tEnd, (tEnd - t) / period]); - if (tEnd < 1) { - out.push([tEnd, 0]); - } - t = tEnd; + const period = 1 / teeth; + for (let i = 0; i < teeth; i++) { + const t0 = i * period; + out.push([t0, 0]); + out.push([t0 + period, 1]); + if (i < teeth - 1) out.push([t0 + period, 0]); } return out; } @@ -658,27 +668,91 @@ export function firstOrderHoldSamples(n = 6): Sample[] { return out; } -export function backlashSamples(): Sample[] { +/** Backlash — closed hysteresis loop: the rising branch, then the falling one + * traced back, so the dead zone between them is visible as an open shape. */ +export function backlashSamples(width = 0.42, gain = 0.8): Sample[] { return [ - [-1, -0.7], - [-0.3, -0.7], - [0.3, 0.7], - [1, 0.7] + // rising branch + [-1, -gain], + [-1 + 2 * width, -gain], + [1, gain], + // falling branch, traced back to the start + [1 - 2 * width, gain], + [-1, -gain] ]; } +/** Discrete random draws — one value per sample instant, rendered as stems. */ +export function randomStemSamples(n = 11, seed = 7): Sample[] { + let s = seed; + const rand = () => { + s = (s * 9301 + 49297) % 233280; + return s / 233280; + }; + const out: Sample[] = []; + for (let i = 0; i < n; i++) { + out.push([(i + 0.5) / n, rand() * 1.8 - 0.9]); + } + return out; +} + +/** Reconstructed analog ramp — the smooth counterpart to a quantizer staircase. */ +export function rampBipolarSamples(): Sample[] { + return [ + [-1, -1], + [1, 1] + ]; +} + +/* --- Converters (ADC / DAC) -------------------------------------------- + * Both icons show the same analog signal as a dashed ghost. The ADC adds the + * sample instants taken from it, the DAC the held signal reconstructed from + * them — so the pair reads as "sampling" versus "holding". + */ + +const CONVERTER_AMP = 0.85; +const CONVERTER_CYCLES = 1.15; + +function converterSignal(t: number): number { + return CONVERTER_AMP * Math.sin(2 * Math.PI * CONVERTER_CYCLES * t); +} + +/** The continuous signal both converter icons reference. */ +export function converterAnalogSamples(n = 90): Sample[] { + return Array.from({ length: n }, (_, i) => { + const t = i / (n - 1); + return [t, converterSignal(t)] as Sample; + }); +} + +/** Sample instants taken from that signal — drawn as stems by the ADC icon. */ +export function converterSampleSamples(n = 7): Sample[] { + return Array.from({ length: n }, (_, i) => { + const t = (i + 0.5) / n; + return [t, converterSignal(t)] as Sample; + }); +} + +/** Zero-order-hold reconstruction of those samples — the DAC output. */ +export function converterHeldSamples(n = 7): Sample[] { + const out: Sample[] = []; + for (let i = 0; i < n; i++) { + const v = converterSignal((i + 0.5) / n); + out.push([i / n, v]); + out.push([(i + 1) / n, v]); + } + return out; +} + /* --- Scope-style display signals --------------------------------------- */ -/** Superposition of three sin/cos components — visually rich oscilloscope trace */ -export function superposedSignal(n = 220): Sample[] { +/** Ringing step response — the Scope trace. A decaying oscillation reads as a + * signal at any size and spans the screen without the noise of a superposition. */ +export function scopeTrace(decay = 1.45, cycles = 3, n = 200): Sample[] { const out: Sample[] = []; for (let i = 0; i < n; i++) { const t = i / (n - 1); - const v = - 0.55 * Math.sin(2 * Math.PI * 1.2 * t) + - 0.4 * Math.sin(2 * Math.PI * 5.2 * t + 0.4) + - 0.08 * Math.cos(2 * Math.PI * 11 * t); - out.push([t, v]); + out.push([t, Math.exp(-decay * t) * Math.sin(2 * Math.PI * cycles * t)]); } return out; } @@ -708,24 +782,24 @@ export function dampedOscillation(zeta = 0.06, cycles = 2.5, t0 = 0.05, n = 140) return out; } +/** Spectrum line heights — one entry per bin, rendered as bars by IconScope. + * Shaped like a real spectrum: a dominant fundamental with decaying harmonics + * sitting on a low noise floor. */ export function spectrumBars(): Sample[] { - const peaks = [ - [0.08, 0.15], - [0.18, 0.55], - [0.28, 0.85], - [0.38, 0.65], - [0.5, 0.4], - [0.62, 0.7], - [0.74, 0.3], - [0.86, 0.5], - [0.94, 0.18] - ] as Array<[number, number]>; - const out: Sample[] = [[0, 0]]; - for (const [t, h] of peaks) { - out.push([t, 0]); - out.push([t, h]); - out.push([t, 0]); - } - out.push([1, 0]); - return out; + return [ + [0.1, 0.28], + [0.22, 1.0], + [0.34, 0.2], + [0.46, 0.62], + [0.58, 0.16], + [0.7, 0.4], + [0.82, 0.14], + [0.94, 0.24] + ]; +} + +/** PID step response hitting an actuator limit — the flat top is the + * saturation the anti-windup logic deals with. */ +export function antiWindupStepSamples(limit = 0.82, t0 = 0.12, n = 100): Sample[] { + return pt2StepSamples(0.4, 18, t0, n).map(([t, v]) => [t, Math.min(v, limit)] as Sample); } diff --git a/src/lib/components/icons/blocks/registry.ts b/src/lib/components/icons/blocks/registry.ts index 084e8878..524909f2 100644 --- a/src/lib/components/icons/blocks/registry.ts +++ b/src/lib/components/icons/blocks/registry.ts @@ -4,19 +4,25 @@ * Renderer types: * - 'plot': programmatic curve with optional axes (Sources, responses, nonlinearities) * - 'scope': framed plot with grid (Scope/Spectrum) + * - 'pz': pole-zero map in the s-plane (zero/pole/gain blocks) * - 'math': KaTeX-rendered LaTeX (transfer functions, ODEs, operators) * - 'glyph': monospace text label (PID, ADC, DAC, …) * - 'svg': raw SVG file from ./svg/.svg (geometric symbols) + * + * Every icon shares the geometry defined in ./curves (96×64 viewBox, centred + * PLOT_BOX, axes overshooting it by a fixed margin), so icons stay comparable + * in weight and size when placed side by side on the canvas. */ import type { Sample } from './curves'; import * as C from './curves'; -type AxesMode = 'none' | 'baseline' | 'cross'; +type AxesMode = 'none' | 'baseline' | 'yaxis' | 'cross'; export type IconDef = - | { kind: 'plot'; samples: () => Sample[]; samplesDashed?: () => Sample[]; xRange?: [number, number]; yRange?: [number, number]; axes?: AxesMode; markers?: boolean; decoration?: 'arrow-up' | 'arrow-down' } - | { kind: 'scope'; samples: () => Sample[]; samples2?: () => Sample[]; yRange?: [number, number]; gridX?: number; gridY?: number } + | { kind: 'plot'; samples: () => Sample[]; samplesDashed?: () => Sample[]; xRange?: [number, number]; yRange?: [number, number]; axes?: AxesMode; markers?: boolean; decoration?: 'arrow-up' | 'arrow-down'; asymptotes?: number[]; badge?: string; stems?: boolean } + | { kind: 'pz'; poles?: Array<[number, number]>; zeros?: Array<[number, number]> } + | { kind: 'scope'; samples: () => Sample[]; samples2?: () => Sample[]; yRange?: [number, number]; gridX?: number; gridY?: number; bars?: boolean } | { kind: 'surface'; fn?: (u: number, v: number) => number; rows?: number; cols?: number } | { kind: 'math'; latex: string } | { kind: 'glyph'; text: string; size?: number } @@ -42,7 +48,8 @@ export const iconRegistry: Record = { ChirpPhaseNoiseSource: { kind: 'plot', samples: () => C.chirpSamples(), yRange: Y_BIPOLAR }, WhiteNoise: { kind: 'plot', samples: () => C.whiteNoiseSamples(), yRange: Y_BIPOLAR }, PinkNoise: { kind: 'plot', samples: () => C.pinkNoiseSamples(), yRange: Y_BIPOLAR }, - RandomNumberGenerator: { kind: 'plot', samples: () => C.whiteNoiseSamples(16, 13), yRange: Y_BIPOLAR }, + // Discrete draws, not a continuous trace — keeps it apart from the noise sources. + RandomNumberGenerator: { kind: 'plot', samples: () => C.randomStemSamples(), yRange: Y_BIPOLAR, stems: true }, ClockSource: { kind: 'plot', samples: () => C.clockSamples() }, Source: { kind: 'math', latex: 'f(t)' }, @@ -59,7 +66,8 @@ export const iconRegistry: Record = { yRange: Y_BIPOLAR }, PID: { kind: 'plot', samples: () => C.pidStepSamples(), yRange: PT2_RANGE }, - AntiWindupPID: { kind: 'plot', samples: () => C.pt1StepSamples(0.12, 0.12) }, + // Saturating response — visibly different from the plain PID overshoot. + AntiWindupPID: { kind: 'plot', samples: () => C.antiWindupStepSamples(), yRange: PT2_RANGE }, /* --- Bode magnitude (Filters / generic transfer functions) --- */ ButterworthLowpassFilter: { kind: 'plot', samples: () => C.butterLowpassBode() }, @@ -68,18 +76,27 @@ export const iconRegistry: Record = { ButterworthBandstopFilter: { kind: 'plot', samples: () => C.butterBandstopBode() }, FIR: { kind: 'plot', samples: () => C.firBode() }, TransferFunctionNumDen: { kind: 'plot', samples: () => C.genericBode(0.35), yRange: GENERIC_BODE_Y }, - TransferFunctionZPG: { kind: 'plot', samples: () => C.genericBode(0.35), yRange: GENERIC_BODE_Y }, + // Parametrised by zeros/poles/gain — shown as the pole-zero map rather than + // the same Bode magnitude as the coefficient form. + TransferFunctionZPG: { kind: 'pz' }, /* --- Static nonlinearities (input-output, x in real domain) --- */ Tanh: { kind: 'plot', samples: () => C.tanhSamples(), xRange: X_BIPOLAR, yRange: Y_BIPOLAR }, Exp: { kind: 'plot', samples: () => C.expSamples() }, Log: { kind: 'plot', samples: () => C.logSamples() }, - Log10: { kind: 'plot', samples: () => C.logSamples() }, + // Same curve shape as Log — the base is what distinguishes the two. + Log10: { kind: 'plot', samples: () => C.logSamples(), badge: '10' }, Sqrt: { kind: 'plot', samples: () => C.sqrtSamples() }, - Abs: { kind: 'plot', samples: () => C.absSamples(), xRange: X_BIPOLAR }, + // The V sits on the zero line, so drawing the y-axis through its vertex too + // would turn the icon into a four-armed fan — baseline only. + Abs: { kind: 'plot', samples: () => C.absSamples(), xRange: X_BIPOLAR, axes: 'baseline' }, Clip: { kind: 'plot', samples: () => C.clipSamples(), xRange: X_BIPOLAR, yRange: Y_TIGHT }, - Deadband: { kind: 'plot', samples: () => C.deadbandSamples(), xRange: X_BIPOLAR, yRange: Y_TIGHT }, - Relay: { kind: 'plot', samples: () => C.relaySamples(), xRange: X_BIPOLAR, yRange: Y_BIPOLAR }, + // The dead zone lies exactly on the x-axis — drawing it would hide the very + // feature the icon is about. + Deadband: { kind: 'plot', samples: () => C.deadbandSamples(), xRange: X_BIPOLAR, yRange: Y_TIGHT, axes: 'yaxis' }, + // Axis-parallel hysteresis loop — with axes drawn through it the icon reads + // as a hash mark, so the loop stands on its own. + Relay: { kind: 'plot', samples: () => C.relaySamples(0.45), xRange: X_BIPOLAR, yRange: Y_BIPOLAR, axes: 'none' }, RateLimiter: { kind: 'plot', samples: () => C.rateLimiterSamples() }, SampleHold: { kind: 'plot', samples: () => C.sampleHoldSamples() }, Backlash: { kind: 'plot', samples: () => C.backlashSamples(), xRange: X_BIPOLAR, yRange: Y_BIPOLAR }, @@ -87,35 +104,61 @@ export const iconRegistry: Record = { /* --- Trig / power --- */ Sin: { kind: 'plot', samples: () => C.sinFunctionSamples(), xRange: TRIG_X_RANGE, yRange: Y_BIPOLAR }, Cos: { kind: 'plot', samples: () => C.cosFunctionSamples(), xRange: TRIG_X_RANGE, yRange: Y_BIPOLAR }, - Tan: { kind: 'plot', samples: () => C.tanFunctionSamples(), xRange: TRIG_X_RANGE, yRange: [-1.6, 1.6] }, + Tan: { + kind: 'plot', + samples: () => C.tanFunctionSamples(), + xRange: TRIG_X_RANGE, + yRange: [-1.6, 1.6], + asymptotes: [-Math.PI / 2, Math.PI / 2] + }, Pow: { kind: 'plot', samples: () => C.powSamples(2), xRange: X_BIPOLAR }, Mod: { kind: 'plot', samples: () => C.modSamples() }, + Atan2: { kind: 'plot', samples: () => C.atan2Samples(), xRange: X_BIPOLAR, yRange: [-1.25, 1.25] }, - /* --- Quantisation / counters --- */ - ADC: { kind: 'plot', samples: () => C.quantizerSamples(), xRange: X_BIPOLAR, yRange: Y_BIPOLAR }, - DAC: { kind: 'plot', samples: () => C.quantizerSamples(), xRange: X_BIPOLAR, yRange: Y_BIPOLAR }, + /* --- Quantisation / counters --- + * ADC and DAC share the same dashed analog signal: the ADC adds the sample + * instants taken from it, the DAC the held signal rebuilt from them. */ + ADC: { + kind: 'plot', + samples: () => C.converterSampleSamples(), + samplesDashed: () => C.converterAnalogSamples(), + yRange: Y_BIPOLAR, + stems: true + }, + DAC: { + kind: 'plot', + samples: () => C.converterHeldSamples(), + samplesDashed: () => C.converterAnalogSamples(), + yRange: Y_BIPOLAR + }, Counter: { kind: 'plot', samples: () => C.counterUpSamples() }, CounterUp: { kind: 'plot', samples: () => C.counterUpSamples(), decoration: 'arrow-up' }, - CounterDown: { kind: 'plot', samples: () => C.counterUpSamples(), decoration: 'arrow-down' }, + CounterDown: { kind: 'plot', samples: () => C.counterDownSamples(), decoration: 'arrow-down' }, /* --- Lookup tables --- */ LUT1D: { kind: 'plot', samples: () => C.lut1dSamples(), xRange: X_BIPOLAR, yRange: Y_BIPOLAR, markers: true }, LUT: { kind: 'surface', fn: (u, v) => -0.18 * (u + v) + 0.3 * u * v }, - /* --- Math (KaTeX) --- */ + /* --- Math (KaTeX) --- + * Kept to a single short line wherever possible: at canvas scale a block is + * 80×40 px, so a two-line formula degrades into unreadable texture. */ ODE: { kind: 'math', latex: '\\dot{x} = f(x, u, t)' }, - StateSpace: { kind: 'math', latex: '\\begin{aligned}\\dot{x} &= Ax{+}Bu\\\\ y &= Cx{+}Du\\end{aligned}' }, - DynamicalSystem: { kind: 'math', latex: '\\begin{aligned}\\dot{x} &= f(x, u, t)\\\\ y &= g(x, u, t)\\end{aligned}' }, + StateSpace: { kind: 'math', latex: '\\dot{x} = Ax{+}Bu' }, + // Keeps the output equation ODE does not have — abbreviated so two lines + // still scale up rather than turning to texture. + DynamicalSystem: { kind: 'math', latex: '\\begin{aligned}\\dot{x} &= f\\\\ y &= g\\end{aligned}' }, DynamicalFunction: { kind: 'math', latex: 'f(u, t)' }, Function: { kind: 'math', latex: 'f(u)' }, - Polynomial: { kind: 'math', latex: 'y = \\sum_{k=0}^{n} c_k\\,u^{n-k}' }, + Polynomial: { kind: 'math', latex: '\\sum c_k\\,u^{k}' }, /* --- Discrete-time blocks --- */ - FirstOrderHold: { kind: 'plot', samples: () => C.firstOrderHoldSamples() }, + // Markers make it read as interpolation between sample instants — the same + // instants SampleHold shows as a staircase. + FirstOrderHold: { kind: 'plot', samples: () => C.firstOrderHoldSamples(), markers: true }, DiscreteIntegrator: { kind: 'math', latex: '\\dfrac{T}{z-1}' }, DiscreteDerivative: { kind: 'math', latex: '\\dfrac{z-1}{T\\,z}' }, - DiscreteStateSpace: { kind: 'math', latex: '\\begin{aligned}x[k{+}1] &= Ax[k]{+}Bu[k]\\\\ y[k] &= Cx[k]{+}Du[k]\\end{aligned}' }, - DiscreteTransferFunction: { kind: 'math', latex: 'H(z) = \\dfrac{B(z)}{A(z)}' }, + DiscreteStateSpace: { kind: 'math', latex: 'x_{k+1} = Ax_k{+}Bu_k' }, + DiscreteTransferFunction: { kind: 'math', latex: '\\dfrac{B(z)}{A(z)}' }, TappedDelay: { kind: 'svg', name: 'TappedDelay' }, /* --- Geometric SVGs (kept as files) --- */ @@ -123,19 +166,33 @@ export const iconRegistry: Record = { Multiplier: { kind: 'svg', name: 'Multiplier' }, Amplifier: { kind: 'svg', name: 'Amplifier' }, Rescale: { kind: 'svg', name: 'Amplifier' }, + Divider: { kind: 'svg', name: 'Divider' }, LogicAnd: { kind: 'svg', name: 'LogicAnd' }, LogicOr: { kind: 'svg', name: 'LogicOr' }, LogicNot: { kind: 'svg', name: 'LogicNot' }, + Equal: { kind: 'svg', name: 'Equal' }, + GreaterThan: { kind: 'svg', name: 'GreaterThan' }, + LessThan: { kind: 'svg', name: 'LessThan' }, + Alias: { kind: 'svg', name: 'Alias' }, + Wrapper: { kind: 'svg', name: 'Wrapper' }, Switch: { kind: 'svg', name: 'Switch' }, Subsystem: { kind: 'svg', name: 'Subsystem' }, + Interface: { kind: 'svg', name: 'Interface' }, Scope: { kind: 'scope', - samples: () => C.superposedSignal(), - yRange: [-1.05, 1.05], - gridX: 0, - gridY: 0 + samples: () => C.scopeTrace(), + yRange: [-1.15, 1.15], + gridX: 4, + gridY: 2 }, - Spectrum: { kind: 'scope', samples: () => C.spectrumBars(), yRange: [0, 1], gridX: 0, gridY: 0 } + Spectrum: { + kind: 'scope', + samples: () => C.spectrumBars(), + yRange: [0, 1.12], + gridX: 0, + gridY: 2, + bars: true + } }; export function getIconDef(blockClass: string | undefined): IconDef | undefined { diff --git a/src/lib/components/icons/blocks/svg/Adder.svg b/src/lib/components/icons/blocks/svg/Adder.svg index 83baff2e..b86e2b9e 100644 --- a/src/lib/components/icons/blocks/svg/Adder.svg +++ b/src/lib/components/icons/blocks/svg/Adder.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/lib/components/icons/blocks/svg/Alias.svg b/src/lib/components/icons/blocks/svg/Alias.svg new file mode 100644 index 00000000..b6b7f723 --- /dev/null +++ b/src/lib/components/icons/blocks/svg/Alias.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/lib/components/icons/blocks/svg/Amplifier.svg b/src/lib/components/icons/blocks/svg/Amplifier.svg index 08a6e1a9..7913b764 100644 --- a/src/lib/components/icons/blocks/svg/Amplifier.svg +++ b/src/lib/components/icons/blocks/svg/Amplifier.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/lib/components/icons/blocks/svg/Divider.svg b/src/lib/components/icons/blocks/svg/Divider.svg new file mode 100644 index 00000000..62634aae --- /dev/null +++ b/src/lib/components/icons/blocks/svg/Divider.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/lib/components/icons/blocks/svg/Equal.svg b/src/lib/components/icons/blocks/svg/Equal.svg new file mode 100644 index 00000000..5604f7d6 --- /dev/null +++ b/src/lib/components/icons/blocks/svg/Equal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/components/icons/blocks/svg/GreaterThan.svg b/src/lib/components/icons/blocks/svg/GreaterThan.svg new file mode 100644 index 00000000..8766f357 --- /dev/null +++ b/src/lib/components/icons/blocks/svg/GreaterThan.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/components/icons/blocks/svg/Interface.svg b/src/lib/components/icons/blocks/svg/Interface.svg new file mode 100644 index 00000000..b8651302 --- /dev/null +++ b/src/lib/components/icons/blocks/svg/Interface.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/lib/components/icons/blocks/svg/LessThan.svg b/src/lib/components/icons/blocks/svg/LessThan.svg new file mode 100644 index 00000000..aa3da34c --- /dev/null +++ b/src/lib/components/icons/blocks/svg/LessThan.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/components/icons/blocks/svg/LogicAnd.svg b/src/lib/components/icons/blocks/svg/LogicAnd.svg index c40f7632..c42f4efd 100644 --- a/src/lib/components/icons/blocks/svg/LogicAnd.svg +++ b/src/lib/components/icons/blocks/svg/LogicAnd.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/lib/components/icons/blocks/svg/LogicNot.svg b/src/lib/components/icons/blocks/svg/LogicNot.svg index 9a2938e7..197c221c 100644 --- a/src/lib/components/icons/blocks/svg/LogicNot.svg +++ b/src/lib/components/icons/blocks/svg/LogicNot.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/src/lib/components/icons/blocks/svg/LogicOr.svg b/src/lib/components/icons/blocks/svg/LogicOr.svg index 4ee6d6e9..6d7de672 100644 --- a/src/lib/components/icons/blocks/svg/LogicOr.svg +++ b/src/lib/components/icons/blocks/svg/LogicOr.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/lib/components/icons/blocks/svg/Multiplier.svg b/src/lib/components/icons/blocks/svg/Multiplier.svg index 28f582f2..d35ad99a 100644 --- a/src/lib/components/icons/blocks/svg/Multiplier.svg +++ b/src/lib/components/icons/blocks/svg/Multiplier.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/lib/components/icons/blocks/svg/Subsystem.svg b/src/lib/components/icons/blocks/svg/Subsystem.svg index 1ad964a1..735ae20f 100644 --- a/src/lib/components/icons/blocks/svg/Subsystem.svg +++ b/src/lib/components/icons/blocks/svg/Subsystem.svg @@ -1,4 +1,4 @@ - + diff --git a/src/lib/components/icons/blocks/svg/Switch.svg b/src/lib/components/icons/blocks/svg/Switch.svg index 5d54a6b2..0d4dca4f 100644 --- a/src/lib/components/icons/blocks/svg/Switch.svg +++ b/src/lib/components/icons/blocks/svg/Switch.svg @@ -1,4 +1,4 @@ - + diff --git a/src/lib/components/icons/blocks/svg/TappedDelay.svg b/src/lib/components/icons/blocks/svg/TappedDelay.svg index ad4328a2..4c8e501c 100644 --- a/src/lib/components/icons/blocks/svg/TappedDelay.svg +++ b/src/lib/components/icons/blocks/svg/TappedDelay.svg @@ -1,4 +1,4 @@ - + diff --git a/src/lib/components/icons/blocks/svg/Wrapper.svg b/src/lib/components/icons/blocks/svg/Wrapper.svg new file mode 100644 index 00000000..197974be --- /dev/null +++ b/src/lib/components/icons/blocks/svg/Wrapper.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/routes/icons/+page.svelte b/src/routes/icons/+page.svelte new file mode 100644 index 00000000..1f56792f --- /dev/null +++ b/src/routes/icons/+page.svelte @@ -0,0 +1,290 @@ + + + + + Block Icon Gallery + + + +
+
+

Block Icon Gallery

+
+ {stats.withIcon}/{stats.total} mit Icon + {stats.missing} ohne + {#each stats.kinds as [kind, n]}{kind}: {n}{/each} +
+
+ + +
+
+ + {#each byCategory as [category, list]} +
+

{category} ({list.length})

+
+ {#each list as def} + {@const has = hasBlockIcon(def.blockClass)} + {@const kind = getIconDef(def.blockClass)?.kind} +
+
+ {#if has} + + {:else} + + {/if} +
+
+ +
+ {def.name} + {#if has} +
+ {:else} + {def.name} + {/if} +
+
+
+ {def.name} + {#if kind}{kind}{/if} +
+
+ {/each} +
+
+ {/each} +
+ +