feature/treatment #16
@@ -1,10 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useId, useState } from 'react';
|
import { useId } from 'react';
|
||||||
import { FDI_LOWER_LEFT_TO_RIGHT, FDI_UPPER_LEFT_TO_RIGHT, getToothShapeKind } from '@/components/treatment/fdiToothMeta';
|
import { FDI_LOWER_LEFT_TO_RIGHT, FDI_UPPER_LEFT_TO_RIGHT, getToothShapeKind } from '@/components/treatment/fdiToothMeta';
|
||||||
import { ToothGlyph } from '@/components/ui/treatment/ToothGlyph';
|
import { ToothGlyph } from '@/components/ui/treatment/ToothGlyph';
|
||||||
import { Dropdown } from '@/components/ui/common/Dropdown';
|
|
||||||
import { FDI_CHART_DESIGNS, type FdiChartDesignId } from '@/components/ui/treatment/fdiChartDesigns';
|
|
||||||
import type { FdiToothId } from '@/types/treatment';
|
import type { FdiToothId } from '@/types/treatment';
|
||||||
|
|
||||||
const TOOTH_NUMBER_GAP = 'mt-1';
|
const TOOTH_NUMBER_GAP = 'mt-1';
|
||||||
@@ -29,10 +27,9 @@ interface FdiToothChartProps {
|
|||||||
export function FdiToothChart({ selected, onToggle, disabled }: FdiToothChartProps) {
|
export function FdiToothChart({ selected, onToggle, disabled }: FdiToothChartProps) {
|
||||||
const uid = useId().replace(/:/g, '');
|
const uid = useId().replace(/:/g, '');
|
||||||
const archPeak = 10;
|
const archPeak = 10;
|
||||||
const [chartDesign, setChartDesign] = useState<FdiChartDesignId>('clinical');
|
|
||||||
|
|
||||||
const TOOTH_TWEAKS: Record<FdiToothId, { glyph: string; offset: number; rotate: number }> = {
|
const TOOTH_TWEAKS: Record<FdiToothId, { glyph: string; offset: number; rotate: number }> = {
|
||||||
'18': { glyph: 'w-[1.9rem] h-[4.5rem]', offset: 7, rotate: -11 },
|
'18': { glyph: 'w-[1.98rem] h-[4.65rem]', offset: 7, rotate: -11 },
|
||||||
'17': { glyph: 'w-[2rem] h-[4.75rem]', offset: 8, rotate: -8 },
|
'17': { glyph: 'w-[2rem] h-[4.75rem]', offset: 8, rotate: -8 },
|
||||||
'16': { glyph: 'w-[2.1rem] h-[4.95rem]', offset: 9, rotate: -6 },
|
'16': { glyph: 'w-[2.1rem] h-[4.95rem]', offset: 9, rotate: -6 },
|
||||||
'15': { glyph: 'w-[1.8rem] h-[4.8rem]', offset: 9, rotate: -4 },
|
'15': { glyph: 'w-[1.8rem] h-[4.8rem]', offset: 9, rotate: -4 },
|
||||||
@@ -47,9 +44,9 @@ export function FdiToothChart({ selected, onToggle, disabled }: FdiToothChartPro
|
|||||||
'25': { glyph: 'w-[1.8rem] h-[4.8rem]', offset: 9, rotate: 4 },
|
'25': { glyph: 'w-[1.8rem] h-[4.8rem]', offset: 9, rotate: 4 },
|
||||||
'26': { glyph: 'w-[2.1rem] h-[4.95rem]', offset: 9, rotate: 6 },
|
'26': { glyph: 'w-[2.1rem] h-[4.95rem]', offset: 9, rotate: 6 },
|
||||||
'27': { glyph: 'w-[2rem] h-[4.75rem]', offset: 8, rotate: 8 },
|
'27': { glyph: 'w-[2rem] h-[4.75rem]', offset: 8, rotate: 8 },
|
||||||
'28': { glyph: 'w-[1.9rem] h-[4.5rem]', offset: 7, rotate: 11 },
|
'28': { glyph: 'w-[1.98rem] h-[4.65rem]', offset: 7, rotate: 11 },
|
||||||
|
|
||||||
'48': { glyph: 'w-[1.9rem] h-[4.55rem]', offset: -7, rotate: -11 },
|
'48': { glyph: 'w-[1.98rem] h-[4.7rem]', offset: -7, rotate: -11 },
|
||||||
'47': { glyph: 'w-[2rem] h-[4.8rem]', offset: -8, rotate: -8 },
|
'47': { glyph: 'w-[2rem] h-[4.8rem]', offset: -8, rotate: -8 },
|
||||||
'46': { glyph: 'w-[2.15rem] h-[5rem]', offset: -9, rotate: -6 },
|
'46': { glyph: 'w-[2.15rem] h-[5rem]', offset: -9, rotate: -6 },
|
||||||
'45': { glyph: 'w-[1.82rem] h-[4.85rem]', offset: -9, rotate: -4 },
|
'45': { glyph: 'w-[1.82rem] h-[4.85rem]', offset: -9, rotate: -4 },
|
||||||
@@ -64,7 +61,7 @@ export function FdiToothChart({ selected, onToggle, disabled }: FdiToothChartPro
|
|||||||
'35': { glyph: 'w-[1.82rem] h-[4.85rem]', offset: -9, rotate: 4 },
|
'35': { glyph: 'w-[1.82rem] h-[4.85rem]', offset: -9, rotate: 4 },
|
||||||
'36': { glyph: 'w-[2.15rem] h-[5rem]', offset: -9, rotate: 6 },
|
'36': { glyph: 'w-[2.15rem] h-[5rem]', offset: -9, rotate: 6 },
|
||||||
'37': { glyph: 'w-[2rem] h-[4.8rem]', offset: -8, rotate: 8 },
|
'37': { glyph: 'w-[2rem] h-[4.8rem]', offset: -8, rotate: 8 },
|
||||||
'38': { glyph: 'w-[1.9rem] h-[4.55rem]', offset: -7, rotate: 11 },
|
'38': { glyph: 'w-[1.98rem] h-[4.7rem]', offset: -7, rotate: 11 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const toothSizeClass = (fdi: FdiToothId) => {
|
const toothSizeClass = (fdi: FdiToothId) => {
|
||||||
@@ -133,7 +130,6 @@ export function FdiToothChart({ selected, onToggle, disabled }: FdiToothChartPro
|
|||||||
aria-label={`FDI tooth ${fdi}${isSel ? ', selected' : ''}`}
|
aria-label={`FDI tooth ${fdi}${isSel ? ', selected' : ''}`}
|
||||||
>
|
>
|
||||||
<ToothGlyph
|
<ToothGlyph
|
||||||
variant={chartDesign}
|
|
||||||
fdi={fdi}
|
fdi={fdi}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
gradientId={gid}
|
gradientId={gid}
|
||||||
@@ -161,17 +157,6 @@ export function FdiToothChart({ selected, onToggle, disabled }: FdiToothChartPro
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 sm:items-end w-full sm:max-w-xs">
|
<div className="flex flex-col gap-2 sm:items-end w-full sm:max-w-xs">
|
||||||
<Dropdown
|
|
||||||
label="Chart style (temporary)"
|
|
||||||
value={chartDesign}
|
|
||||||
onChange={(e) => setChartDesign(e.target.value as FdiChartDesignId)}
|
|
||||||
>
|
|
||||||
{FDI_CHART_DESIGNS.map((d) => (
|
|
||||||
<option key={d.id} value={d.id}>
|
|
||||||
{d.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Dropdown>
|
|
||||||
<p className="text-xs text-text-secondary tabular-nums text-right">
|
<p className="text-xs text-text-secondary tabular-nums text-right">
|
||||||
Selected: {selected.size === 0 ? '—' : [...selected].sort().join(', ')}
|
Selected: {selected.size === 0 ? '—' : [...selected].sort().join(', ')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
import { memo, type ReactNode } from 'react';
|
import { memo, type ReactNode } from 'react';
|
||||||
import type { FdiToothId } from '@/types/treatment';
|
import type { FdiToothId } from '@/types/treatment';
|
||||||
import type { ToothShapeKind } from '@/components/treatment/fdiToothMeta';
|
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 { 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 {
|
interface ToothGlyphProps {
|
||||||
fdi: FdiToothId;
|
fdi: FdiToothId;
|
||||||
@@ -19,7 +14,6 @@ interface ToothGlyphProps {
|
|||||||
mirrored?: boolean;
|
mirrored?: boolean;
|
||||||
upsideDown?: boolean;
|
upsideDown?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: FdiChartDesignId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filledSilhouette(
|
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" />;
|
return <path d={d} fill="none" stroke={stroke} strokeWidth={strokeW * widthScale} strokeLinecap="round" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderClinicalFill(model: ToothPathModel, paint: ToothGlyphPaint): ReactNode {
|
function renderClinicalFill(model: ToothPathModel, selected: boolean, gradientId: string): ReactNode {
|
||||||
const { crownFill, rootFill, stroke, strokeW } = paint;
|
const stroke = selected ? 'var(--color-primary)' : '#1e293b';
|
||||||
|
const strokeW = selected ? 2.4 : 1.35;
|
||||||
|
const fill = `url(#${gradientId})`;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{filledSilhouette(model, crownFill, rootFill, stroke, strokeW)}
|
{filledSilhouette(model, fill, fill, stroke, strokeW)}
|
||||||
{detailOnly(model, stroke, strokeW)}
|
{detailOnly(model, stroke, strokeW)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/** FDI chart tooth: clinical gradient + roots only. */
|
||||||
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). */
|
|
||||||
export const ToothGlyph = memo(function ToothGlyph({
|
export const ToothGlyph = memo(function ToothGlyph({
|
||||||
fdi,
|
fdi,
|
||||||
kind,
|
kind,
|
||||||
@@ -97,15 +60,9 @@ export const ToothGlyph = memo(function ToothGlyph({
|
|||||||
mirrored,
|
mirrored,
|
||||||
upsideDown,
|
upsideDown,
|
||||||
className = 'w-8 h-[4.85rem]',
|
className = 'w-8 h-[4.85rem]',
|
||||||
variant = 'clinical',
|
|
||||||
}: ToothGlyphProps) {
|
}: ToothGlyphProps) {
|
||||||
const lastDigit = Number(fdi[1]);
|
const model: ToothPathModel | null = getToothPathModel(fdi, kind, upper);
|
||||||
const model: ToothPathModel | null =
|
const filter = selected ? 'drop-shadow(0 0 6px rgba(9, 169, 188, 0.65))' : undefined;
|
||||||
variant === 'clinical'
|
|
||||||
? getToothPathModel(fdi, kind, upper)
|
|
||||||
: getTextbookToothPathModel(kind, upper, lastDigit);
|
|
||||||
const paint = getToothGlyphPaint(variant, gradientId, selected);
|
|
||||||
const { showClinicalGradient, filter, structuralMode } = paint;
|
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return (
|
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 (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -126,15 +83,13 @@ export const ToothGlyph = memo(function ToothGlyph({
|
|||||||
style={{ filter }}
|
style={{ filter }}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{showClinicalGradient && (
|
<defs>
|
||||||
<defs>
|
<radialGradient id={gradientId} cx="45%" cy="35%" r="65%">
|
||||||
<radialGradient id={gradientId} cx="45%" cy="35%" r="65%">
|
<stop offset="0%" stopColor={selected ? '#cffafe' : '#ffffff'} />
|
||||||
<stop offset="0%" stopColor={selected ? '#cffafe' : '#ffffff'} />
|
<stop offset="55%" stopColor={selected ? '#5eead4' : '#e0f2fe'} />
|
||||||
<stop offset="55%" stopColor={selected ? '#5eead4' : '#e0f2fe'} />
|
<stop offset="100%" stopColor={selected ? '#0e7490' : '#93c5fd'} />
|
||||||
<stop offset="100%" stopColor={selected ? '#0e7490' : '#93c5fd'} />
|
</radialGradient>
|
||||||
</radialGradient>
|
</defs>
|
||||||
</defs>
|
|
||||||
)}
|
|
||||||
<g
|
<g
|
||||||
transform={[
|
transform={[
|
||||||
mirrored ? 'translate(36 0) scale(-1 1)' : '',
|
mirrored ? 'translate(36 0) scale(-1 1)' : '',
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export const FDI_CHART_DESIGNS = [
|
|
||||||
{ id: 'clinical', label: 'Clinical (gradient + roots)' },
|
|
||||||
{ id: 'wireframeSculpt', label: 'Wireframe shells (line sculpture)' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type FdiChartDesignId = (typeof FDI_CHART_DESIGNS)[number]['id'];
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import type { FdiChartDesignId } from '@/components/ui/treatment/fdiChartDesigns';
|
|
||||||
|
|
||||||
export type ToothStructuralMode = 'fill' | 'wireframe';
|
|
||||||
|
|
||||||
export interface ToothGlyphPaint {
|
|
||||||
crownFill: string;
|
|
||||||
rootFill: string;
|
|
||||||
stroke: string;
|
|
||||||
strokeW: number;
|
|
||||||
showClinicalGradient: boolean;
|
|
||||||
structuralMode: ToothStructuralMode;
|
|
||||||
filter?: string;
|
|
||||||
wireOuter?: string;
|
|
||||||
wireInner?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getToothGlyphPaint(
|
|
||||||
variant: FdiChartDesignId,
|
|
||||||
gradientId: string,
|
|
||||||
selected: boolean,
|
|
||||||
): ToothGlyphPaint {
|
|
||||||
const strokeSel = 'var(--color-primary)';
|
|
||||||
const strokeDefault = '#1e293b';
|
|
||||||
|
|
||||||
if (variant === 'clinical') {
|
|
||||||
const g = `url(#${gradientId})`;
|
|
||||||
return {
|
|
||||||
crownFill: g,
|
|
||||||
rootFill: g,
|
|
||||||
stroke: selected ? strokeSel : strokeDefault,
|
|
||||||
strokeW: selected ? 2.4 : 1.35,
|
|
||||||
showClinicalGradient: true,
|
|
||||||
structuralMode: 'fill',
|
|
||||||
filter: selected ? 'drop-shadow(0 0 6px rgba(9, 169, 188, 0.65))' : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variant === 'wireframeSculpt') {
|
|
||||||
return {
|
|
||||||
crownFill: 'none',
|
|
||||||
rootFill: 'none',
|
|
||||||
stroke: selected ? strokeSel : '#0f172a',
|
|
||||||
strokeW: selected ? 1.65 : 1.05,
|
|
||||||
showClinicalGradient: false,
|
|
||||||
structuralMode: 'wireframe',
|
|
||||||
wireOuter: selected ? '#ecfeff' : '#f8fafc',
|
|
||||||
wireInner: '#64748b',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const _exhaustive: never = variant;
|
|
||||||
return _exhaustive;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { ToothShapeKind } from '@/components/treatment/fdiToothMeta';
|
|
||||||
|
|
||||||
/** Inner “shell” contours: scaled copies of closed paths (wireframe). */
|
|
||||||
export function wireframeInnerScale(kind: ToothShapeKind): number {
|
|
||||||
if (kind === 'molar') return 0.9;
|
|
||||||
if (kind === 'premolar') return 0.88;
|
|
||||||
if (kind === 'canine') return 0.87;
|
|
||||||
return 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Approximate transform origin per region for scaled wireframe rings. */
|
|
||||||
export function wireframeOriginY(part: 'crown' | 'root', kind: ToothShapeKind): number {
|
|
||||||
if (part === 'crown') return kind === 'canine' ? 16 : 18;
|
|
||||||
return kind === 'molar' ? 50 : kind === 'premolar' ? 46 : 48;
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ export interface ToothPathModel {
|
|||||||
detailStroke?: { d: string; widthScale: number };
|
detailStroke?: { d: string; widthScale: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** FDI-specific closed paths for the clinical (gradient) chart. Wireframe uses `getTextbookToothPathModel`. */
|
/** FDI-specific closed paths for the clinical (gradient + roots) chart. */
|
||||||
export function getToothPathModel(fdi: FdiToothId, kind: ToothShapeKind, upper?: boolean): ToothPathModel | null {
|
export function getToothPathModel(fdi: FdiToothId, kind: ToothShapeKind, upper?: boolean): ToothPathModel | null {
|
||||||
const lastDigit = Number(fdi[1]);
|
const lastDigit = Number(fdi[1]);
|
||||||
const isThirdMolar = lastDigit === 8;
|
const isThirdMolar = lastDigit === 8;
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import type { ToothShapeKind } from '@/components/treatment/fdiToothMeta';
|
|
||||||
import type { ToothPathModel } from '@/components/ui/treatment/toothPathModel';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Textbook-style silhouettes (chisel incisor, spear canine, bicuspid premolar, blocky molar).
|
|
||||||
* Used **only** for “Wireframe shells (line sculpture)” so shapes read like the first presentation,
|
|
||||||
* distinct from the clinical FDI-accurate paths.
|
|
||||||
*/
|
|
||||||
export function getTextbookToothPathModel(
|
|
||||||
kind: ToothShapeKind,
|
|
||||||
upper: boolean | undefined,
|
|
||||||
lastDigit: number,
|
|
||||||
): ToothPathModel {
|
|
||||||
switch (kind) {
|
|
||||||
case 'incisor': {
|
|
||||||
const lateral = lastDigit === 2;
|
|
||||||
if (lateral) {
|
|
||||||
return {
|
|
||||||
crown:
|
|
||||||
'M13 8 L23 8 L24 11 L24.5 26 Q23 30.5 18 31.5 Q13 30.5 11.5 26 L12 11 Z',
|
|
||||||
roots: ['M15.5 31.5 L20.5 31.5 L20 48 L18.5 69 L17.5 69 L16 48 Z'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
crown:
|
|
||||||
'M12 8 L24 8 L25 11 L25.5 27 Q24 31 18 32 Q12 31 10.5 27 L11 11 Z',
|
|
||||||
roots: ['M15 32 L21 32 L20.2 50 L18 70 L17 70 L15.8 50 Z'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'canine':
|
|
||||||
return {
|
|
||||||
crown:
|
|
||||||
'M18 6 L22 11 L24.5 18 L24 28 L21.5 32 L14.5 32 L12 28 L11.5 18 L14 11 Z',
|
|
||||||
roots: ['M15 32 L21 32 L20.5 52 L19 73 L17 73 L15.5 52 Z'],
|
|
||||||
};
|
|
||||||
case 'premolar':
|
|
||||||
return {
|
|
||||||
crown:
|
|
||||||
'M8 14 L13 9 Q15.5 7.5 18 9.5 Q20.5 7.5 23 9 L28 14 L29.5 25 L27 32 L9 32 L6.5 25 Z',
|
|
||||||
roots: [
|
|
||||||
'M13 31.5 L16.5 31.5 L15 52 L12.5 70 L10.5 69 L12 52 Z',
|
|
||||||
'M19.5 31.5 L23 31.5 L24.5 52 L25.5 69 L23.5 70 L22 52 Z',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
case 'molar': {
|
|
||||||
const crown =
|
|
||||||
'M6 14 L8 9 L12.5 7.5 Q18 9.5 23.5 7.5 L28 9 L30 14 L31 25 L29 32.5 L7 32.5 L5 25 Z';
|
|
||||||
const upperMolar = upper === true && lastDigit >= 6 && lastDigit <= 8;
|
|
||||||
if (upperMolar) {
|
|
||||||
return {
|
|
||||||
crown,
|
|
||||||
roots: [
|
|
||||||
'M8.5 32.5 L12 32.5 L10.5 54 L9 71 L7 69.5 L8.5 54 Z',
|
|
||||||
'M15.5 32.5 L20.5 32.5 L19 56 L18 72 L16.5 72 L15.5 56 Z',
|
|
||||||
'M24 32.5 L27.5 32.5 L27.5 54 L29 71 L27 69.5 L26 54 Z',
|
|
||||||
],
|
|
||||||
detailStroke: {
|
|
||||||
d: 'M11 10.5 Q18 12.5 25 10.5 M10 18 Q18 20 26 18',
|
|
||||||
widthScale: 0.55,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
crown,
|
|
||||||
roots: [
|
|
||||||
'M9 32.5 L15 32.5 L13 56 L10.5 71 L8 69.5 L10 56 Z',
|
|
||||||
'M21 32.5 L27 32.5 L26 56 L27.5 71 L25 69.5 L24 56 Z',
|
|
||||||
],
|
|
||||||
detailStroke: {
|
|
||||||
d: 'M12 11 Q18 13.5 24 11 M11 19 Q18 21.5 25 19 M16 62 Q18 65 20 62',
|
|
||||||
widthScale: 0.5,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return { crown: 'M10 10 L26 10 L26 60 L10 60 Z', roots: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user