1// The polar grid of cymbal2_grid.m, seen from above: real radial lines from
2// the washer circle (l = 3) to the free edge (l = Nr), virtual lines beyond
3// both boundaries drawn dashed, and the azimuthal spokes. Sliders reproduce
4// the n_radial / n_theta parameters of the app.
6import { useState } from 'react';
7import { useCanvas, drawFrame, PLOT_COLORS } from '../../components/plots/canvasUtils';
8import { Figure, FigControls, FigSlider } from '../figparts';
10const HEIGHT = 340;
11const A = 0.23;
12const C = 0.029;
14export default function GridFig() {
15 const [nr, setNr] = useState(24);
16 const [nth, setNth] = useState(60);
18 const ref = useCanvas(
19 (ctx, w, h) => {
20 drawFrame(ctx, w, h);
21 const cx = w / 2;
22 const cy = h / 2 + 6;
23 const hr = (A - C) / (nr - 3);
24 const rOuterVirtual = C + (nr - 1) * hr; // l = Nr + 2
25 const scale = (Math.min(w, h) / 2 - 26) / rOuterVirtual;
26 const rl = (l: number) => Math.max(0, C + (l - 3) * hr) * scale;
28 // spokes
29 ctx.strokeStyle = 'rgba(42, 47, 56, 0.9)';
30 for (let j = 0; j < nth; j++) {
31 const th = (2 * Math.PI * j) / nth;
32 ctx.beginPath();
33 ctx.moveTo(cx + rl(3) * Math.cos(th), cy + rl(3) * Math.sin(th));
34 ctx.lineTo(cx + rl(nr) * Math.cos(th), cy + rl(nr) * Math.sin(th));
35 ctx.stroke();
36 }
38 // real circles
39 for (let l = 3; l <= nr; l++) {
40 ctx.strokeStyle = l === 3 || l === nr ? PLOT_COLORS.accent : 'rgba(139, 147, 161, 0.35)';
41 ctx.lineWidth = l === 3 || l === nr ? 2 : 1;
42 ctx.beginPath();
43 ctx.arc(cx, cy, rl(l), 0, 2 * Math.PI);
44 ctx.stroke();
45 }
46 ctx.lineWidth = 1;
48 // virtual circles, dashed
49 ctx.setLineDash([4, 4]);
50 ctx.strokeStyle = 'rgba(229, 115, 115, 0.55)';
51 for (const l of [1, 2, nr + 1, nr + 2]) {
52 const r = rl(l);
53 if (r <= 1) continue;
54 ctx.beginPath();
55 ctx.arc(cx, cy, r, 0, 2 * Math.PI);
56 ctx.stroke();
57 }
58 ctx.setLineDash([]);
60 // node dots when sparse enough to read
61 const totalNodes = (nr - 2) * nth;
62 if (totalNodes <= 2600) {
63 ctx.fillStyle = 'rgba(214, 218, 226, 0.7)';
64 for (let l = 3; l <= nr; l++) {
65 for (let j = 0; j < nth; j++) {
66 const th = (2 * Math.PI * j) / nth;
67 ctx.beginPath();
68 ctx.arc(cx + rl(l) * Math.cos(th), cy + rl(l) * Math.sin(th), 1.4, 0, 2 * Math.PI);
69 ctx.fill();
70 }
71 }
72 }
74 // legend and readouts, kept off the disk where they collide
75 ctx.font = '11px system-ui';
76 ctx.textAlign = 'left';
77 ctx.fillStyle = PLOT_COLORS.accent;
78 ctx.fillText('amber: washer circle (l = 3) and free edge (l = Nr)', 12, 20);
79 ctx.fillStyle = 'rgba(229, 115, 115, 0.9)';
80 ctx.fillText('dashed: virtual lines', 12, 38);
81 ctx.fillStyle = PLOT_COLORS.axis;
82 const stepMm = (hr * 1000).toFixed(1);
83 ctx.fillText(`radial spacing h_r = ${stepMm} mm`, 12, h - 26);
84 ctx.fillText(
85 `unknowns per step: ${(nr + 2) * nth} displacement + ${(nr + 2) * nth} stress function`,
86 12,
87 h - 8,
88 );
89 },
90 [nr, nth],
91 HEIGHT,
92 );
94 return (
95 <Figure
96 caption={
97 <>
98 The grid of Samejima's Fig. 4 for the paper's shell (23 cm radius, 2.9 cm washer
99 circle). Real nodes live on the radial lines l = 3 … N<sub>r</sub>, between the
100 washer circle and the free edge, both drawn amber. The two dashed lines beyond each
101 boundary carry virtual nodes: the fourth-order operators need neighbors two lines
102 deep on each side, and the boundary-condition equations are what determine the
103 values there. The paper's production grid is N<sub>r</sub> = 24, N<sub>θ</sub>{' '}
104 = 60, drawn here as the default.
105 </>
106 }
107 >
108 <canvas ref={ref} />
109 <FigControls>
110 <FigSlider
111 label="n_radial"
112 value={nr}
113 min={8}
114 max={40}
115 step={1}
116 format={(v) => `${Math.round(v)}`}
117 onChange={(v) => setNr(Math.round(v))}
118 />
119 <FigSlider
120 label="n_theta"
121 value={nth}
122 min={12}
123 max={96}
124 step={2}
125 format={(v) => `${Math.round(v)}`}
126 onChange={(v) => setNth(2 * Math.round(v / 2))}
127 />
128 </FigControls>
129 </Figure>
130 );
131}