feature: FDI tooth chart UI finalized.

This commit is contained in:
2026-05-08 12:42:01 +03:30
parent 1a11323b74
commit b6b408e41e
7 changed files with 22 additions and 234 deletions

View File

@@ -3,12 +3,7 @@
import { memo, type ReactNode } from 'react';
import type { FdiToothId } from '@/types/treatment';
import type { ToothShapeKind } from '@/components/treatment/fdiToothMeta';
import type { FdiChartDesignId } from '@/components/ui/treatment/fdiChartDesigns';
import { getToothGlyphPaint } from '@/components/ui/treatment/toothGlyphPaint';
import type { ToothGlyphPaint } from '@/components/ui/treatment/toothGlyphPaint';
import { getToothPathModel, type ToothPathModel } from '@/components/ui/treatment/toothPathModel';
import { getTextbookToothPathModel } from '@/components/ui/treatment/toothPathModelTextbook';
import { wireframeInnerScale, wireframeOriginY } from '@/components/ui/treatment/toothGlyphStructural';
interface ToothGlyphProps {
fdi: FdiToothId;
@@ -19,7 +14,6 @@ interface ToothGlyphProps {
mirrored?: boolean;
upsideDown?: boolean;
className?: string;
variant?: FdiChartDesignId;
}
function filledSilhouette(
@@ -45,49 +39,18 @@ function detailOnly(model: ToothPathModel, stroke: string, strokeW: number): Rea
return <path d={d} fill="none" stroke={stroke} strokeWidth={strokeW * widthScale} strokeLinecap="round" />;
}
function renderClinicalFill(model: ToothPathModel, paint: ToothGlyphPaint): ReactNode {
const { crownFill, rootFill, stroke, strokeW } = paint;
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})`;
return (
<>
{filledSilhouette(model, crownFill, rootFill, stroke, strokeW)}
{filledSilhouette(model, fill, fill, stroke, strokeW)}
{detailOnly(model, stroke, strokeW)}
</>
);
}
function renderWireframe(model: ToothPathModel, paint: ToothGlyphPaint, kind: ToothShapeKind): ReactNode {
const { stroke, strokeW } = paint;
const outer = paint.wireOuter ?? '#f8fafc';
const innerCol = paint.wireInner ?? '#64748b';
const s = wireframeInnerScale(kind);
const crownOy = wireframeOriginY('crown', kind);
const rootOy = wireframeOriginY('root', kind);
const rootScale = (i: number) => s - (model.roots.length > 1 ? i * 0.018 : 0);
return (
<>
<g>
<path d={model.crown} fill="none" stroke={outer} strokeWidth={strokeW + 2.2} strokeLinejoin="round" />
<path d={model.crown} fill="none" stroke={stroke} strokeWidth={strokeW} strokeLinejoin="round" />
<g transform={`translate(18 ${crownOy}) scale(${s}) translate(-18 ${-crownOy})`}>
<path d={model.crown} fill="none" stroke={innerCol} strokeWidth="0.6" strokeLinejoin="round" opacity={0.7} />
</g>
</g>
{model.roots.map((d, i) => (
<g key={i}>
<path d={d} fill="none" stroke={outer} strokeWidth={strokeW + 2.2} strokeLinejoin="round" />
<path d={d} fill="none" stroke={stroke} strokeWidth={strokeW} strokeLinejoin="round" />
<g transform={`translate(18 ${rootOy}) scale(${rootScale(i)}) translate(-18 ${-rootOy})`}>
<path d={d} fill="none" stroke={innerCol} strokeWidth="0.6" strokeLinejoin="round" opacity={0.65} />
</g>
</g>
))}
{detailOnly(model, stroke, strokeW)}
</>
);
}
/** Clinical uses FDI-specific paths; wireframe uses textbook silhouettes + line sculpture (original combo). */
/** FDI chart tooth: clinical gradient + roots only. */
export const ToothGlyph = memo(function ToothGlyph({
fdi,
kind,
@@ -97,15 +60,9 @@ export const ToothGlyph = memo(function ToothGlyph({
mirrored,
upsideDown,
className = 'w-8 h-[4.85rem]',
variant = 'clinical',
}: ToothGlyphProps) {
const lastDigit = Number(fdi[1]);
const model: ToothPathModel | null =
variant === 'clinical'
? getToothPathModel(fdi, kind, upper)
: getTextbookToothPathModel(kind, upper, lastDigit);
const paint = getToothGlyphPaint(variant, gradientId, selected);
const { showClinicalGradient, filter, structuralMode } = paint;
const model: ToothPathModel | null = getToothPathModel(fdi, kind, upper);
const filter = selected ? 'drop-shadow(0 0 6px rgba(9, 169, 188, 0.65))' : undefined;
if (!model) {
return (
@@ -117,7 +74,7 @@ export const ToothGlyph = memo(function ToothGlyph({
);
}
const body = structuralMode === 'wireframe' ? renderWireframe(model, paint, kind) : renderClinicalFill(model, paint);
const body = renderClinicalFill(model, selected, gradientId);
return (
<svg
@@ -126,15 +83,13 @@ export const ToothGlyph = memo(function ToothGlyph({
style={{ filter }}
aria-hidden
>
{showClinicalGradient && (
<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>
)}
<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>
<g
transform={[
mirrored ? 'translate(36 0) scale(-1 1)' : '',