Skip to content

Commit 014af12

Browse files
authored
Merge pull request #326 from pathsim/feat/block-icons
Rework block icons
2 parents 780a467 + fa2b341 commit 014af12

23 files changed

Lines changed: 776 additions & 158 deletions

src/lib/components/icons/BlockIcon.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import IconGlyph from './blocks/IconGlyph.svelte';
2525
import IconScope from './blocks/IconScope.svelte';
2626
import IconSurface from './blocks/IconSurface.svelte';
27+
import IconPoleZero from './blocks/IconPoleZero.svelte';
2728
2829
interface Props {
2930
blockClass: string | undefined;
@@ -46,14 +47,20 @@
4647
axes={def.axes}
4748
markers={def.markers}
4849
decoration={def.decoration}
50+
asymptotes={def.asymptotes}
51+
badge={def.badge}
52+
stems={def.stems}
4953
/>
54+
{:else if def.kind === 'pz'}
55+
<IconPoleZero poles={def.poles} zeros={def.zeros} />
5056
{:else if def.kind === 'scope'}
5157
<IconScope
5258
samples={def.samples()}
5359
samples2={def.samples2?.()}
5460
yRange={def.yRange}
5561
gridX={def.gridX}
5662
gridY={def.gridY}
63+
bars={def.bars}
5764
/>
5865
{:else if def.kind === 'surface'}
5966
<IconSurface fn={def.fn} rows={def.rows} cols={def.cols} />
Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script lang="ts">
2-
import { AXIS_BOX, mapX, mapY, buildPath, type Sample } from './curves';
2+
import { AXIS_BOX, PLOT_BOX, mapX, mapY, buildPath, type Sample } from './curves';
33
4-
type AxesMode = 'none' | 'baseline' | 'cross';
4+
/** 'baseline' = x only, 'yaxis' = y only, 'cross' = both. */
5+
type AxesMode = 'none' | 'baseline' | 'yaxis' | 'cross';
56
type Decoration = 'arrow-up' | 'arrow-down';
67
78
interface Props {
@@ -12,6 +13,12 @@
1213
axes?: AxesMode;
1314
markers?: boolean;
1415
decoration?: Decoration;
16+
/** Vertical asymptotes in sample-x coordinates, drawn dashed. */
17+
asymptotes?: number[];
18+
/** Small superscript label in the top-right corner (e.g. base of a log). */
19+
badge?: string;
20+
/** Draw samples as stems from the zero line instead of a connected line. */
21+
stems?: boolean;
1522
}
1623
1724
let {
@@ -21,7 +28,10 @@
2128
yRange = [0, 1],
2229
axes = 'cross',
2330
markers = false,
24-
decoration
31+
decoration,
32+
asymptotes,
33+
badge,
34+
stems = false
2535
}: Props = $props();
2636
2737
const path = $derived(buildPath(samples, xRange[0], xRange[1], yRange[0], yRange[1]));
@@ -31,43 +41,80 @@
3141
: ''
3242
);
3343
44+
// The zero line only sits inside the plot when 0 is actually part of the
45+
// value range; otherwise it falls back to the box edge. For x this uses a
46+
// strict test, so a time signal (x starting at 0) keeps its y-axis just
47+
// left of the trace instead of drawing it straight through the first edge.
3448
const xAxisY = $derived(
3549
yRange[0] <= 0 && yRange[1] >= 0 ? mapY(0, yRange[0], yRange[1]) : AXIS_BOX.y1
3650
);
3751
const yAxisX = $derived(
38-
xRange[0] <= 0 && xRange[1] >= 0 ? mapX(0, xRange[0], xRange[1]) : AXIS_BOX.x0
52+
xRange[0] < 0 && xRange[1] > 0 ? mapX(0, xRange[0], xRange[1]) : AXIS_BOX.x0
3953
);
4054
4155
const finiteSamples = $derived(samples.filter(([, v]) => Number.isFinite(v)));
56+
const asymptoteX = $derived(
57+
(asymptotes ?? []).map((x) => mapX(x, xRange[0], xRange[1]))
58+
);
4259
</script>
4360

4461
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 64" fill="none" stroke="currentColor"
45-
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
46-
{#if axes === 'baseline' || axes === 'cross'}
47-
<line x1={AXIS_BOX.x0} y1={xAxisY} x2={AXIS_BOX.x1} y2={xAxisY} />
48-
{/if}
49-
{#if axes === 'cross'}
50-
<line x1={yAxisX} y1={AXIS_BOX.y0} x2={yAxisX} y2={AXIS_BOX.y1} />
62+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
63+
<!-- Axes are drawn first so the trace stays on top where they cross. -->
64+
{#if axes !== 'none'}
65+
<g class="axis">
66+
{#if axes === 'baseline' || axes === 'cross'}
67+
<line x1={AXIS_BOX.x0} y1={xAxisY} x2={AXIS_BOX.x1} y2={xAxisY} />
68+
{/if}
69+
{#if axes === 'yaxis' || axes === 'cross'}
70+
<line x1={yAxisX} y1={AXIS_BOX.y0} x2={yAxisX} y2={AXIS_BOX.y1} />
71+
{/if}
72+
</g>
5173
{/if}
74+
{#each asymptoteX as ax}
75+
<line class="asymptote" x1={ax} y1={PLOT_BOX.y0} x2={ax} y2={PLOT_BOX.y1} />
76+
{/each}
5277
{#if pathDashed}
53-
<path d={pathDashed} stroke-dasharray="3 2.5" />
78+
<path d={pathDashed} class="ghost" stroke-dasharray="3.5 3" />
79+
{/if}
80+
{#if stems}
81+
{#each finiteSamples as [x, v]}
82+
{@const sx = mapX(x, xRange[0], xRange[1])}
83+
<line x1={sx} y1={xAxisY} x2={sx} y2={mapY(v, yRange[0], yRange[1])} />
84+
<circle cx={sx} cy={mapY(v, yRange[0], yRange[1])} r="2.4" fill="currentColor" stroke="none" />
85+
{/each}
86+
{:else}
87+
<path d={path} />
5488
{/if}
55-
<path d={path} />
5689
{#if markers}
5790
{#each finiteSamples as [x, v]}
5891
<circle
5992
cx={mapX(x, xRange[0], xRange[1])}
6093
cy={mapY(v, yRange[0], yRange[1])}
61-
r="3"
94+
r="2.8"
6295
fill="currentColor"
6396
stroke="none"
6497
/>
6598
{/each}
6699
{/if}
67100
{#if decoration === 'arrow-up'}
68-
<path d="M 88 40 L 88 24 M 84 28 L 88 24 L 92 28" />
101+
<path d="M 86 44 L 86 22 M 82 26 L 86 22 L 90 26" />
69102
{:else if decoration === 'arrow-down'}
70-
<path d="M 88 24 L 88 40 M 84 36 L 88 40 L 92 36" />
103+
<path d="M 86 22 L 86 44 M 82 40 L 86 44 L 90 40" />
104+
{/if}
105+
{#if badge}
106+
<!-- Top-left: the corner a rising characteristic leaves free. -->
107+
<text
108+
x={AXIS_BOX.x0 + 4}
109+
y={AXIS_BOX.y0}
110+
text-anchor="start"
111+
dominant-baseline="hanging"
112+
fill="currentColor"
113+
stroke="none"
114+
font-family="ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, monospace"
115+
font-size="11"
116+
font-weight="600">{badge}</text
117+
>
71118
{/if}
72119
</svg>
73120

@@ -77,4 +124,20 @@
77124
height: 100%;
78125
display: block;
79126
}
127+
128+
/* Axes carry the same weight as the trace — at canvas scale anything
129+
* lighter disappears. */
130+
.axis {
131+
stroke-width: 1.6;
132+
}
133+
134+
.asymptote {
135+
stroke-width: 1.2;
136+
opacity: 0.55;
137+
stroke-dasharray: 3 3;
138+
}
139+
140+
.ghost {
141+
opacity: 0.5;
142+
}
80143
</style>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<script lang="ts">
2+
/**
3+
* Pole-zero map in the s-plane — poles as ×, zeros as ○.
4+
* Used for blocks parametrised by zeros/poles/gain, so they read
5+
* differently from the Bode-magnitude icons of coefficient-based blocks.
6+
*/
7+
import { AXIS_BOX, PLOT_BOX } from './curves';
8+
9+
interface Props {
10+
/** Poles as [re, im] in a normalised [-1, 1] plane. */
11+
poles?: Array<[number, number]>;
12+
/** Zeros as [re, im]. */
13+
zeros?: Array<[number, number]>;
14+
}
15+
16+
let {
17+
poles = [
18+
[-0.55, 0.6],
19+
[-0.55, -0.6]
20+
],
21+
zeros = [[0.45, 0]]
22+
}: Props = $props();
23+
24+
const cx = (PLOT_BOX.x0 + PLOT_BOX.x1) / 2;
25+
const cy = (PLOT_BOX.y0 + PLOT_BOX.y1) / 2;
26+
const sx = PLOT_BOX.width / 2;
27+
const sy = PLOT_BOX.height / 2;
28+
29+
const px = (re: number) => cx + re * sx;
30+
const py = (im: number) => cy - im * sy;
31+
32+
const R = 3.4;
33+
</script>
34+
35+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 64" fill="none" stroke="currentColor"
36+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
37+
<g class="axis">
38+
<line x1={AXIS_BOX.x0} y1={cy} x2={AXIS_BOX.x1} y2={cy} />
39+
<line x1={cx} y1={AXIS_BOX.y0} x2={cx} y2={AXIS_BOX.y1} />
40+
</g>
41+
{#each poles as [re, im]}
42+
{@const x = px(re)}
43+
{@const y = py(im)}
44+
<path d="M {x - R} {y - R} L {x + R} {y + R} M {x + R} {y - R} L {x - R} {y + R}" />
45+
{/each}
46+
{#each zeros as [re, im]}
47+
<circle cx={px(re)} cy={py(im)} r={R} />
48+
{/each}
49+
</svg>
50+
51+
<style>
52+
svg {
53+
width: 100%;
54+
height: 100%;
55+
display: block;
56+
}
57+
58+
.axis {
59+
stroke-width: 1.6;
60+
}
61+
</style>
Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,43 @@
11
<script lang="ts">
2-
import { mapY, buildPath, type Sample } from './curves';
2+
/**
3+
* Scope-style icon — a framed screen with a graticule and one or two traces.
4+
* With `bars` the samples are drawn as spectrum bars instead of a line.
5+
*/
6+
import { type Sample } from './curves';
37
48
interface Props {
59
samples: Sample[];
610
samples2?: Sample[];
711
yRange?: [number, number];
8-
/** Number of vertical grid divisions */
12+
/** Number of vertical grid divisions (0 disables). */
913
gridX?: number;
10-
/** Number of horizontal grid divisions */
14+
/** Number of horizontal grid divisions (0 disables). */
1115
gridY?: number;
16+
/** Render samples as bars rising from the baseline (spectrum). */
17+
bars?: boolean;
1218
}
1319
14-
let { samples, samples2, yRange = [-1.1, 1.1], gridX = 4, gridY = 3 }: Props = $props();
20+
let {
21+
samples,
22+
samples2,
23+
yRange = [-1.1, 1.1],
24+
gridX = 4,
25+
gridY = 3,
26+
bars = false
27+
}: Props = $props();
1528
16-
// Screen frame box (slightly larger than default plot box for scope feel)
17-
const FRAME = { x0: 5, x1: 91, y0: 6, y1: 58 } as const;
29+
/* Screen frame, centred in the 96×64 viewBox. */
30+
const FRAME = { x0: 6, x1: 90, y0: 7, y1: 57 } as const;
1831
const W = FRAME.x1 - FRAME.x0;
1932
const H = FRAME.y1 - FRAME.y0;
2033
21-
// Local plot mapping inside the frame with small inset
22-
const INSET = 7;
23-
const plotX0 = FRAME.x0 + INSET;
24-
const plotX1 = FRAME.x1 - INSET;
25-
const plotY0 = FRAME.y0 + INSET;
26-
const plotY1 = FRAME.y1 - INSET;
34+
/* Signal area inset from the frame so the trace never touches the bezel. */
35+
const INSET_X = 7;
36+
const INSET_Y = 6;
37+
const plotX0 = FRAME.x0 + INSET_X;
38+
const plotX1 = FRAME.x1 - INSET_X;
39+
const plotY0 = FRAME.y0 + INSET_Y;
40+
const plotY1 = FRAME.y1 - INSET_Y;
2741
2842
function localMapX(t: number): number {
2943
return plotX0 + t * (plotX1 - plotX0);
@@ -38,34 +52,41 @@
3852
.map(([t, v], i) => `${i === 0 ? 'M' : 'L'} ${localMapX(t).toFixed(2)} ${localMapY(v).toFixed(2)}`)
3953
.join(' ');
4054
}
41-
const path = $derived(pathFor(samples));
55+
const path = $derived(bars ? '' : pathFor(samples));
4256
const path2 = $derived(samples2 ? pathFor(samples2) : '');
4357
4458
const gridXLines = $derived(
45-
Array.from({ length: gridX - 1 }, (_, i) => FRAME.x0 + ((i + 1) * W) / gridX)
59+
gridX > 1 ? Array.from({ length: gridX - 1 }, (_, i) => FRAME.x0 + ((i + 1) * W) / gridX) : []
4660
);
4761
const gridYLines = $derived(
48-
Array.from({ length: gridY - 1 }, (_, i) => FRAME.y0 + ((i + 1) * H) / gridY)
62+
gridY > 1 ? Array.from({ length: gridY - 1 }, (_, i) => FRAME.y0 + ((i + 1) * H) / gridY) : []
4963
);
64+
65+
const baseline = $derived(localMapY(yRange[0]));
5066
</script>
5167

5268
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 64" fill="none" stroke="currentColor"
5369
stroke-linecap="round" stroke-linejoin="round">
54-
<!-- Outer screen frame -->
55-
<rect x={FRAME.x0} y={FRAME.y0} width={W} height={H} rx="3" stroke-width="1.5" />
56-
<!-- Grid -->
57-
<g stroke-width="0.6" opacity="0.4">
70+
<rect x={FRAME.x0} y={FRAME.y0} width={W} height={H} rx="4" stroke-width="1.6" />
71+
<!-- Graticule: present but clearly behind the trace. -->
72+
<g class="grid">
5873
{#each gridXLines as gx}
59-
<line x1={gx} y1={FRAME.y0 + 1} x2={gx} y2={FRAME.y1 - 1} />
74+
<line x1={gx} y1={FRAME.y0 + 2} x2={gx} y2={FRAME.y1 - 2} />
6075
{/each}
6176
{#each gridYLines as gy}
62-
<line x1={FRAME.x0 + 1} y1={gy} x2={FRAME.x1 - 1} y2={gy} />
77+
<line x1={FRAME.x0 + 2} y1={gy} x2={FRAME.x1 - 2} y2={gy} />
6378
{/each}
6479
</g>
65-
<!-- Signal traces -->
66-
<path d={path} stroke-width="1.5" />
67-
{#if path2}
68-
<path d={path2} stroke-width="1.5" stroke-dasharray="4 4" stroke-dashoffset="2" />
80+
{#if bars}
81+
{#each samples as [t, v]}
82+
<line class="bar" x1={localMapX(t)} y1={baseline} x2={localMapX(t)} y2={localMapY(v)} />
83+
{/each}
84+
<line class="baseline" x1={plotX0} y1={baseline} x2={plotX1} y2={baseline} />
85+
{:else}
86+
<path d={path} stroke-width="1.6" />
87+
{#if path2}
88+
<path d={path2} stroke-width="1.6" stroke-dasharray="4 4" stroke-dashoffset="2" />
89+
{/if}
6990
{/if}
7091
</svg>
7192

@@ -75,4 +96,19 @@
7596
height: 100%;
7697
display: block;
7798
}
99+
100+
.grid {
101+
stroke-width: 0.8;
102+
opacity: 0.35;
103+
}
104+
105+
.bar {
106+
stroke-width: 3.2;
107+
stroke-linecap: butt;
108+
}
109+
110+
.baseline {
111+
stroke-width: 1.2;
112+
opacity: 0.5;
113+
}
78114
</style>

0 commit comments

Comments
 (0)