2026-05-07 03:40:29 +03:30
|
|
|
'use client';
|
|
|
|
|
|
2026-05-08 03:58:53 +03:30
|
|
|
import { memo, type ReactNode } from 'react';
|
|
|
|
|
import type { FdiToothId } from '@/types/treatment';
|
2026-05-07 03:40:29 +03:30
|
|
|
import type { ToothShapeKind } from '@/components/treatment/fdiToothMeta';
|
2026-05-08 03:58:53 +03:30
|
|
|
import { getToothPathModel, type ToothPathModel } from '@/components/ui/treatment/toothPathModel';
|
2026-05-07 03:40:29 +03:30
|
|
|
|
|
|
|
|
interface ToothGlyphProps {
|
2026-05-08 03:58:53 +03:30
|
|
|
fdi: FdiToothId;
|
2026-05-07 03:40:29 +03:30
|
|
|
kind: ToothShapeKind;
|
|
|
|
|
gradientId: string;
|
|
|
|
|
selected: boolean;
|
2026-05-08 03:58:53 +03:30
|
|
|
upper?: boolean;
|
2026-05-07 03:40:29 +03:30
|
|
|
mirrored?: boolean;
|
2026-05-08 03:58:53 +03:30
|
|
|
upsideDown?: boolean;
|
|
|
|
|
className?: string;
|
2026-05-07 03:40:29 +03:30
|
|
|
}
|
|
|
|
|
|
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. */
|
2026-05-07 03:40:29 +03:30
|
|
|
export const ToothGlyph = memo(function ToothGlyph({
|
2026-05-08 03:58:53 +03:30
|
|
|
fdi,
|
2026-05-07 03:40:29 +03:30
|
|
|
kind,
|
|
|
|
|
gradientId,
|
|
|
|
|
selected,
|
2026-05-08 03:58:53 +03:30
|
|
|
upper,
|
2026-05-07 03:40:29 +03:30
|
|
|
mirrored,
|
2026-05-08 03:58:53 +03:30
|
|
|
upsideDown,
|
|
|
|
|
className = 'w-8 h-[4.85rem]',
|
2026-05-07 03:40:29 +03:30
|
|
|
}: 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-07 03:40:29 +03:30
|
|
|
|
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);
|
2026-05-07 03:40:29 +03:30
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<svg
|
|
|
|
|
viewBox="0 0 36 78"
|
2026-05-08 03:58:53 +03:30
|
|
|
className={`${className} shrink-0`}
|
2026-05-07 03:40:29 +03:30
|
|
|
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>
|
2026-05-07 03:40:29 +03:30
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
});
|