Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/feedback-anatomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,21 @@ Dental review of the new paths, landmark coordinates, deformation and content is
- Browser checks cover desktop and 390×844 layouts: canal tables, Swedish passage details, transparency, jaw scrubbing/playback, the production Swedish guide redirect, hyoid attachment tabs, skull descriptions and nerve group/side controls.
- The current gingiva/core asset was preserved, as were context and per-tooth geometry. The cranial extension changes `neurovascular.glb` and the manifest.
- The original feedback extension is organized in eight commits on `codex/dental-feedback`. The description, hyoid and nerve organization follow-up is organized in three further commits. Server review and merge approval remain pending.

## Feedback recheck, 2026-10-07

The repeated feedback was checked against the current registry, localized content, visibility controls, motion engine and shipped assets. The requested features are already present:

| Request | Where to find it |
|---|---|
| Nerve openings, names and locations | Search for a foramen or nerve, then choose **Visa nervens passage**. The mental foramen panel includes premolar location and variation, and links to the mental/inferior alveolar nerves and mandibular canal. |
| Canal-count percentages | Select any permanent tooth. **Kanalantal i en studie** shows counts, percentages, sample size, method and source; coverage includes wisdom teeth. |
| Temporalis, buccinator, masseter and orbicularis oris | The existing context asset contains each requested muscle. Use **Nerver & muskler** and search or the structure tree. |
| Swedish sinus terminology | The visible preset and category say **Bihålor**; maxillary sinus names identify the sinus in the upper jaw. Older search aliases and guide redirects remain usable. |
| Individual bone/muscle transparency | Select a structure and choose **Genomskinlig**, or use the transparency control beside a layer. Individual masseter transparency was checked in the browser. |
| Additional cranial nerves | **Nervgrupper** offers trigeminal V, facial VII, IX/X/XII and all nerves. The requested nerves are selectable on both sides. |
| Joint motion and pterygoid relationships | **Käkrörelse** offers play/pause, manual opening, reset and joint-side views, with condyle, disc, eminence and muscle context. Playback and manual opening were checked in the browser. |

The trigeminal root/ganglion and V1/V2/V3 are present, but this remains a representative teaching model rather than every small branch of a complete cranial-nerve atlas. The existing schematic and draft notices remain in place.

Recheck validation: **211 tests in 30 files pass**, including all 32 permanent teeth, five-language description coverage, passage navigation and jaw motion. The shipped-asset validator passes for **411 meshes**, checking decompression, file sizes, node registration, finite coordinates, triangle counts and bounds. The production build passes. The first-visit guide now runs faster, focuses the representative tooth within the space left by its panels, keeps labels on, and uses the opposite light/dark palette with a prominent skip control.
47 changes: 47 additions & 0 deletions docs/onboarding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Scripted onboarding

The first-visit demonstration starts when the core scene is ready. It uses the
existing development timeline, search, detail panel, tooth dissection and nerve
preset. It takes about 39 seconds, excluding asset loading, and keeps labels on.
The Teeth preset clears surrounding anatomy before focusing the representative
tooth; the camera refits it after the caption and panel positions are measured.
The caption uses the inverse of the app's color theme and provides a prominent
Skip guide button. The top-bar play button replays it. Closing, skipping or pressing Escape
cancels playback and returns to the adult scene; language, theme, numbering and
orbit preferences are retained. The guide records `ds.guide.seen.v1` in local
storage when opened. If storage is blocked, it appears once per mounted session.

`src/app/tour.ts` owns the step data and the reusable, abortable player. A step
declares a caption ID, an optional `data-tour` target, its action, panel, reading
time and optional selection/dissection/loading conditions to await. `playTour`
accepts a script and a UI adapter, so another script can reuse playback without
changing its timing, pause or cancellation logic. Do not use translated labels
or positional CSS selectors as control identities. Add a stable `data-tour`
attribute to the actual control instead. Offscreen controls are scrolled into
view before measurement; desktop/mobile equivalents can share the same target.

`src/ui/GuidedTour.tsx` adapts playback to app state and renders the caption,
cursor and red pen circle. Click steps invoke real control handlers. Search
uses the same store-backed query as the search input. The representative tooth
is preloaded before playback so cancelling during loading cannot select it
later. Asset failures or missing controls offer retry and exploration. Playback
pauses when the page is hidden, supports a manual pause, and respects reduced
motion. Keyboard focus stays in the guide and returns to the replay control.

The caption avoids the demonstrated control. The app measures it along with
other panels and passes its clearance to the camera, including a top inset on
phones. Resize and position observers keep the anatomy clear as trays change.
Closing the guide removes its clearance. The cursor follows moving controls
imperatively rather than rendering React on every frame.

Captions are in `src/i18n/tour.ts` for all five interface languages. Development
steps reuse the existing draft educational descriptions, identify the scene as
schematic and expose its existing medical references. This guide introduces no
new selectable anatomy.

Run `npm test` and `npm run build`. Tests cover playback cancellation, pauses,
missing controls, visit storage, preference preservation, caption coverage,
real component target coverage and camera framing. For visual verification,
replay in desktop and compact layouts, inspect the search and scrolled detail
targets, and let the sequence finish. Check Skip and Escape during playback,
then reload to confirm it does not automatically repeat.
46 changes: 46 additions & 0 deletions docs/premolar-root-correction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Upper second premolar root correction

FDI 15 and 25 (permanent teeth) now use a schematic single root, replacing the
two-rooted BodyParts3D specimens in this teaching example. FDI 14 and 24 retain
their original two-rooted form. The crown and cervical collar are retained;
the root below the collar tapers continuously to one apex. Tissue layers and
canal paths are rebuilt from the corrected external surface.

This is **draft schematic anatomy pending dental expert review**, not a measured
reconstruction. Root dimensions and curvature are modeling choices. Existing
localized descriptions already describe the usual single root and acknowledge
anatomical variation; they remain unchanged. One root is not universal in real
patients: [Alqedairi et al., 2021](https://pubmed.ncbi.nlm.nih.gov/34389013/)
found one root and one canal to be the most common configuration for maxillary
second premolars, and two roots and canals for first premolars.

`premolar_roots.prepare_root` is shared by the full asset build and the selective
morphology refresh. Only 15/25 use the v10 tissue cache, preventing reuse of their
old bifid-root layers. The selective refresh updates core tooth shells, both
tooth-layer GLBs, and the associated manifest entries.

Regression: `python -m unittest discover -s tools/pipeline -p test_premolar_roots.py`
checks single root cross-sections at four depths, closed positive-volume geometry,
preserved crown surface, and unchanged first-premolar / lower-premolar inputs.
Before correction it failed for both 15 and 25 at 55% root depth (two contours).

Selective rebuild (from the repository root):

```sh
python tools/pipeline/refresh_morphology.py --bp3d tools/pipeline/raw/stl --out tools/pipeline/.cache/premolar-roots --only 15,25
# Optional if the native Python simplifier cannot load:
node tools/pipeline/simplify_morphology.mjs tools/pipeline/.cache/premolar-roots
node tools/pipeline/refresh_morphology.mjs tools/pipeline/.cache/premolar-roots
```

The portable simplifier uses the existing meshoptimizer dependency, keeps shell
vertices unchanged, limits relative tissue surface error to 0.001, and rejects
open, inconsistently oriented, or non-positive-volume results. On this Windows
host the native Python simplifier was blocked by application control, so this
portable path was used. No application-control settings were changed.

Verification of the final decoded compressed assets: 15/25 each have one contour
at 35%, 55%, 75%, and 90% root depth; 14/24 retain two contours at the latter
three depths. All 18 checked shell/tissue meshes are closed, consistently wound,
and have positive volume. Both corrected shells were visually inspected from
buccal and mesial projections. Description coverage and production build pass.
Binary file modified public/models/context.glb
Binary file not shown.
Binary file modified public/models/core.glb
Binary file not shown.
2 changes: 1 addition & 1 deletion public/models/manifest.json

Large diffs are not rendered by default.

Binary file modified public/models/teeth/tooth-15.glb
Binary file not shown.
Binary file modified public/models/teeth/tooth-25.glb
Binary file not shown.
9 changes: 8 additions & 1 deletion src/anatomy/paranasal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export const PARANASAL_NAMES: Record<string, Omit<Names, 'en'>> = {
'sphenoidal-sinus': { sv: 'Kilbensbihåla', de: 'Keilbeinhöhle', es: 'Seno esfenoidal', la: 'Sinus sphenoidalis' },
};
const summaries: Record<string, Record<Lang, string>> = {
'maxillary-sinus': {
en: 'An air-filled cavity in the body of the maxilla, below the orbit and lateral to the nasal cavity. Its floor lies near the roots of the upper premolars and molars. Mucus passes toward an opening high on its medial wall and then into the middle nasal meatus. The displayed space is derived from the atlas maxilla with approximate clearances; its ostium and drainage pathway are not modeled.',
sv: 'Ett luftfyllt hålrum i överkäksbenets kropp, under ögonhålan och lateralt om näshålan. Golvet ligger nära de övre premolarernas och molarernas rötter. Slem förs mot en öppning högt på den mediala väggen och vidare till mellersta näsgången. Det visade rummet är härlett från atlasens överkäksben med ungefärliga avstånd; ostium och dränageväg är inte modellerade.',
de: 'Eine luftgefüllte Höhle im Oberkieferkörper unter der Augenhöhle und seitlich der Nasenhöhle. Ihr Boden liegt nahe den Wurzeln der oberen Prämolaren und Molaren. Schleim gelangt zu einer hoch gelegenen Öffnung der medialen Wand und weiter in den mittleren Nasengang. Der Raum ist mit ungefähren Abständen aus dem Atlasoberkiefer abgeleitet; Ostium und Abflussweg sind nicht modelliert.',
es: 'Cavidad aérea del cuerpo del maxilar, bajo la órbita y lateral a la cavidad nasal. Su suelo está próximo a las raíces de los premolares y molares superiores. El moco pasa hacia una abertura alta de la pared medial y luego al meato nasal medio. El espacio mostrado deriva del maxilar del atlas con separaciones aproximadas; el ostium y la vía de drenaje no están modelados.',
la: 'Cavitas aeria in corpore maxillae, infra orbitam et lateraliter cavitati nasi. Fundus radicibus praemolarium et molarium superiorum proximus est. Mucus ad ostium alte in pariete mediali situm et deinde ad meatum nasi medium fertur. Spatium ex maxilla atlantis cum intervallis approximatis derivatum est; ostium et via effluxus non finguntur.',
},
'paranasal-sinuses': {
en: 'The maxillary, frontal and sphenoidal sinuses and ethmoidal air cells are air spaces surrounding the nasal cavity. Their mucosal lining participates in mucus clearance; their broader functions remain debated. The displayed groups are maxillary, frontal and sphenoidal sinuses. Explore each displayed group in relation to the jaws, orbit and skull base. These teaching volumes do not show drainage pathways or individual variation.',
sv: 'Käkbihålor, pannbihålor, kilbensbihålor och silbensceller är luftrum runt näshålan. Slemhinnan deltar i slemtransport; deras övriga funktioner är omdiskuterade. Här visas käkbihålor, pannbihålor och kilbensbihålor. Utforska de visade gruppernas relation till käkar, ögonhålor och skallbas. Undervisningsvolymerna visar inte dränagevägar eller individuell variation.',
Expand All @@ -29,5 +36,5 @@ const summaries: Record<string, Record<Lang, string>> = {
},
};
export function paranasalContent(lang: Lang) {
return Object.fromEntries(Object.entries(summaries).map(([key, summary]) => [key, { summary: summary[lang], sources: [{ title: 'StatPearls: Nose Paranasal Sinuses', url: 'https://www.ncbi.nlm.nih.gov/sites/books/NBK499826/' }], related: key === 'paranasal-sinuses' ? ['maxillary-sinus', 'frontal-sinus', 'sphenoidal-sinus'] : ['paranasal-sinuses', key === 'frontal-sinus' ? 'frontal-bone' : 'sphenoid-bone'] }]));
return Object.fromEntries(Object.entries(summaries).map(([key, summary]) => [key, { summary: summary[lang], sources: [{ title: 'StatPearls: Nose Paranasal Sinuses', url: 'https://www.ncbi.nlm.nih.gov/sites/books/NBK499826/' }], related: key === 'paranasal-sinuses' ? ['maxillary-sinus', 'frontal-sinus', 'sphenoidal-sinus'] : key === 'maxillary-sinus' ? ['paranasal-sinuses', 'maxilla', 'maxillary-alveolar-process', 'infraorbital-nerve'] : ['paranasal-sinuses', key === 'frontal-sinus' ? 'frontal-bone' : 'sphenoid-bone'] }]));
}
2 changes: 1 addition & 1 deletion src/anatomy/structures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('maxillary sinus (issue #25)', () => {
expect(s?.provenance).toBe('modeled');
expect(s?.categories).toEqual(['sinus']);
expect(s?.meshes).toEqual([`maxillary-sinus-${side}`]);
expect(styleKeyFor(`maxillary-sinus-${side}`, s!.categories)).toBe('sinus');
expect(styleKeyFor(`maxillary-sinus-${side}`, s!.categories)).toBe('sinus-maxillary');
}
});

Expand Down
7 changes: 6 additions & 1 deletion src/anatomy/study.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ describe('sinus and vessel study views', () => {
for (const side of ['right', 'left']) {
const structure = registry.require(`${group}-${side}`);
expect(structure.meshes.length).toBe(1);
for (const lang of LANGS) expect(resolveContent(registry, structure.id, lang).summary!.length).toBeGreaterThan(100);
for (const lang of LANGS) {
const content = resolveContent(registry, structure.id, lang);
expect(content.summary!.length).toBeGreaterThan(100);
expect(content.sources.length).toBeGreaterThan(0);
expect(content.status).toBe('draft');
}
}
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function parseRepoUrl(raw: string | undefined): string | null {
return url && /^https:\/\/\S+$/.test(url) ? url : null;
}

/** The public repository, linked from the "Made by Yoseph" credit unless VITE_DS_REPO_URL overrides it. */
/** The public repository, linked from Support repo unless VITE_DS_REPO_URL overrides it. */
export const DEFAULT_REPO_URL = 'https://github.com/Yoosseph/dental-scope';

export const REPO_URL = parseRepoUrl(import.meta.env.VITE_DS_REPO_URL) ?? DEFAULT_REPO_URL;
Loading
Loading