Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/lib/components/icons/BlockIcon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -46,14 +47,20 @@
axes={def.axes}
markers={def.markers}
decoration={def.decoration}
asymptotes={def.asymptotes}
badge={def.badge}
stems={def.stems}
/>
{:else if def.kind === 'pz'}
<IconPoleZero poles={def.poles} zeros={def.zeros} />
{:else if def.kind === 'scope'}
<IconScope
samples={def.samples()}
samples2={def.samples2?.()}
yRange={def.yRange}
gridX={def.gridX}
gridY={def.gridY}
bars={def.bars}
/>
{:else if def.kind === 'surface'}
<IconSurface fn={def.fn} rows={def.rows} cols={def.cols} />
Expand Down
93 changes: 78 additions & 15 deletions src/lib/components/icons/blocks/IconPlot.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import { AXIS_BOX, mapX, mapY, buildPath, type Sample } from './curves';
import { AXIS_BOX, PLOT_BOX, mapX, mapY, buildPath, type Sample } from './curves';

type AxesMode = 'none' | 'baseline' | 'cross';
/** 'baseline' = x only, 'yaxis' = y only, 'cross' = both. */
type AxesMode = 'none' | 'baseline' | 'yaxis' | 'cross';
type Decoration = 'arrow-up' | 'arrow-down';

interface Props {
Expand All @@ -12,6 +13,12 @@
axes?: AxesMode;
markers?: boolean;
decoration?: Decoration;
/** Vertical asymptotes in sample-x coordinates, drawn dashed. */
asymptotes?: number[];
/** Small superscript label in the top-right corner (e.g. base of a log). */
badge?: string;
/** Draw samples as stems from the zero line instead of a connected line. */
stems?: boolean;
}

let {
Expand All @@ -21,7 +28,10 @@
yRange = [0, 1],
axes = 'cross',
markers = false,
decoration
decoration,
asymptotes,
badge,
stems = false
}: Props = $props();

const path = $derived(buildPath(samples, xRange[0], xRange[1], yRange[0], yRange[1]));
Expand All @@ -31,43 +41,80 @@
: ''
);

// The zero line only sits inside the plot when 0 is actually part of the
// value range; otherwise it falls back to the box edge. For x this uses a
// strict test, so a time signal (x starting at 0) keeps its y-axis just
// left of the trace instead of drawing it straight through the first edge.
const xAxisY = $derived(
yRange[0] <= 0 && yRange[1] >= 0 ? mapY(0, yRange[0], yRange[1]) : AXIS_BOX.y1
);
const yAxisX = $derived(
xRange[0] <= 0 && xRange[1] >= 0 ? mapX(0, xRange[0], xRange[1]) : AXIS_BOX.x0
xRange[0] < 0 && xRange[1] > 0 ? mapX(0, xRange[0], xRange[1]) : AXIS_BOX.x0
);

const finiteSamples = $derived(samples.filter(([, v]) => Number.isFinite(v)));
const asymptoteX = $derived(
(asymptotes ?? []).map((x) => mapX(x, xRange[0], xRange[1]))
);
</script>

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 64" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
{#if axes === 'baseline' || axes === 'cross'}
<line x1={AXIS_BOX.x0} y1={xAxisY} x2={AXIS_BOX.x1} y2={xAxisY} />
{/if}
{#if axes === 'cross'}
<line x1={yAxisX} y1={AXIS_BOX.y0} x2={yAxisX} y2={AXIS_BOX.y1} />
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<!-- Axes are drawn first so the trace stays on top where they cross. -->
{#if axes !== 'none'}
<g class="axis">
{#if axes === 'baseline' || axes === 'cross'}
<line x1={AXIS_BOX.x0} y1={xAxisY} x2={AXIS_BOX.x1} y2={xAxisY} />
{/if}
{#if axes === 'yaxis' || axes === 'cross'}
<line x1={yAxisX} y1={AXIS_BOX.y0} x2={yAxisX} y2={AXIS_BOX.y1} />
{/if}
</g>
{/if}
{#each asymptoteX as ax}
<line class="asymptote" x1={ax} y1={PLOT_BOX.y0} x2={ax} y2={PLOT_BOX.y1} />
{/each}
{#if pathDashed}
<path d={pathDashed} stroke-dasharray="3 2.5" />
<path d={pathDashed} class="ghost" stroke-dasharray="3.5 3" />
{/if}
{#if stems}
{#each finiteSamples as [x, v]}
{@const sx = mapX(x, xRange[0], xRange[1])}
<line x1={sx} y1={xAxisY} x2={sx} y2={mapY(v, yRange[0], yRange[1])} />
<circle cx={sx} cy={mapY(v, yRange[0], yRange[1])} r="2.4" fill="currentColor" stroke="none" />
{/each}
{:else}
<path d={path} />
{/if}
<path d={path} />
{#if markers}
{#each finiteSamples as [x, v]}
<circle
cx={mapX(x, xRange[0], xRange[1])}
cy={mapY(v, yRange[0], yRange[1])}
r="3"
r="2.8"
fill="currentColor"
stroke="none"
/>
{/each}
{/if}
{#if decoration === 'arrow-up'}
<path d="M 88 40 L 88 24 M 84 28 L 88 24 L 92 28" />
<path d="M 86 44 L 86 22 M 82 26 L 86 22 L 90 26" />
{:else if decoration === 'arrow-down'}
<path d="M 88 24 L 88 40 M 84 36 L 88 40 L 92 36" />
<path d="M 86 22 L 86 44 M 82 40 L 86 44 L 90 40" />
{/if}
{#if badge}
<!-- Top-left: the corner a rising characteristic leaves free. -->
<text
x={AXIS_BOX.x0 + 4}
y={AXIS_BOX.y0}
text-anchor="start"
dominant-baseline="hanging"
fill="currentColor"
stroke="none"
font-family="ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, monospace"
font-size="11"
font-weight="600">{badge}</text
>
{/if}
</svg>

Expand All @@ -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;
}
</style>
61 changes: 61 additions & 0 deletions src/lib/components/icons/blocks/IconPoleZero.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script lang="ts">
/**
* Pole-zero map in the s-plane — poles as ×, zeros as ○.
* Used for blocks parametrised by zeros/poles/gain, so they read
* differently from the Bode-magnitude icons of coefficient-based blocks.
*/
import { AXIS_BOX, PLOT_BOX } from './curves';

interface Props {
/** Poles as [re, im] in a normalised [-1, 1] plane. */
poles?: Array<[number, number]>;
/** Zeros as [re, im]. */
zeros?: Array<[number, number]>;
}

let {
poles = [
[-0.55, 0.6],
[-0.55, -0.6]
],
zeros = [[0.45, 0]]
}: Props = $props();

const cx = (PLOT_BOX.x0 + PLOT_BOX.x1) / 2;
const cy = (PLOT_BOX.y0 + PLOT_BOX.y1) / 2;
const sx = PLOT_BOX.width / 2;
const sy = PLOT_BOX.height / 2;

const px = (re: number) => cx + re * sx;
const py = (im: number) => cy - im * sy;

const R = 3.4;
</script>

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 64" fill="none" stroke="currentColor"
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<g class="axis">
<line x1={AXIS_BOX.x0} y1={cy} x2={AXIS_BOX.x1} y2={cy} />
<line x1={cx} y1={AXIS_BOX.y0} x2={cx} y2={AXIS_BOX.y1} />
</g>
{#each poles as [re, im]}
{@const x = px(re)}
{@const y = py(im)}
<path d="M {x - R} {y - R} L {x + R} {y + R} M {x + R} {y - R} L {x - R} {y + R}" />
{/each}
{#each zeros as [re, im]}
<circle cx={px(re)} cy={py(im)} r={R} />
{/each}
</svg>

<style>
svg {
width: 100%;
height: 100%;
display: block;
}

.axis {
stroke-width: 1.6;
}
</style>
86 changes: 61 additions & 25 deletions src/lib/components/icons/blocks/IconScope.svelte
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
<script lang="ts">
import { mapY, buildPath, type Sample } from './curves';
/**
* Scope-style icon — a framed screen with a graticule and one or two traces.
* With `bars` the samples are drawn as spectrum bars instead of a line.
*/
import { type Sample } from './curves';

interface Props {
samples: Sample[];
samples2?: Sample[];
yRange?: [number, number];
/** Number of vertical grid divisions */
/** Number of vertical grid divisions (0 disables). */
gridX?: number;
/** Number of horizontal grid divisions */
/** Number of horizontal grid divisions (0 disables). */
gridY?: number;
/** Render samples as bars rising from the baseline (spectrum). */
bars?: boolean;
}

let { samples, samples2, yRange = [-1.1, 1.1], gridX = 4, gridY = 3 }: Props = $props();
let {
samples,
samples2,
yRange = [-1.1, 1.1],
gridX = 4,
gridY = 3,
bars = false
}: Props = $props();

// Screen frame box (slightly larger than default plot box for scope feel)
const FRAME = { x0: 5, x1: 91, y0: 6, y1: 58 } as const;
/* Screen frame, centred in the 96×64 viewBox. */
const FRAME = { x0: 6, x1: 90, y0: 7, y1: 57 } as const;
const W = FRAME.x1 - FRAME.x0;
const H = FRAME.y1 - FRAME.y0;

// Local plot mapping inside the frame with small inset
const INSET = 7;
const plotX0 = FRAME.x0 + INSET;
const plotX1 = FRAME.x1 - INSET;
const plotY0 = FRAME.y0 + INSET;
const plotY1 = FRAME.y1 - INSET;
/* Signal area inset from the frame so the trace never touches the bezel. */
const INSET_X = 7;
const INSET_Y = 6;
const plotX0 = FRAME.x0 + INSET_X;
const plotX1 = FRAME.x1 - INSET_X;
const plotY0 = FRAME.y0 + INSET_Y;
const plotY1 = FRAME.y1 - INSET_Y;

function localMapX(t: number): number {
return plotX0 + t * (plotX1 - plotX0);
Expand All @@ -38,34 +52,41 @@
.map(([t, v], i) => `${i === 0 ? 'M' : 'L'} ${localMapX(t).toFixed(2)} ${localMapY(v).toFixed(2)}`)
.join(' ');
}
const path = $derived(pathFor(samples));
const path = $derived(bars ? '' : pathFor(samples));
const path2 = $derived(samples2 ? pathFor(samples2) : '');

const gridXLines = $derived(
Array.from({ length: gridX - 1 }, (_, i) => FRAME.x0 + ((i + 1) * W) / gridX)
gridX > 1 ? Array.from({ length: gridX - 1 }, (_, i) => FRAME.x0 + ((i + 1) * W) / gridX) : []
);
const gridYLines = $derived(
Array.from({ length: gridY - 1 }, (_, i) => FRAME.y0 + ((i + 1) * H) / gridY)
gridY > 1 ? Array.from({ length: gridY - 1 }, (_, i) => FRAME.y0 + ((i + 1) * H) / gridY) : []
);

const baseline = $derived(localMapY(yRange[0]));
</script>

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 64" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round">
<!-- Outer screen frame -->
<rect x={FRAME.x0} y={FRAME.y0} width={W} height={H} rx="3" stroke-width="1.5" />
<!-- Grid -->
<g stroke-width="0.6" opacity="0.4">
<rect x={FRAME.x0} y={FRAME.y0} width={W} height={H} rx="4" stroke-width="1.6" />
<!-- Graticule: present but clearly behind the trace. -->
<g class="grid">
{#each gridXLines as gx}
<line x1={gx} y1={FRAME.y0 + 1} x2={gx} y2={FRAME.y1 - 1} />
<line x1={gx} y1={FRAME.y0 + 2} x2={gx} y2={FRAME.y1 - 2} />
{/each}
{#each gridYLines as gy}
<line x1={FRAME.x0 + 1} y1={gy} x2={FRAME.x1 - 1} y2={gy} />
<line x1={FRAME.x0 + 2} y1={gy} x2={FRAME.x1 - 2} y2={gy} />
{/each}
</g>
<!-- Signal traces -->
<path d={path} stroke-width="1.5" />
{#if path2}
<path d={path2} stroke-width="1.5" stroke-dasharray="4 4" stroke-dashoffset="2" />
{#if bars}
{#each samples as [t, v]}
<line class="bar" x1={localMapX(t)} y1={baseline} x2={localMapX(t)} y2={localMapY(v)} />
{/each}
<line class="baseline" x1={plotX0} y1={baseline} x2={plotX1} y2={baseline} />
{:else}
<path d={path} stroke-width="1.6" />
{#if path2}
<path d={path2} stroke-width="1.6" stroke-dasharray="4 4" stroke-dashoffset="2" />
{/if}
{/if}
</svg>

Expand All @@ -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;
}
</style>
Loading