Files
dyolink/frontend/src/components/ui/treatment/ToothGlyph.tsx

106 lines
3.0 KiB
TypeScript
Raw Normal View History

'use client';
2026-05-08 03:58:53 +03:30
import { memo, type ReactNode } from 'react';
import type { FdiToothId } from '@/types/treatment';
import type { ToothShapeKind } from '@/components/treatment/fdiToothMeta';
2026-05-08 03:58:53 +03:30
import { getToothPathModel, type ToothPathModel } from '@/components/ui/treatment/toothPathModel';
interface ToothGlyphProps {
2026-05-08 03:58:53 +03:30
fdi: FdiToothId;
kind: ToothShapeKind;
gradientId: string;
selected: boolean;
2026-05-08 03:58:53 +03:30
upper?: boolean;
mirrored?: boolean;
2026-05-08 03:58:53 +03:30
upsideDown?: boolean;
className?: string;
}
2026-05-08 03:58:53 +03:30
function filledSilhouette(
model: ToothPathModel,
crownFill: string,
rootFill: string,
stroke: string,
strokeW: number,
): ReactNode {
return (
<>
<path d={model.crown} fill={crownFill} stroke={stroke} strokeWidth={strokeW} strokeLinejoin="round" />
{model.roots.map((d, i) => (
<path key={i} d={d} fill={rootFill} stroke={stroke} strokeWidth={strokeW} strokeLinejoin="round" />
))}
</>
);
}
function detailOnly(model: ToothPathModel, stroke: string, strokeW: number): ReactNode {
if (!model.detailStroke) return null;
const { d, widthScale } = model.detailStroke;
return <path d={d} fill="none" stroke={stroke} strokeWidth={strokeW * widthScale} strokeLinecap="round" />;
}
2026-05-08 12:42:01 +03:30
function renderClinicalFill(model: ToothPathModel, selected: boolean, gradientId: string): ReactNode {
const stroke = selected ? 'var(--color-primary)' : '#1e293b';
const strokeW = selected ? 2.4 : 1.35;
const fill = `url(#${gradientId})`;
2026-05-08 03:58:53 +03:30
return (
<>
2026-05-08 12:42:01 +03:30
{filledSilhouette(model, fill, fill, stroke, strokeW)}
2026-05-08 03:58:53 +03:30
{detailOnly(model, stroke, strokeW)}
</>
);
}
2026-05-08 12:42:01 +03:30
/** FDI chart tooth: clinical gradient + roots only. */
export const ToothGlyph = memo(function ToothGlyph({
2026-05-08 03:58:53 +03:30
fdi,
kind,
gradientId,
selected,
2026-05-08 03:58:53 +03:30
upper,
mirrored,
2026-05-08 03:58:53 +03:30
upsideDown,
className = 'w-8 h-[4.85rem]',
}: ToothGlyphProps) {
2026-05-08 12:42:01 +03:30
const model: ToothPathModel | null = getToothPathModel(fdi, kind, upper);
const filter = selected ? 'drop-shadow(0 0 6px rgba(9, 169, 188, 0.65))' : undefined;
2026-05-08 03:58:53 +03:30
if (!model) {
return (
<svg viewBox="0 0 36 78" className={`${className} shrink-0`} aria-hidden>
<text x="4" y="40" fontSize="8" fill="currentColor" opacity="0.35">
?
</text>
</svg>
);
}
2026-05-08 12:42:01 +03:30
const body = renderClinicalFill(model, selected, gradientId);
return (
<svg
viewBox="0 0 36 78"
2026-05-08 03:58:53 +03:30
className={`${className} shrink-0`}
style={{ filter }}
aria-hidden
>
2026-05-08 12:42:01 +03:30
<defs>
<radialGradient id={gradientId} cx="45%" cy="35%" r="65%">
<stop offset="0%" stopColor={selected ? '#cffafe' : '#ffffff'} />
<stop offset="55%" stopColor={selected ? '#5eead4' : '#e0f2fe'} />
<stop offset="100%" stopColor={selected ? '#0e7490' : '#93c5fd'} />
</radialGradient>
</defs>
2026-05-08 03:58:53 +03:30
<g
transform={[
mirrored ? 'translate(36 0) scale(-1 1)' : '',
upsideDown ? 'translate(0 78) scale(1 -1)' : '',
]
.filter(Boolean)
.join(' ') || undefined}
>
{body}
</g>
</svg>
);
});