diagram: kind=space (fixed oblique 3D view)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import type { DiagramSpec } from './types';
|
||||
import { Plane } from './plane';
|
||||
import { NumberLine } from './numberline';
|
||||
import { Solid } from './solid';
|
||||
import { Space } from './space';
|
||||
import { Chart } from './charts';
|
||||
import { Mapping, Tree, Venn } from './sets';
|
||||
import { Clock, Fraction, Grid, Groups, Pattern, Table } from './misc';
|
||||
@@ -23,6 +24,7 @@ function Body({ spec }: { spec: DiagramSpec }): ReactNode {
|
||||
case 'plane': return <Plane spec={spec} />;
|
||||
case 'numberline': return <NumberLine spec={spec} />;
|
||||
case 'solid': return <Solid spec={spec} />;
|
||||
case 'space': return <Space spec={spec} />;
|
||||
case 'bar': case 'linechart': case 'pie': return <Chart spec={spec} />;
|
||||
case 'venn': return <Venn spec={spec} />;
|
||||
case 'mapping': return <Mapping spec={spec} />;
|
||||
|
||||
125
src/diagram/space.tsx
Normal file
125
src/diagram/space.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { SpaceSpec } from './types';
|
||||
import { COLORS, INK, Label, SOFT, color, mathText } from './svg';
|
||||
|
||||
/**
|
||||
* فضای سهبعدی با تصویرِ مایلِ ثابت: x رو به بیننده (پایینچپ)، y راست، z بالا. چرخش و تعامل ندارد.
|
||||
* هر نقطه (x, y, z) به صفحه میرود با sx = y − 0.46·x ، sy = z − 0.36·x و کلِ شکل برای جا شدن در
|
||||
* کادر مقیاس میشود. جهتِ x کوتاهشده است (نسبتِ ۰٫۵۸) تا طولِ ظاهری بزرگتر از واقعی دیده نشود.
|
||||
*/
|
||||
type XY = [number, number];
|
||||
type P3 = [number, number, number];
|
||||
const W = 320;
|
||||
const H = 270;
|
||||
const PAD = 34;
|
||||
const proj = ([x, y, z]: P3): XY => [y - 0.46 * x, z - 0.36 * x];
|
||||
|
||||
export function Space({ spec }: { spec: SpaceSpec }) {
|
||||
const pts: Record<string, P3> = { O: [0, 0, 0], ...spec.points };
|
||||
const items = spec.items ?? [];
|
||||
const get = (n: string): P3 | undefined => pts[n];
|
||||
|
||||
// کرانِ شکل: همهی نقطهها، جعبهها و محورها (محور تا کمی بیشتر از بزرگترین مختصات)
|
||||
const all: P3[] = Object.values(pts);
|
||||
for (const it of items) if (it.type === 'box') all.push(it.to);
|
||||
const ext = (k: 0 | 1 | 2) => Math.max(1, ...all.map((p) => Math.abs(p[k])));
|
||||
const ax: P3 = [ext(0) * 1.15, ext(1) * 1.15, ext(2) * 1.15];
|
||||
const corners: P3[] = [...all];
|
||||
if (spec.axes !== false) corners.push([ax[0], 0, 0], [0, ax[1], 0], [0, 0, ax[2]]);
|
||||
const xs = corners.map((c) => proj(c)[0]), ys = corners.map((c) => proj(c)[1]);
|
||||
const [x0, x1, y0, y1] = [Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys)];
|
||||
const s = Math.min((W - 2 * PAD) / Math.max(x1 - x0, 1e-6), (H - 2 * PAD) / Math.max(y1 - y0, 1e-6));
|
||||
const ox = (W - (x1 - x0) * s) / 2 - x0 * s;
|
||||
const oy = (H - (y1 - y0) * s) / 2 + y1 * s;
|
||||
const T = (p: P3): XY => {
|
||||
const [u, v] = proj(p);
|
||||
return [ox + u * s, oy - v * s];
|
||||
};
|
||||
|
||||
const els: ReactNode[] = [];
|
||||
let key = 0;
|
||||
const k = () => key++;
|
||||
const line = (a: P3, b: P3, col: string, w = 1.8, dash?: string, arrow = false) => {
|
||||
const A = T(a), B = T(b);
|
||||
els.push(<line key={k()} x1={A[0]} y1={A[1]} x2={B[0]} y2={B[1]} stroke={col} strokeWidth={w} strokeDasharray={dash} strokeLinecap="round" />);
|
||||
if (arrow) {
|
||||
const ang = Math.atan2(B[1] - A[1], B[0] - A[0]);
|
||||
const h = 9, a1 = ang + Math.PI - 0.38, a2 = ang + Math.PI + 0.38;
|
||||
els.push(<path key={k()} d={`M${B[0]},${B[1]} L${B[0] + h * Math.cos(a1)},${B[1] + h * Math.sin(a1)} L${B[0] + h * Math.cos(a2)},${B[1] + h * Math.sin(a2)} Z`} fill={col} />);
|
||||
}
|
||||
};
|
||||
const text = (p: XY, t: string | undefined, col = INK, dx = 8, dy = -7) => {
|
||||
if (t) els.push(<Label key={k()} x={p[0] + dx} y={p[1] + dy} color={col} size={12}>{mathText(t)}</Label>);
|
||||
};
|
||||
|
||||
// صفحههای مختصات بهصورت شبکهی کمرنگ پشت همه چیز
|
||||
if (spec.axes !== false) {
|
||||
const mk = (a: P3, b: P3, c: P3, d: P3) => {
|
||||
const q = [a, b, c, d].map(T);
|
||||
els.push(<path key={k()} d={`M${q.map((p) => p.join(',')).join(' L')} Z`} fill={`${COLORS.blue}0c`} stroke="none" />);
|
||||
};
|
||||
mk([0, 0, 0], [0, ax[1], 0], [ax[0], ax[1], 0], [ax[0], 0, 0]);
|
||||
line([0, 0, 0], [ax[0], 0, 0], SOFT, 1.6, undefined, true);
|
||||
line([0, 0, 0], [0, ax[1], 0], SOFT, 1.6, undefined, true);
|
||||
line([0, 0, 0], [0, 0, ax[2]], SOFT, 1.6, undefined, true);
|
||||
text(T([ax[0], 0, 0]), 'x', SOFT, -10, 12);
|
||||
text(T([0, ax[1], 0]), 'y', SOFT, 8, 4);
|
||||
text(T([0, 0, ax[2]]), 'z', SOFT, 8, 6);
|
||||
}
|
||||
|
||||
// چندضلعیها اول (نیمهشفاف)، بعد جعبهها، پارهخطها، نقطهها
|
||||
for (const it of items) {
|
||||
if (it.type !== 'polygon') continue;
|
||||
const ps = it.points.map(get).filter((p): p is P3 => !!p);
|
||||
if (ps.length < 3) continue;
|
||||
const q = ps.map(T);
|
||||
const c = color(it.color, COLORS.blue);
|
||||
els.push(<path key={k()} d={`M${q.map((p) => p.join(',')).join(' L')} Z`} fill={`${c}26`} stroke={c} strokeWidth={1.8} strokeLinejoin="round" />);
|
||||
text([q.reduce((a, p) => a + p[0], 0) / q.length, q.reduce((a, p) => a + p[1], 0) / q.length], it.label, c, 0, 4);
|
||||
}
|
||||
for (const it of items) {
|
||||
if (it.type !== 'box') continue;
|
||||
const [a, b, c] = it.to;
|
||||
const col = color(it.color, COLORS.teal);
|
||||
const v = (i: number, j: number, l: number): P3 => [i * a, j * b, l * c];
|
||||
const hidden: [P3, P3][] = [[v(0, 0, 0), v(1, 0, 0)], [v(0, 0, 0), v(0, 1, 0)], [v(0, 0, 0), v(0, 0, 1)]];
|
||||
const shown: [P3, P3][] = [
|
||||
[v(1, 0, 0), v(1, 1, 0)], [v(0, 1, 0), v(1, 1, 0)],
|
||||
[v(1, 0, 0), v(1, 0, 1)], [v(0, 1, 0), v(0, 1, 1)], [v(1, 1, 0), v(1, 1, 1)],
|
||||
[v(0, 0, 1), v(1, 0, 1)], [v(0, 0, 1), v(0, 1, 1)], [v(1, 0, 1), v(1, 1, 1)], [v(0, 1, 1), v(1, 1, 1)],
|
||||
];
|
||||
hidden.forEach(([p, q]) => line(p, q, col, 1.3, '4 3'));
|
||||
shown.forEach(([p, q]) => line(p, q, col, 1.8));
|
||||
}
|
||||
for (const it of items) {
|
||||
if (it.type === 'segment' || it.type === 'vector') {
|
||||
const a = get(it.from), b = get(it.to);
|
||||
if (!a || !b) continue;
|
||||
const col = color(it.color, it.type === 'vector' ? COLORS.blue : INK);
|
||||
line(a, b, col, 2.2, it.dashed ? '5 4' : undefined, it.type === 'vector');
|
||||
const A = T(a), B = T(b);
|
||||
text([(A[0] + B[0]) / 2, (A[1] + B[1]) / 2], it.label, col, 8, -6);
|
||||
} else if (it.type === 'drop') {
|
||||
const p = get(it.point);
|
||||
if (!p) continue;
|
||||
const col = color(it.color, SOFT);
|
||||
line(p, [p[0], p[1], 0], col, 1.2, '3 3');
|
||||
line([p[0], p[1], 0], [p[0], 0, 0], col, 1.2, '3 3');
|
||||
line([p[0], p[1], 0], [0, p[1], 0], col, 1.2, '3 3');
|
||||
}
|
||||
}
|
||||
for (const it of items) {
|
||||
if (it.type !== 'point') continue;
|
||||
const p = get(it.point);
|
||||
if (!p) continue;
|
||||
const q = T(p), col = color(it.color, INK);
|
||||
els.push(<circle key={k()} cx={q[0]} cy={q[1]} r={3.6} fill={col} />);
|
||||
text(q, it.label ?? it.point, col, 8, -6);
|
||||
}
|
||||
// مبدأ
|
||||
const o = T([0, 0, 0]);
|
||||
els.push(<circle key={k()} cx={o[0]} cy={o[1]} r={2.6} fill={SOFT} />);
|
||||
if (!items.some((i) => i.type === 'point' && i.point === 'O')) text(o, 'O', SOFT, -9, 13);
|
||||
|
||||
return <svg viewBox={`0 0 ${W} ${H}`} width="100%" role="img" aria-label="شکلِ سهبعدی">{els}</svg>;
|
||||
}
|
||||
@@ -117,6 +117,14 @@ export interface SolidSpec {
|
||||
labels?: { a?: string; b?: string; c?: string; r?: string; h?: string };
|
||||
}
|
||||
|
||||
export type SpaceItem =
|
||||
| { type: 'point' | 'drop'; point: string; label?: string; color?: ColorName }
|
||||
| { type: 'vector' | 'segment'; from: string; to: string; label?: string; dashed?: boolean; color?: ColorName }
|
||||
| { type: 'polygon'; points: string[]; label?: string; color?: ColorName }
|
||||
| { type: 'box'; to: [number, number, number]; label?: string; color?: ColorName };
|
||||
/** فضای سهبعدی با تصویرِ مایلِ ثابت (x رو به بیننده، y راست، z بالا). */
|
||||
export interface SpaceSpec { kind: 'space'; title: string; axes?: boolean; points: Record<string, [number, number, number]>; items: SpaceItem[] }
|
||||
|
||||
export type DiagramSpec =
|
||||
| PlaneSpec | NumberLineSpec | FractionSpec | GridSpec | GroupsSpec | ClockSpec
|
||||
| ChartSpec | TableSpec | VennSpec | MappingSpec | TreeSpec | SolidSpec | PatternSpec;
|
||||
| ChartSpec | TableSpec | VennSpec | MappingSpec | TreeSpec | SolidSpec | PatternSpec | SpaceSpec;
|
||||
|
||||
Reference in New Issue
Block a user