3d-odontogram 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,10 +53,17 @@ The component renders a `<canvas>` that fills its parent — size it with a cont
|
|
|
53
53
|
Colors accept a hex number (`0xff0000`) or any CSS color string (`"#ff0000"`, `"red"`).
|
|
54
54
|
Changing the `theme` prop after mount re-applies it live — no reload of the 3D model.
|
|
55
55
|
|
|
56
|
-
##
|
|
56
|
+
## Model files (zero setup by default)
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
The `.bin.gz` model files (`ip1.bin.gz` for child, `ip3.bin.gz` for adult) are **not** bundled
|
|
59
|
+
into this npm package — they're tens of MB, far too big to ship in every install. Instead, by
|
|
60
|
+
default the component loads them from a public CDN (jsDelivr, serving the files straight from
|
|
61
|
+
this package's GitHub repo), so it works out of the box with no extra setup.
|
|
62
|
+
|
|
63
|
+
If you'd rather self-host the models (e.g. for offline use, a private network, or to avoid the
|
|
64
|
+
external CDN dependency), download `ip1.bin.gz` / `ip3.bin.gz` from
|
|
65
|
+
[`public/assets/bin`](https://github.com/jivimuz/Jivident_3D_Odontogram/tree/main/public/assets/bin)
|
|
66
|
+
into your own app's public folder and override the base path and/or file names:
|
|
60
67
|
|
|
61
68
|
```tsx
|
|
62
69
|
<Odontogram3D
|
|
@@ -97,7 +104,7 @@ handleRef.current?.applyTheme({ highlightColor: "#22c55e" });
|
|
|
97
104
|
| --- | --- | --- | --- |
|
|
98
105
|
| `toothType` | `"child" \| "adult"` | `"child"` | 20-tooth or 32-tooth model. |
|
|
99
106
|
| `theme` | `OdontogramTheme` | see below | Colors for tooth/highlight/marker/background/lights. |
|
|
100
|
-
| `modelBaseUrl` | `string` | `
|
|
107
|
+
| `modelBaseUrl` | `string` | jsDelivr CDN (this repo's `public/assets/bin`) | Base path for the `.bin.gz` model files. |
|
|
101
108
|
| `modelFileNames` | `{ child?: string; adult?: string }` | `{ child: "ip1.bin.gz", adult: "ip3.bin.gz" }` | File names per tooth type. |
|
|
102
109
|
| `initialPoints` | `MappedPoint[]` | — | Points to render as soon as the model loads. |
|
|
103
110
|
| `onPointsChange` | `(points: MappedPoint[]) => void` | — | Called whenever points are added/removed/replaced. |
|
package/dist/index.cjs
CHANGED
|
@@ -161,6 +161,7 @@ function mapForToothType(toothType) {
|
|
|
161
161
|
|
|
162
162
|
// src/engine.ts
|
|
163
163
|
var DEFAULT_MODEL_FILE_NAMES = { child: "ip1.bin.gz", adult: "ip3.bin.gz" };
|
|
164
|
+
var DEFAULT_MODEL_BASE_URL = "https://cdn.jsdelivr.net/gh/jivimuz/Jivident_3D_Odontogram@main/public/assets/bin";
|
|
164
165
|
var OdontogramEngine = class {
|
|
165
166
|
constructor(canvas, options = {}) {
|
|
166
167
|
this.raycaster = new THREE2.Raycaster();
|
|
@@ -237,7 +238,7 @@ var OdontogramEngine = class {
|
|
|
237
238
|
}
|
|
238
239
|
resolveModelUrl(toothType) {
|
|
239
240
|
var _a;
|
|
240
|
-
const base = (_a = this.options.modelBaseUrl) != null ? _a :
|
|
241
|
+
const base = (_a = this.options.modelBaseUrl) != null ? _a : DEFAULT_MODEL_BASE_URL;
|
|
241
242
|
const fileNames = __spreadValues(__spreadValues({}, DEFAULT_MODEL_FILE_NAMES), this.options.modelFileNames);
|
|
242
243
|
const fileName = toothType === "adult" ? fileNames.adult : fileNames.child;
|
|
243
244
|
return `${base.replace(/\/$/, "")}/${fileName}`;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/Odontogram3D.tsx","../src/engine.ts","../src/theme.ts","../src/toothNameMaps.ts"],"sourcesContent":["export { Odontogram3D } from \"./Odontogram3D\";\nexport type { Odontogram3DProps } from \"./Odontogram3D\";\nexport { OdontogramEngine } from \"./engine\";\nexport { DEFAULT_THEME, mergeTheme, toHexColor } from \"./theme\";\nexport { TOOTH_MESH_NAME_MAP_CHILD, TOOTH_MESH_NAME_MAP_ADULT } from \"./toothNameMaps\";\nexport type {\n ColorInput,\n MappedPoint,\n Odontogram3DHandle,\n OdontogramTheme,\n ToothType,\n} from \"./types\";\n","\"use client\";\n\nimport React, {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from \"react\";\nimport { OdontogramEngine } from \"./engine\";\nimport type {\n MappedPoint,\n Odontogram3DHandle,\n OdontogramTheme,\n ToothType,\n} from \"./types\";\n\nexport interface Odontogram3DProps {\n /** \"child\" (20 teeth) or \"adult\" (32 teeth). Default: \"child\". */\n toothType?: ToothType;\n /** Colors for the tooth, highlight, marker, canvas background and lighting. */\n theme?: OdontogramTheme;\n /** Base path where the `.bin.gz` model files are served from. Default: \"/assets/bin\". */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. */\n modelFileNames?: { child?: string; adult?: string };\n /** Diagnostic points to render as soon as the model finishes loading (e.g. from imported JSON). */\n initialPoints?: MappedPoint[];\n /** Fires whenever a point is added/removed/replaced. */\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Fires when the model starts/finishes loading. */\n onLoadingChange?: (loading: boolean) => void;\n minDistance?: number;\n maxDistance?: number;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Self-contained 3D odontogram canvas: owns its own Three.js scene/renderer/model instance,\n * so multiple instances can be mounted on the same page without colliding.\n */\nexport const Odontogram3D = forwardRef<Odontogram3DHandle, Odontogram3DProps>(\n function Odontogram3D(\n {\n toothType = \"child\",\n theme,\n modelBaseUrl,\n modelFileNames,\n initialPoints,\n onPointsChange,\n onLoadingChange,\n minDistance,\n maxDistance,\n className,\n style,\n },\n ref\n ) {\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const engineRef = useRef<OdontogramEngine | null>(null);\n const isLoadingRef = useRef(true);\n const onPointsChangeRef = useRef(onPointsChange);\n const onLoadingChangeRef = useRef(onLoadingChange);\n onPointsChangeRef.current = onPointsChange;\n onLoadingChangeRef.current = onLoadingChange;\n\n // Mount once: create the engine instance for this component's lifetime.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const engine = new OdontogramEngine(canvas, {\n theme,\n modelBaseUrl,\n modelFileNames,\n minDistance,\n maxDistance,\n onLoadingChange: (loading) => {\n isLoadingRef.current = loading;\n onLoadingChangeRef.current?.(loading);\n },\n onPointsChange: (points) => onPointsChangeRef.current?.(points),\n });\n engineRef.current = engine;\n\n engine.loadModel(toothType).then(() => {\n if (initialPoints && initialPoints.length > 0) {\n engine.setMappedPoints(initialPoints);\n }\n });\n\n const handleResize = () => engine.resize();\n window.addEventListener(\"resize\", handleResize);\n\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n engine.dispose();\n engineRef.current = null;\n };\n // Intentionally mount/unmount only: toothType/theme changes below are handled by\n // dedicated effects instead of recreating the whole scene.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Reload the model whenever toothType changes (but not on the very first mount).\n const previousToothType = useRef(toothType);\n useEffect(() => {\n if (previousToothType.current === toothType) return;\n previousToothType.current = toothType;\n engineRef.current?.loadModel(toothType);\n }, [toothType]);\n\n // Re-apply theme changes live, without reloading the model.\n useEffect(() => {\n if (!theme) return;\n engineRef.current?.applyTheme(theme);\n }, [theme]);\n\n const handlePointerDown = useCallback((event: React.MouseEvent<HTMLCanvasElement>) => {\n if (isLoadingRef.current) return;\n engineRef.current?.handlePointerDown(event.clientX, event.clientY);\n }, []);\n\n useImperativeHandle(\n ref,\n () => ({\n resetMapping: () => engineRef.current?.resetMapping(),\n getMappedPoints: () => engineRef.current?.getMappedPoints() ?? [],\n setMappedPoints: (points: MappedPoint[]) => engineRef.current?.setMappedPoints(points),\n updatePoint: (id: number, patch: Partial<MappedPoint>) => engineRef.current?.updatePoint(id, patch),\n applyTheme: (nextTheme: OdontogramTheme) => engineRef.current?.applyTheme(nextTheme),\n }),\n []\n );\n\n return (\n <canvas\n ref={canvasRef}\n className={className}\n style={{ width: \"100%\", height: \"100%\", ...style }}\n onMouseDown={handlePointerDown}\n />\n );\n }\n);\n","import * as THREE from \"three\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\nimport { GLTFLoader } from \"three/examples/jsm/loaders/GLTFLoader.js\";\nimport { DEFAULT_THEME, mergeTheme, toHexColor } from \"./theme\";\nimport { mapForToothType } from \"./toothNameMaps\";\nimport type { MappedPoint, OdontogramTheme, ToothType } from \"./types\";\n\nexport interface OdontogramEngineOptions {\n theme?: OdontogramTheme;\n /** Base path where `<toothType>` model files are served from. Default: \"/assets/bin\". */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. Defaults to \"ip1.bin.gz\" (child) / \"ip3.bin.gz\" (adult). */\n modelFileNames?: { child?: string; adult?: string };\n onLoadingChange?: (loading: boolean) => void;\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Camera orbit distance limits. */\n minDistance?: number;\n maxDistance?: number;\n}\n\nconst DEFAULT_MODEL_FILE_NAMES = { child: \"ip1.bin.gz\", adult: \"ip3.bin.gz\" };\n\n/**\n * Encapsulates one Three.js scene/renderer/model instance. Every instance owns its own\n * state (no module-level globals), so multiple <Odontogram3D /> can be mounted at once.\n */\nexport class OdontogramEngine {\n private canvas: HTMLCanvasElement;\n private scene: THREE.Scene;\n private camera: THREE.PerspectiveCamera;\n private renderer: THREE.WebGLRenderer;\n private controls: OrbitControls;\n private raycaster = new THREE.Raycaster();\n private pointer = new THREE.Vector2();\n private toothMeshes: THREE.Object3D[] = [];\n private animationFrameId: number | null = null;\n private theme: Required<OdontogramTheme>;\n private toothMaterialTemplate: THREE.MeshPhongMaterial;\n private mappedPoints: MappedPoint[] = [];\n private options: OdontogramEngineOptions;\n private disposed = false;\n\n constructor(canvas: HTMLCanvasElement, options: OdontogramEngineOptions = {}) {\n this.canvas = canvas;\n this.options = options;\n this.theme = mergeTheme(options.theme);\n\n this.scene = new THREE.Scene();\n\n this.camera = new THREE.PerspectiveCamera(\n 50,\n canvas.clientWidth / canvas.clientHeight || 1,\n 0.1,\n 200\n );\n this.camera.position.set(0, 2.5, 100);\n\n this.renderer = new THREE.WebGLRenderer({ antialias: true, canvas, alpha: true });\n this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.applyCanvasBackground();\n\n this.controls = new OrbitControls(this.camera, this.renderer.domElement);\n this.controls.enableDamping = true;\n this.controls.dampingFactor = 0.05;\n this.controls.screenSpacePanning = false;\n this.controls.minDistance = options.minDistance ?? 100;\n this.controls.maxDistance = options.maxDistance ?? 130;\n this.controls.target.set(4, 8, 0);\n\n this.toothMaterialTemplate = new THREE.MeshPhongMaterial({\n color: toHexColor(this.theme.toothColor),\n specular: 0x555555,\n shininess: 30,\n flatShading: false,\n side: THREE.DoubleSide,\n });\n\n this.setupLights();\n\n const animate = () => {\n if (this.disposed) return;\n this.animationFrameId = requestAnimationFrame(animate);\n this.controls.update();\n this.renderer.render(this.scene, this.camera);\n };\n animate();\n }\n\n private lights: {\n ambient: THREE.AmbientLight;\n key: THREE.DirectionalLight;\n fill: THREE.DirectionalLight;\n } | null = null;\n\n private setupLights() {\n const ambient = new THREE.AmbientLight(\n toHexColor(this.theme.ambientLightColor),\n this.theme.ambientLightIntensity\n );\n this.scene.add(ambient);\n\n const key = new THREE.DirectionalLight(\n toHexColor(this.theme.keyLightColor),\n this.theme.keyLightIntensity\n );\n key.position.set(10, 10, 10);\n this.scene.add(key);\n\n const fill = new THREE.DirectionalLight(\n toHexColor(this.theme.fillLightColor),\n this.theme.fillLightIntensity\n );\n fill.position.set(-10, -10, -5);\n this.scene.add(fill);\n\n this.lights = { ambient, key, fill };\n }\n\n private applyCanvasBackground() {\n this.renderer.setClearColor(\n toHexColor(this.theme.canvasBackground),\n this.theme.canvasBackgroundOpacity\n );\n }\n\n private resolveModelUrl(toothType: ToothType) {\n const base = this.options.modelBaseUrl ?? \"/assets/bin\";\n const fileNames = { ...DEFAULT_MODEL_FILE_NAMES, ...this.options.modelFileNames };\n const fileName = toothType === \"adult\" ? fileNames.adult : fileNames.child;\n return `${base.replace(/\\/$/, \"\")}/${fileName}`;\n }\n\n /** Re-apply a (partial) theme to lights, background and every tooth currently in its default state. */\n applyTheme(theme: OdontogramTheme) {\n this.theme = mergeTheme({ ...this.theme, ...theme });\n this.applyCanvasBackground();\n\n if (this.lights) {\n this.lights.ambient.color.setHex(toHexColor(this.theme.ambientLightColor));\n this.lights.ambient.intensity = this.theme.ambientLightIntensity;\n this.lights.key.color.setHex(toHexColor(this.theme.keyLightColor));\n this.lights.key.intensity = this.theme.keyLightIntensity;\n this.lights.fill.color.setHex(toHexColor(this.theme.fillLightColor));\n this.lights.fill.intensity = this.theme.fillLightIntensity;\n }\n\n this.toothMaterialTemplate.color.setHex(toHexColor(this.theme.toothColor));\n\n const mappedToothIds = new Set(this.mappedPoints.map((p) => p.toothId));\n this.forEachToothGroup((group) => {\n const isMapped = Boolean(group.userData.isMapped);\n const color = isMapped ? this.theme.highlightColor : this.theme.toothColor;\n this.setGroupColor(group, toHexColor(color));\n });\n\n // Markers use their own small sphere material, update those too.\n this.scene.children\n .filter((child) => child.name.startsWith(\"Marker-\"))\n .forEach((marker) => {\n if (marker instanceof THREE.Mesh && marker.material instanceof THREE.MeshBasicMaterial) {\n marker.material.color.setHex(toHexColor(this.theme.markerColor));\n }\n });\n\n void mappedToothIds; // kept for clarity/future use, no-op reference\n }\n\n private forEachToothGroup(callback: (group: THREE.Object3D) => void) {\n const seen = new Set<THREE.Object3D>();\n this.toothMeshes.forEach((mesh) => {\n const group = mesh.userData.parentGroup as THREE.Object3D | undefined;\n if (group && !seen.has(group)) {\n seen.add(group);\n callback(group);\n }\n });\n }\n\n private setGroupColor(group: THREE.Object3D, hex: number) {\n group.traverse((child) => {\n if (\n child instanceof THREE.Mesh &&\n child.userData.parentGroup === group &&\n child.material instanceof THREE.MeshPhongMaterial\n ) {\n child.material.color.setHex(hex);\n }\n });\n }\n\n async loadModel(toothType: ToothType) {\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) {\n this.scene.remove(oldArchGroup);\n oldArchGroup.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material.dispose();\n }\n });\n }\n\n this.toothMeshes = [];\n this.mappedPoints = [];\n this.options.onPointsChange?.([]);\n\n const loader = new GLTFLoader();\n const modelUrl = this.resolveModelUrl(toothType);\n this.options.onLoadingChange?.(true);\n\n try {\n const gltf = await loader.loadAsync(modelUrl);\n if (this.disposed) return;\n\n const loadedModel = gltf.scene;\n loadedModel.scale.set(1.0, 1.0, 1.0);\n loadedModel.position.set(0, -2, 0);\n loadedModel.rotation.set(0, 0, 0);\n loadedModel.name = \"ArchGroup\";\n\n const nameMap = mapForToothType(toothType);\n\n loadedModel.traverse((child) => {\n if (!(child instanceof THREE.Mesh)) return;\n\n child.geometry.computeVertexNormals();\n if (child.geometry.attributes.color) {\n child.geometry.deleteAttribute(\"color\");\n }\n\n let mappedName = nameMap[child.name];\n let targetObject: THREE.Object3D = child;\n\n if (!mappedName && child.parent && child.parent.type !== \"Scene\") {\n if (nameMap[child.parent.name]) {\n mappedName = nameMap[child.parent.name];\n targetObject = child.parent;\n }\n }\n\n child.material = this.toothMaterialTemplate.clone();\n\n if (!targetObject.userData.isTooth) {\n targetObject.userData = {\n toothId: mappedName ?? child.name,\n isTooth: true,\n isMapped: false,\n markerIds: [] as number[],\n };\n }\n\n child.userData.parentGroup = targetObject;\n this.toothMeshes.push(child);\n });\n\n this.scene.add(loadedModel);\n } catch (error) {\n console.error(`[odontogram-3d] Failed to load model at ${modelUrl}:`, error);\n } finally {\n this.options.onLoadingChange?.(false);\n }\n }\n\n private removeMarkers(markerIds: number[]) {\n markerIds.forEach((id) => {\n const marker = this.scene.getObjectByName(`Marker-${id}`);\n if (marker instanceof THREE.Mesh) {\n this.scene.remove(marker);\n marker.geometry.dispose();\n marker.material.dispose();\n }\n });\n }\n\n private addMarker(id: number, position: THREE.Vector3) {\n const geometry = new THREE.SphereGeometry(0.05, 8, 8);\n const material = new THREE.MeshBasicMaterial({ color: toHexColor(this.theme.markerColor) });\n const marker = new THREE.Mesh(geometry, material);\n marker.position.copy(position);\n marker.name = `Marker-${id}`;\n this.scene.add(marker);\n }\n\n private emitPointsChange() {\n this.options.onPointsChange?.([...this.mappedPoints]);\n }\n\n /** Handle a click/tap on the canvas (expects coordinates relative to the viewport, like a MouseEvent). */\n handlePointerDown(clientX: number, clientY: number) {\n const rect = this.canvas.getBoundingClientRect();\n this.pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;\n this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;\n\n this.raycaster.setFromCamera(this.pointer, this.camera);\n const intersects = this.raycaster.intersectObjects(this.toothMeshes, false);\n if (intersects.length === 0) return;\n\n const clickedMesh = intersects[0].object;\n const toothGroup = clickedMesh.userData.parentGroup as THREE.Object3D | undefined;\n if (!toothGroup || !toothGroup.userData.isTooth) return;\n\n const toothId: string = toothGroup.userData.toothId;\n const point = intersects[0].point;\n const isCurrentlyMapped = Boolean(toothGroup.userData.isMapped);\n\n if (isCurrentlyMapped) {\n this.removeMarkers(toothGroup.userData.markerIds ?? []);\n this.setGroupColor(toothGroup, toHexColor(this.theme.toothColor));\n toothGroup.userData.isMapped = false;\n toothGroup.userData.markerIds = [];\n this.mappedPoints = this.mappedPoints.filter((p) => p.toothId !== toothId);\n this.emitPointsChange();\n } else {\n const newMarkerId = Date.now();\n this.setGroupColor(toothGroup, toHexColor(this.theme.highlightColor));\n toothGroup.userData.isMapped = true;\n toothGroup.userData.markerIds = [newMarkerId];\n this.addMarker(newMarkerId, point);\n\n const newPoint: MappedPoint = {\n id: newMarkerId,\n toothId,\n x: point.x.toFixed(2),\n y: point.y.toFixed(2),\n z: point.z.toFixed(2),\n type: \"Karies\",\n description: \"\",\n };\n this.mappedPoints = [...this.mappedPoints, newPoint];\n this.emitPointsChange();\n }\n }\n\n getMappedPoints(): MappedPoint[] {\n return [...this.mappedPoints];\n }\n\n /** Patch metadata (e.g. description) on an existing point without touching the 3D scene. */\n updatePoint(id: number, patch: Partial<MappedPoint>) {\n this.mappedPoints = this.mappedPoints.map((p) => (p.id === id ? { ...p, ...patch } : p));\n this.emitPointsChange();\n }\n\n resetMapping() {\n const existingMarkerIds = this.scene.children\n .filter((c) => c.name.startsWith(\"Marker-\"))\n .map((c) => Number(c.name.replace(\"Marker-\", \"\")));\n this.removeMarkers(existingMarkerIds);\n\n this.forEachToothGroup((group) => {\n this.setGroupColor(group, toHexColor(this.theme.toothColor));\n group.userData.isMapped = false;\n group.userData.markerIds = [];\n });\n\n this.mappedPoints = [];\n this.emitPointsChange();\n }\n\n /** Replace all diagnostic points and redraw markers/highlights to match (e.g. after importing JSON). */\n setMappedPoints(points: MappedPoint[]) {\n this.resetMapping();\n\n points.forEach((point) => {\n const group = this.toothMeshes.find(\n (mesh) => (mesh.userData.parentGroup as THREE.Object3D | undefined)?.userData.toothId === point.toothId\n )?.userData.parentGroup as THREE.Object3D | undefined;\n\n if (!group) return;\n\n this.setGroupColor(group, toHexColor(this.theme.highlightColor));\n group.userData.isMapped = true;\n group.userData.markerIds = [...(group.userData.markerIds ?? []), point.id];\n\n this.addMarker(point.id, new THREE.Vector3(parseFloat(point.x), parseFloat(point.y), parseFloat(point.z)));\n });\n\n this.mappedPoints = [...points];\n this.emitPointsChange();\n }\n\n resize() {\n const container = this.canvas.parentElement;\n if (!container) return;\n const { clientWidth, clientHeight } = container;\n if (clientWidth === 0 || clientHeight === 0) return;\n\n this.camera.aspect = clientWidth / clientHeight;\n this.camera.updateProjectionMatrix();\n this.renderer.setSize(clientWidth, clientHeight, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n }\n\n dispose() {\n this.disposed = true;\n if (this.animationFrameId !== null) cancelAnimationFrame(this.animationFrameId);\n\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) this.scene.remove(oldArchGroup);\n\n this.scene.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material?.dispose();\n }\n });\n\n this.toothMaterialTemplate.dispose();\n this.controls.dispose();\n this.renderer.dispose();\n }\n}\n\nexport { DEFAULT_THEME };\n","import * as THREE from \"three\";\nimport type { ColorInput, OdontogramTheme } from \"./types\";\n\nexport const DEFAULT_THEME: Required<OdontogramTheme> = {\n toothColor: 0xf5f5dc, // Ivory\n highlightColor: 0x3b82f6, // Tailwind blue-500\n markerColor: 0xff0000, // Red\n canvasBackground: 0xecf0f1,\n canvasBackgroundOpacity: 0,\n ambientLightColor: 0xffffff,\n ambientLightIntensity: 0.8,\n keyLightColor: 0xffffff,\n keyLightIntensity: 0.6,\n fillLightColor: 0xffffff,\n fillLightIntensity: 0.4,\n};\n\nexport function mergeTheme(theme?: OdontogramTheme): Required<OdontogramTheme> {\n return { ...DEFAULT_THEME, ...theme };\n}\n\n/** Three.js color helpers accept numbers (0xff0000) or strings (\"#ff0000\"); normalize both to a hex number. */\nexport function toHexColor(input: ColorInput): number {\n if (typeof input === \"number\") return input;\n return new THREE.Color(input).getHex();\n}\n","export const TOOTH_MESH_NAME_MAP_CHILD: { [key: string]: string } = {\n Gums_Maxilla001_0: \"Gums Maxilla\",\n Gums_Mandibula001_0: \"Gums Mandibulla\",\n A_0: \"(Left Top) Molar\",\n B_0: \"(Left Top) Premolar\",\n C_0: \"(Left Top) Canine\",\n D_0: \"(Left Top) Lateral\",\n E_0: \"(Left Top) Central\",\n F_0: \"(Right Top) Central\",\n G_0: \"(Right Top) Lateral\",\n H_0: \"(Right Top) Canine\",\n I_0: \"(Right Top) Premolar\",\n J_0: \"(Right Top) Molar\",\n K_0: \"(Left Bottom) Molar\",\n L_0: \"(Left Bottom) Premolar\",\n M_0: \"(Left Bottom) Canine\",\n N_0: \"(Left Bottom) Lateral\",\n O_0: \"(Left Bottom) Central\",\n P_0: \"(Right Bottom) Central\",\n Q_0: \"(Right Bottom) Lateral\",\n R_0: \"(Right Bottom) Canine\",\n S_0: \"(Right Bottom) Premolar\",\n T_0: \"(Right Bottom) Molar\",\n};\n\nexport const TOOTH_MESH_NAME_MAP_ADULT: { [key: string]: string } = {\n // --- Rahang Atas Kanan (Quadrant 1) ---\n \"Box023_01_-_Default_0\": \"11 (Right Top) Central Incisor\",\n \"Box024_01_-_Default_0\": \"12 (Right Top) Lateral Incisor\",\n \"Box025_01_-_Default_0\": \"13 (Right Top) Canine\",\n \"Box031_01_-_Default_0\": \"14 (Right Top) First Premolar\",\n \"Box033_01_-_Default_0\": \"15 (Right Top) Second Premolar\",\n \"Box032_01_-_Default_0\": \"16 (Right Top) First Molar\",\n \"Box035_01_-_Default_0\": \"17 (Right Top) Second Molar\",\n \"Box034_01_-_Default_0\": \"18 (Right Top) Third Molar\",\n\n // --- Rahang Atas Kiri (Quadrant 2) ---\n \"Box020_01_-_Default_0\": \"21 (Left Top) Central Incisor\",\n \"Box021_01_-_Default_0\": \"22 (Left Top) Lateral Incisor\",\n \"Box022_01_-_Default_0\": \"23 (Left Top) Canine\",\n \"Box026_01_-_Default_0\": \"24 (Left Top) First Premolar\",\n \"Box028_01_-_Default_0\": \"25 (Left Top) Second Premolar\",\n \"Box027_01_-_Default_0\": \"26 (Left Top) First Molar\",\n \"Box030_01_-_Default_0\": \"27 (Left Top) Second Molar\",\n \"Box029_01_-_Default_0\": \"28 (Left Top) Third Molar\",\n\n // --- Rahang Bawah Kiri (Quadrant 3) ---\n \"Box011_01_-_Default_0\": \"38 (Left Bottom) Third Molar\",\n \"Box013_01_-_Default_0\": \"37 (Left Bottom) Second Molar\",\n \"Box008_01_-_Default_0\": \"36 (Left Bottom) First Molar\",\n \"Box009_01_-_Default_0\": \"35 (Left Bottom) Second Premolar\",\n \"Box007_01_-_Default_0\": \"34 (Left Bottom) First Premolar\",\n \"Box003_01_-_Default_0\": \"33 (Left Bottom) Canine\",\n \"Box002_01_-_Default_0\": \"32 (Left Bottom) Lateral Incisor\",\n \"Box001_01_-_Default_0\": \"31 (Left Bottom) Central Incisor\",\n\n // --- Rahang Bawah Kanan (Quadrant 4) ---\n \"Box004_01_-_Default_0\": \"41 (Right Bottom) Central Incisor\",\n \"Box005_01_-_Default_0\": \"42 (Right Bottom) Lateral Incisor\",\n \"Box006_01_-_Default_0\": \"43 (Right Bottom) Canine\",\n \"Box015_01_-_Default_0\": \"44 (Right Bottom) First Premolar\",\n \"Box017_01_-_Default_0\": \"45 (Right Bottom) Second Premolar\",\n \"Box016_01_-_Default_0\": \"46 (Right Bottom) First Molar\",\n \"Box019_01_-_Default_0\": \"47 (Right Bottom) Second Molar\",\n \"GeoSphere001_01_-_Default_0\": \"48 (Right Bottom) Third Molar\",\n\n // --- Lain-lain ---\n \"Tube002_07_-_Default_0\": \"(Top Tube) Gums Maxilla\",\n \"Tube001_07_-_Default_0\": \"(Bottom Tube) Gums Mandibula\",\n \"lingua_02_-_Default_0\": \"(Tongue) Lingua\",\n};\n\nexport function mapForToothType(toothType: string): { [key: string]: string } {\n return toothType === \"child\" || !toothType\n ? TOOTH_MESH_NAME_MAP_CHILD\n : TOOTH_MESH_NAME_MAP_ADULT;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,mBAMO;;;ACRP,IAAAA,SAAuB;AACvB,2BAA8B;AAC9B,wBAA2B;;;ACF3B,YAAuB;AAGhB,IAAM,gBAA2C;AAAA,EACtD,YAAY;AAAA;AAAA,EACZ,gBAAgB;AAAA;AAAA,EAChB,aAAa;AAAA;AAAA,EACb,kBAAkB;AAAA,EAClB,yBAAyB;AAAA,EACzB,mBAAmB;AAAA,EACnB,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,oBAAoB;AACtB;AAEO,SAAS,WAAW,OAAoD;AAC7E,SAAO,kCAAK,gBAAkB;AAChC;AAGO,SAAS,WAAW,OAA2B;AACpD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO,IAAU,YAAM,KAAK,EAAE,OAAO;AACvC;;;ACzBO,IAAM,4BAAuD;AAAA,EAClE,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEO,IAAM,4BAAuD;AAAA;AAAA,EAElE,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA;AAAA,EAGzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA;AAAA,EAGzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA;AAAA,EAGzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,+BAA+B;AAAA;AAAA,EAG/B,0BAA0B;AAAA,EAC1B,0BAA0B;AAAA,EAC1B,yBAAyB;AAC3B;AAEO,SAAS,gBAAgB,WAA8C;AAC5E,SAAO,cAAc,WAAW,CAAC,YAC7B,4BACA;AACN;;;AFxDA,IAAM,2BAA2B,EAAE,OAAO,cAAc,OAAO,aAAa;AAMrE,IAAM,mBAAN,MAAuB;AAAA,EAgB5B,YAAY,QAA2B,UAAmC,CAAC,GAAG;AAV9E,SAAQ,YAAY,IAAU,iBAAU;AACxC,SAAQ,UAAU,IAAU,eAAQ;AACpC,SAAQ,cAAgC,CAAC;AACzC,SAAQ,mBAAkC;AAG1C,SAAQ,eAA8B,CAAC;AAEvC,SAAQ,WAAW;AAiDnB,SAAQ,SAIG;AA7Fb;AA2CI,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,QAAQ,WAAW,QAAQ,KAAK;AAErC,SAAK,QAAQ,IAAU,aAAM;AAE7B,SAAK,SAAS,IAAU;AAAA,MACtB;AAAA,MACA,OAAO,cAAc,OAAO,gBAAgB;AAAA,MAC5C;AAAA,MACA;AAAA,IACF;AACA,SAAK,OAAO,SAAS,IAAI,GAAG,KAAK,GAAG;AAEpC,SAAK,WAAW,IAAU,qBAAc,EAAE,WAAW,MAAM,QAAQ,OAAO,KAAK,CAAC;AAChF,SAAK,SAAS,QAAQ,OAAO,aAAa,OAAO,YAAY;AAC7D,SAAK,SAAS,cAAc,OAAO,gBAAgB;AACnD,SAAK,sBAAsB;AAE3B,SAAK,WAAW,IAAI,mCAAc,KAAK,QAAQ,KAAK,SAAS,UAAU;AACvE,SAAK,SAAS,gBAAgB;AAC9B,SAAK,SAAS,gBAAgB;AAC9B,SAAK,SAAS,qBAAqB;AACnC,SAAK,SAAS,eAAc,aAAQ,gBAAR,YAAuB;AACnD,SAAK,SAAS,eAAc,aAAQ,gBAAR,YAAuB;AACnD,SAAK,SAAS,OAAO,IAAI,GAAG,GAAG,CAAC;AAEhC,SAAK,wBAAwB,IAAU,yBAAkB;AAAA,MACvD,OAAO,WAAW,KAAK,MAAM,UAAU;AAAA,MACvC,UAAU;AAAA,MACV,WAAW;AAAA,MACX,aAAa;AAAA,MACb,MAAY;AAAA,IACd,CAAC;AAED,SAAK,YAAY;AAEjB,UAAM,UAAU,MAAM;AACpB,UAAI,KAAK,SAAU;AACnB,WAAK,mBAAmB,sBAAsB,OAAO;AACrD,WAAK,SAAS,OAAO;AACrB,WAAK,SAAS,OAAO,KAAK,OAAO,KAAK,MAAM;AAAA,IAC9C;AACA,YAAQ;AAAA,EACV;AAAA,EAQQ,cAAc;AACpB,UAAM,UAAU,IAAU;AAAA,MACxB,WAAW,KAAK,MAAM,iBAAiB;AAAA,MACvC,KAAK,MAAM;AAAA,IACb;AACA,SAAK,MAAM,IAAI,OAAO;AAEtB,UAAM,MAAM,IAAU;AAAA,MACpB,WAAW,KAAK,MAAM,aAAa;AAAA,MACnC,KAAK,MAAM;AAAA,IACb;AACA,QAAI,SAAS,IAAI,IAAI,IAAI,EAAE;AAC3B,SAAK,MAAM,IAAI,GAAG;AAElB,UAAM,OAAO,IAAU;AAAA,MACrB,WAAW,KAAK,MAAM,cAAc;AAAA,MACpC,KAAK,MAAM;AAAA,IACb;AACA,SAAK,SAAS,IAAI,KAAK,KAAK,EAAE;AAC9B,SAAK,MAAM,IAAI,IAAI;AAEnB,SAAK,SAAS,EAAE,SAAS,KAAK,KAAK;AAAA,EACrC;AAAA,EAEQ,wBAAwB;AAC9B,SAAK,SAAS;AAAA,MACZ,WAAW,KAAK,MAAM,gBAAgB;AAAA,MACtC,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAEQ,gBAAgB,WAAsB;AA9HhD;AA+HI,UAAM,QAAO,UAAK,QAAQ,iBAAb,YAA6B;AAC1C,UAAM,YAAY,kCAAK,2BAA6B,KAAK,QAAQ;AACjE,UAAM,WAAW,cAAc,UAAU,UAAU,QAAQ,UAAU;AACrE,WAAO,GAAG,KAAK,QAAQ,OAAO,EAAE,CAAC,IAAI,QAAQ;AAAA,EAC/C;AAAA;AAAA,EAGA,WAAW,OAAwB;AACjC,SAAK,QAAQ,WAAW,kCAAK,KAAK,QAAU,MAAO;AACnD,SAAK,sBAAsB;AAE3B,QAAI,KAAK,QAAQ;AACf,WAAK,OAAO,QAAQ,MAAM,OAAO,WAAW,KAAK,MAAM,iBAAiB,CAAC;AACzE,WAAK,OAAO,QAAQ,YAAY,KAAK,MAAM;AAC3C,WAAK,OAAO,IAAI,MAAM,OAAO,WAAW,KAAK,MAAM,aAAa,CAAC;AACjE,WAAK,OAAO,IAAI,YAAY,KAAK,MAAM;AACvC,WAAK,OAAO,KAAK,MAAM,OAAO,WAAW,KAAK,MAAM,cAAc,CAAC;AACnE,WAAK,OAAO,KAAK,YAAY,KAAK,MAAM;AAAA,IAC1C;AAEA,SAAK,sBAAsB,MAAM,OAAO,WAAW,KAAK,MAAM,UAAU,CAAC;AAEzE,UAAM,iBAAiB,IAAI,IAAI,KAAK,aAAa,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;AACtE,SAAK,kBAAkB,CAAC,UAAU;AAChC,YAAM,WAAW,QAAQ,MAAM,SAAS,QAAQ;AAChD,YAAM,QAAQ,WAAW,KAAK,MAAM,iBAAiB,KAAK,MAAM;AAChE,WAAK,cAAc,OAAO,WAAW,KAAK,CAAC;AAAA,IAC7C,CAAC;AAGD,SAAK,MAAM,SACR,OAAO,CAAC,UAAU,MAAM,KAAK,WAAW,SAAS,CAAC,EAClD,QAAQ,CAAC,WAAW;AACnB,UAAI,kBAAwB,eAAQ,OAAO,oBAA0B,0BAAmB;AACtF,eAAO,SAAS,MAAM,OAAO,WAAW,KAAK,MAAM,WAAW,CAAC;AAAA,MACjE;AAAA,IACF,CAAC;AAEH,SAAK;AAAA,EACP;AAAA,EAEQ,kBAAkB,UAA2C;AACnE,UAAM,OAAO,oBAAI,IAAoB;AACrC,SAAK,YAAY,QAAQ,CAAC,SAAS;AACjC,YAAM,QAAQ,KAAK,SAAS;AAC5B,UAAI,SAAS,CAAC,KAAK,IAAI,KAAK,GAAG;AAC7B,aAAK,IAAI,KAAK;AACd,iBAAS,KAAK;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,cAAc,OAAuB,KAAa;AACxD,UAAM,SAAS,CAAC,UAAU;AACxB,UACE,iBAAuB,eACvB,MAAM,SAAS,gBAAgB,SAC/B,MAAM,oBAA0B,0BAChC;AACA,cAAM,SAAS,MAAM,OAAO,GAAG;AAAA,MACjC;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,UAAU,WAAsB;AA/LxC;AAgMI,UAAM,eAAe,KAAK,MAAM,gBAAgB,WAAW;AAC3D,QAAI,cAAc;AAChB,WAAK,MAAM,OAAO,YAAY;AAC9B,mBAAa,SAAS,CAAC,UAAU;AAC/B,YAAI,iBAAuB,aAAM;AAC/B,gBAAM,SAAS,QAAQ;AACvB,gBAAM,WAAW,MAAM;AACvB,cAAI,MAAM,QAAQ,QAAQ,EAAG,UAAS,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,cAC3D,UAAS,QAAQ;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,SAAK,cAAc,CAAC;AACpB,SAAK,eAAe,CAAC;AACrB,qBAAK,SAAQ,mBAAb,4BAA8B,CAAC;AAE/B,UAAM,SAAS,IAAI,6BAAW;AAC9B,UAAM,WAAW,KAAK,gBAAgB,SAAS;AAC/C,qBAAK,SAAQ,oBAAb,4BAA+B;AAE/B,QAAI;AACF,YAAM,OAAO,MAAM,OAAO,UAAU,QAAQ;AAC5C,UAAI,KAAK,SAAU;AAEnB,YAAM,cAAc,KAAK;AACzB,kBAAY,MAAM,IAAI,GAAK,GAAK,CAAG;AACnC,kBAAY,SAAS,IAAI,GAAG,IAAI,CAAC;AACjC,kBAAY,SAAS,IAAI,GAAG,GAAG,CAAC;AAChC,kBAAY,OAAO;AAEnB,YAAM,UAAU,gBAAgB,SAAS;AAEzC,kBAAY,SAAS,CAAC,UAAU;AAC9B,YAAI,EAAE,iBAAuB,aAAO;AAEpC,cAAM,SAAS,qBAAqB;AACpC,YAAI,MAAM,SAAS,WAAW,OAAO;AACnC,gBAAM,SAAS,gBAAgB,OAAO;AAAA,QACxC;AAEA,YAAI,aAAa,QAAQ,MAAM,IAAI;AACnC,YAAI,eAA+B;AAEnC,YAAI,CAAC,cAAc,MAAM,UAAU,MAAM,OAAO,SAAS,SAAS;AAChE,cAAI,QAAQ,MAAM,OAAO,IAAI,GAAG;AAC9B,yBAAa,QAAQ,MAAM,OAAO,IAAI;AACtC,2BAAe,MAAM;AAAA,UACvB;AAAA,QACF;AAEA,cAAM,WAAW,KAAK,sBAAsB,MAAM;AAElD,YAAI,CAAC,aAAa,SAAS,SAAS;AAClC,uBAAa,WAAW;AAAA,YACtB,SAAS,kCAAc,MAAM;AAAA,YAC7B,SAAS;AAAA,YACT,UAAU;AAAA,YACV,WAAW,CAAC;AAAA,UACd;AAAA,QACF;AAEA,cAAM,SAAS,cAAc;AAC7B,aAAK,YAAY,KAAK,KAAK;AAAA,MAC7B,CAAC;AAED,WAAK,MAAM,IAAI,WAAW;AAAA,IAC5B,SAAS,OAAO;AACd,cAAQ,MAAM,2CAA2C,QAAQ,KAAK,KAAK;AAAA,IAC7E,UAAE;AACA,uBAAK,SAAQ,oBAAb,4BAA+B;AAAA,IACjC;AAAA,EACF;AAAA,EAEQ,cAAc,WAAqB;AACzC,cAAU,QAAQ,CAAC,OAAO;AACxB,YAAM,SAAS,KAAK,MAAM,gBAAgB,UAAU,EAAE,EAAE;AACxD,UAAI,kBAAwB,aAAM;AAChC,aAAK,MAAM,OAAO,MAAM;AACxB,eAAO,SAAS,QAAQ;AACxB,eAAO,SAAS,QAAQ;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,UAAU,IAAY,UAAyB;AACrD,UAAM,WAAW,IAAU,sBAAe,MAAM,GAAG,CAAC;AACpD,UAAM,WAAW,IAAU,yBAAkB,EAAE,OAAO,WAAW,KAAK,MAAM,WAAW,EAAE,CAAC;AAC1F,UAAM,SAAS,IAAU,YAAK,UAAU,QAAQ;AAChD,WAAO,SAAS,KAAK,QAAQ;AAC7B,WAAO,OAAO,UAAU,EAAE;AAC1B,SAAK,MAAM,IAAI,MAAM;AAAA,EACvB;AAAA,EAEQ,mBAAmB;AA9R7B;AA+RI,qBAAK,SAAQ,mBAAb,4BAA8B,CAAC,GAAG,KAAK,YAAY;AAAA,EACrD;AAAA;AAAA,EAGA,kBAAkB,SAAiB,SAAiB;AAnStD;AAoSI,UAAM,OAAO,KAAK,OAAO,sBAAsB;AAC/C,SAAK,QAAQ,KAAM,UAAU,KAAK,QAAQ,KAAK,QAAS,IAAI;AAC5D,SAAK,QAAQ,IAAI,GAAG,UAAU,KAAK,OAAO,KAAK,UAAU,IAAI;AAE7D,SAAK,UAAU,cAAc,KAAK,SAAS,KAAK,MAAM;AACtD,UAAM,aAAa,KAAK,UAAU,iBAAiB,KAAK,aAAa,KAAK;AAC1E,QAAI,WAAW,WAAW,EAAG;AAE7B,UAAM,cAAc,WAAW,CAAC,EAAE;AAClC,UAAM,aAAa,YAAY,SAAS;AACxC,QAAI,CAAC,cAAc,CAAC,WAAW,SAAS,QAAS;AAEjD,UAAM,UAAkB,WAAW,SAAS;AAC5C,UAAM,QAAQ,WAAW,CAAC,EAAE;AAC5B,UAAM,oBAAoB,QAAQ,WAAW,SAAS,QAAQ;AAE9D,QAAI,mBAAmB;AACrB,WAAK,eAAc,gBAAW,SAAS,cAApB,YAAiC,CAAC,CAAC;AACtD,WAAK,cAAc,YAAY,WAAW,KAAK,MAAM,UAAU,CAAC;AAChE,iBAAW,SAAS,WAAW;AAC/B,iBAAW,SAAS,YAAY,CAAC;AACjC,WAAK,eAAe,KAAK,aAAa,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO;AACzE,WAAK,iBAAiB;AAAA,IACxB,OAAO;AACL,YAAM,cAAc,KAAK,IAAI;AAC7B,WAAK,cAAc,YAAY,WAAW,KAAK,MAAM,cAAc,CAAC;AACpE,iBAAW,SAAS,WAAW;AAC/B,iBAAW,SAAS,YAAY,CAAC,WAAW;AAC5C,WAAK,UAAU,aAAa,KAAK;AAEjC,YAAM,WAAwB;AAAA,QAC5B,IAAI;AAAA,QACJ;AAAA,QACA,GAAG,MAAM,EAAE,QAAQ,CAAC;AAAA,QACpB,GAAG,MAAM,EAAE,QAAQ,CAAC;AAAA,QACpB,GAAG,MAAM,EAAE,QAAQ,CAAC;AAAA,QACpB,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AACA,WAAK,eAAe,CAAC,GAAG,KAAK,cAAc,QAAQ;AACnD,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,kBAAiC;AAC/B,WAAO,CAAC,GAAG,KAAK,YAAY;AAAA,EAC9B;AAAA;AAAA,EAGA,YAAY,IAAY,OAA6B;AACnD,SAAK,eAAe,KAAK,aAAa,IAAI,CAAC,MAAO,EAAE,OAAO,KAAK,kCAAK,IAAM,SAAU,CAAE;AACvF,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,eAAe;AACb,UAAM,oBAAoB,KAAK,MAAM,SAClC,OAAO,CAAC,MAAM,EAAE,KAAK,WAAW,SAAS,CAAC,EAC1C,IAAI,CAAC,MAAM,OAAO,EAAE,KAAK,QAAQ,WAAW,EAAE,CAAC,CAAC;AACnD,SAAK,cAAc,iBAAiB;AAEpC,SAAK,kBAAkB,CAAC,UAAU;AAChC,WAAK,cAAc,OAAO,WAAW,KAAK,MAAM,UAAU,CAAC;AAC3D,YAAM,SAAS,WAAW;AAC1B,YAAM,SAAS,YAAY,CAAC;AAAA,IAC9B,CAAC;AAED,SAAK,eAAe,CAAC;AACrB,SAAK,iBAAiB;AAAA,EACxB;AAAA;AAAA,EAGA,gBAAgB,QAAuB;AACrC,SAAK,aAAa;AAElB,WAAO,QAAQ,CAAC,UAAU;AA9W9B;AA+WM,YAAM,SAAQ,UAAK,YAAY;AAAA,QAC7B,CAAC,SAAM;AAhXf,cAAAC;AAgXmB,mBAAAA,MAAA,KAAK,SAAS,gBAAd,gBAAAA,IAA0D,SAAS,aAAY,MAAM;AAAA;AAAA,MAClG,MAFc,mBAEX,SAAS;AAEZ,UAAI,CAAC,MAAO;AAEZ,WAAK,cAAc,OAAO,WAAW,KAAK,MAAM,cAAc,CAAC;AAC/D,YAAM,SAAS,WAAW;AAC1B,YAAM,SAAS,YAAY,CAAC,IAAI,WAAM,SAAS,cAAf,YAA4B,CAAC,GAAI,MAAM,EAAE;AAEzE,WAAK,UAAU,MAAM,IAAI,IAAU,eAAQ,WAAW,MAAM,CAAC,GAAG,WAAW,MAAM,CAAC,GAAG,WAAW,MAAM,CAAC,CAAC,CAAC;AAAA,IAC3G,CAAC;AAED,SAAK,eAAe,CAAC,GAAG,MAAM;AAC9B,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,SAAS;AACP,UAAM,YAAY,KAAK,OAAO;AAC9B,QAAI,CAAC,UAAW;AAChB,UAAM,EAAE,aAAa,aAAa,IAAI;AACtC,QAAI,gBAAgB,KAAK,iBAAiB,EAAG;AAE7C,SAAK,OAAO,SAAS,cAAc;AACnC,SAAK,OAAO,uBAAuB;AACnC,SAAK,SAAS,QAAQ,aAAa,cAAc,KAAK;AACtD,SAAK,SAAS,cAAc,OAAO,gBAAgB;AAAA,EACrD;AAAA,EAEA,UAAU;AACR,SAAK,WAAW;AAChB,QAAI,KAAK,qBAAqB,KAAM,sBAAqB,KAAK,gBAAgB;AAE9E,UAAM,eAAe,KAAK,MAAM,gBAAgB,WAAW;AAC3D,QAAI,aAAc,MAAK,MAAM,OAAO,YAAY;AAEhD,SAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,UAAI,iBAAuB,aAAM;AAC/B,cAAM,SAAS,QAAQ;AACvB,cAAM,WAAW,MAAM;AACvB,YAAI,MAAM,QAAQ,QAAQ,EAAG,UAAS,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;AAAA,YAC3D,sCAAU;AAAA,MACjB;AAAA,IACF,CAAC;AAED,SAAK,sBAAsB,QAAQ;AACnC,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;;;ADvRM;AA/FC,IAAM,mBAAe;AAAA,EAC1B,SAASC,cACP;AAAA,IACE,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GACA,KACA;AACA,UAAM,gBAAY,qBAAiC,IAAI;AACvD,UAAM,gBAAY,qBAAgC,IAAI;AACtD,UAAM,mBAAe,qBAAO,IAAI;AAChC,UAAM,wBAAoB,qBAAO,cAAc;AAC/C,UAAM,yBAAqB,qBAAO,eAAe;AACjD,sBAAkB,UAAU;AAC5B,uBAAmB,UAAU;AAG7B,gCAAU,MAAM;AACd,YAAM,SAAS,UAAU;AACzB,UAAI,CAAC,OAAQ;AAEb,YAAM,SAAS,IAAI,iBAAiB,QAAQ;AAAA,QAC1C;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,iBAAiB,CAAC,YAAY;AA9EtC;AA+EU,uBAAa,UAAU;AACvB,mCAAmB,YAAnB,4CAA6B;AAAA,QAC/B;AAAA,QACA,gBAAgB,CAAC,WAAQ;AAlFjC;AAkFoC,yCAAkB,YAAlB,2CAA4B;AAAA;AAAA,MAC1D,CAAC;AACD,gBAAU,UAAU;AAEpB,aAAO,UAAU,SAAS,EAAE,KAAK,MAAM;AACrC,YAAI,iBAAiB,cAAc,SAAS,GAAG;AAC7C,iBAAO,gBAAgB,aAAa;AAAA,QACtC;AAAA,MACF,CAAC;AAED,YAAM,eAAe,MAAM,OAAO,OAAO;AACzC,aAAO,iBAAiB,UAAU,YAAY;AAE9C,aAAO,MAAM;AACX,eAAO,oBAAoB,UAAU,YAAY;AACjD,eAAO,QAAQ;AACf,kBAAU,UAAU;AAAA,MACtB;AAAA,IAIF,GAAG,CAAC,CAAC;AAGL,UAAM,wBAAoB,qBAAO,SAAS;AAC1C,gCAAU,MAAM;AA3GpB;AA4GM,UAAI,kBAAkB,YAAY,UAAW;AAC7C,wBAAkB,UAAU;AAC5B,sBAAU,YAAV,mBAAmB,UAAU;AAAA,IAC/B,GAAG,CAAC,SAAS,CAAC;AAGd,gCAAU,MAAM;AAlHpB;AAmHM,UAAI,CAAC,MAAO;AACZ,sBAAU,YAAV,mBAAmB,WAAW;AAAA,IAChC,GAAG,CAAC,KAAK,CAAC;AAEV,UAAM,wBAAoB,0BAAY,CAAC,UAA+C;AAvH1F;AAwHM,UAAI,aAAa,QAAS;AAC1B,sBAAU,YAAV,mBAAmB,kBAAkB,MAAM,SAAS,MAAM;AAAA,IAC5D,GAAG,CAAC,CAAC;AAEL;AAAA,MACE;AAAA,MACA,OAAO;AAAA,QACL,cAAc,MAAG;AA/HzB;AA+H4B,iCAAU,YAAV,mBAAmB;AAAA;AAAA,QACvC,iBAAiB,MAAG;AAhI5B;AAgI+B,uCAAU,YAAV,mBAAmB,sBAAnB,YAAwC,CAAC;AAAA;AAAA,QAChE,iBAAiB,CAAC,WAAuB;AAjIjD;AAiIoD,iCAAU,YAAV,mBAAmB,gBAAgB;AAAA;AAAA,QAC/E,aAAa,CAAC,IAAY,UAA6B;AAlI/D;AAkIkE,iCAAU,YAAV,mBAAmB,YAAY,IAAI;AAAA;AAAA,QAC7F,YAAY,CAAC,cAA4B;AAnIjD;AAmIoD,iCAAU,YAAV,mBAAmB,WAAW;AAAA;AAAA,MAC5E;AAAA,MACA,CAAC;AAAA,IACH;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL;AAAA,QACA,OAAO,iBAAE,OAAO,QAAQ,QAAQ,UAAW;AAAA,QAC3C,aAAa;AAAA;AAAA,IACf;AAAA,EAEJ;AACF;","names":["THREE","_a","Odontogram3D"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Odontogram3D.tsx","../src/engine.ts","../src/theme.ts","../src/toothNameMaps.ts"],"sourcesContent":["export { Odontogram3D } from \"./Odontogram3D\";\nexport type { Odontogram3DProps } from \"./Odontogram3D\";\nexport { OdontogramEngine } from \"./engine\";\nexport { DEFAULT_THEME, mergeTheme, toHexColor } from \"./theme\";\nexport { TOOTH_MESH_NAME_MAP_CHILD, TOOTH_MESH_NAME_MAP_ADULT } from \"./toothNameMaps\";\nexport type {\n ColorInput,\n MappedPoint,\n Odontogram3DHandle,\n OdontogramTheme,\n ToothType,\n} from \"./types\";\n","\"use client\";\n\nimport React, {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from \"react\";\nimport { OdontogramEngine } from \"./engine\";\nimport type {\n MappedPoint,\n Odontogram3DHandle,\n OdontogramTheme,\n ToothType,\n} from \"./types\";\n\nexport interface Odontogram3DProps {\n /** \"child\" (20 teeth) or \"adult\" (32 teeth). Default: \"child\". */\n toothType?: ToothType;\n /** Colors for the tooth, highlight, marker, canvas background and lighting. */\n theme?: OdontogramTheme;\n /** Base path where the `.bin.gz` model files are served from. Default: \"/assets/bin\". */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. */\n modelFileNames?: { child?: string; adult?: string };\n /** Diagnostic points to render as soon as the model finishes loading (e.g. from imported JSON). */\n initialPoints?: MappedPoint[];\n /** Fires whenever a point is added/removed/replaced. */\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Fires when the model starts/finishes loading. */\n onLoadingChange?: (loading: boolean) => void;\n minDistance?: number;\n maxDistance?: number;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Self-contained 3D odontogram canvas: owns its own Three.js scene/renderer/model instance,\n * so multiple instances can be mounted on the same page without colliding.\n */\nexport const Odontogram3D = forwardRef<Odontogram3DHandle, Odontogram3DProps>(\n function Odontogram3D(\n {\n toothType = \"child\",\n theme,\n modelBaseUrl,\n modelFileNames,\n initialPoints,\n onPointsChange,\n onLoadingChange,\n minDistance,\n maxDistance,\n className,\n style,\n },\n ref\n ) {\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const engineRef = useRef<OdontogramEngine | null>(null);\n const isLoadingRef = useRef(true);\n const onPointsChangeRef = useRef(onPointsChange);\n const onLoadingChangeRef = useRef(onLoadingChange);\n onPointsChangeRef.current = onPointsChange;\n onLoadingChangeRef.current = onLoadingChange;\n\n // Mount once: create the engine instance for this component's lifetime.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const engine = new OdontogramEngine(canvas, {\n theme,\n modelBaseUrl,\n modelFileNames,\n minDistance,\n maxDistance,\n onLoadingChange: (loading) => {\n isLoadingRef.current = loading;\n onLoadingChangeRef.current?.(loading);\n },\n onPointsChange: (points) => onPointsChangeRef.current?.(points),\n });\n engineRef.current = engine;\n\n engine.loadModel(toothType).then(() => {\n if (initialPoints && initialPoints.length > 0) {\n engine.setMappedPoints(initialPoints);\n }\n });\n\n const handleResize = () => engine.resize();\n window.addEventListener(\"resize\", handleResize);\n\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n engine.dispose();\n engineRef.current = null;\n };\n // Intentionally mount/unmount only: toothType/theme changes below are handled by\n // dedicated effects instead of recreating the whole scene.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Reload the model whenever toothType changes (but not on the very first mount).\n const previousToothType = useRef(toothType);\n useEffect(() => {\n if (previousToothType.current === toothType) return;\n previousToothType.current = toothType;\n engineRef.current?.loadModel(toothType);\n }, [toothType]);\n\n // Re-apply theme changes live, without reloading the model.\n useEffect(() => {\n if (!theme) return;\n engineRef.current?.applyTheme(theme);\n }, [theme]);\n\n const handlePointerDown = useCallback((event: React.MouseEvent<HTMLCanvasElement>) => {\n if (isLoadingRef.current) return;\n engineRef.current?.handlePointerDown(event.clientX, event.clientY);\n }, []);\n\n useImperativeHandle(\n ref,\n () => ({\n resetMapping: () => engineRef.current?.resetMapping(),\n getMappedPoints: () => engineRef.current?.getMappedPoints() ?? [],\n setMappedPoints: (points: MappedPoint[]) => engineRef.current?.setMappedPoints(points),\n updatePoint: (id: number, patch: Partial<MappedPoint>) => engineRef.current?.updatePoint(id, patch),\n applyTheme: (nextTheme: OdontogramTheme) => engineRef.current?.applyTheme(nextTheme),\n }),\n []\n );\n\n return (\n <canvas\n ref={canvasRef}\n className={className}\n style={{ width: \"100%\", height: \"100%\", ...style }}\n onMouseDown={handlePointerDown}\n />\n );\n }\n);\n","import * as THREE from \"three\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\nimport { GLTFLoader } from \"three/examples/jsm/loaders/GLTFLoader.js\";\nimport { DEFAULT_THEME, mergeTheme, toHexColor } from \"./theme\";\nimport { mapForToothType } from \"./toothNameMaps\";\nimport type { MappedPoint, OdontogramTheme, ToothType } from \"./types\";\n\nexport interface OdontogramEngineOptions {\n theme?: OdontogramTheme;\n /**\n * Base path where `<toothType>` model files are served from.\n * Default: a public CDN (jsDelivr, serving the files straight from this package's GitHub repo)\n * so the component works with zero setup. Pass your own path/URL to self-host the models instead.\n */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. Defaults to \"ip1.bin.gz\" (child) / \"ip3.bin.gz\" (adult). */\n modelFileNames?: { child?: string; adult?: string };\n onLoadingChange?: (loading: boolean) => void;\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Camera orbit distance limits. */\n minDistance?: number;\n maxDistance?: number;\n}\n\nconst DEFAULT_MODEL_FILE_NAMES = { child: \"ip1.bin.gz\", adult: \"ip3.bin.gz\" };\n\n/** Served straight from the GitHub repo via jsDelivr — no separate asset hosting needed. */\nconst DEFAULT_MODEL_BASE_URL =\n \"https://cdn.jsdelivr.net/gh/jivimuz/Jivident_3D_Odontogram@main/public/assets/bin\";\n\n/**\n * Encapsulates one Three.js scene/renderer/model instance. Every instance owns its own\n * state (no module-level globals), so multiple <Odontogram3D /> can be mounted at once.\n */\nexport class OdontogramEngine {\n private canvas: HTMLCanvasElement;\n private scene: THREE.Scene;\n private camera: THREE.PerspectiveCamera;\n private renderer: THREE.WebGLRenderer;\n private controls: OrbitControls;\n private raycaster = new THREE.Raycaster();\n private pointer = new THREE.Vector2();\n private toothMeshes: THREE.Object3D[] = [];\n private animationFrameId: number | null = null;\n private theme: Required<OdontogramTheme>;\n private toothMaterialTemplate: THREE.MeshPhongMaterial;\n private mappedPoints: MappedPoint[] = [];\n private options: OdontogramEngineOptions;\n private disposed = false;\n\n constructor(canvas: HTMLCanvasElement, options: OdontogramEngineOptions = {}) {\n this.canvas = canvas;\n this.options = options;\n this.theme = mergeTheme(options.theme);\n\n this.scene = new THREE.Scene();\n\n this.camera = new THREE.PerspectiveCamera(\n 50,\n canvas.clientWidth / canvas.clientHeight || 1,\n 0.1,\n 200\n );\n this.camera.position.set(0, 2.5, 100);\n\n this.renderer = new THREE.WebGLRenderer({ antialias: true, canvas, alpha: true });\n this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.applyCanvasBackground();\n\n this.controls = new OrbitControls(this.camera, this.renderer.domElement);\n this.controls.enableDamping = true;\n this.controls.dampingFactor = 0.05;\n this.controls.screenSpacePanning = false;\n this.controls.minDistance = options.minDistance ?? 100;\n this.controls.maxDistance = options.maxDistance ?? 130;\n this.controls.target.set(4, 8, 0);\n\n this.toothMaterialTemplate = new THREE.MeshPhongMaterial({\n color: toHexColor(this.theme.toothColor),\n specular: 0x555555,\n shininess: 30,\n flatShading: false,\n side: THREE.DoubleSide,\n });\n\n this.setupLights();\n\n const animate = () => {\n if (this.disposed) return;\n this.animationFrameId = requestAnimationFrame(animate);\n this.controls.update();\n this.renderer.render(this.scene, this.camera);\n };\n animate();\n }\n\n private lights: {\n ambient: THREE.AmbientLight;\n key: THREE.DirectionalLight;\n fill: THREE.DirectionalLight;\n } | null = null;\n\n private setupLights() {\n const ambient = new THREE.AmbientLight(\n toHexColor(this.theme.ambientLightColor),\n this.theme.ambientLightIntensity\n );\n this.scene.add(ambient);\n\n const key = new THREE.DirectionalLight(\n toHexColor(this.theme.keyLightColor),\n this.theme.keyLightIntensity\n );\n key.position.set(10, 10, 10);\n this.scene.add(key);\n\n const fill = new THREE.DirectionalLight(\n toHexColor(this.theme.fillLightColor),\n this.theme.fillLightIntensity\n );\n fill.position.set(-10, -10, -5);\n this.scene.add(fill);\n\n this.lights = { ambient, key, fill };\n }\n\n private applyCanvasBackground() {\n this.renderer.setClearColor(\n toHexColor(this.theme.canvasBackground),\n this.theme.canvasBackgroundOpacity\n );\n }\n\n private resolveModelUrl(toothType: ToothType) {\n const base = this.options.modelBaseUrl ?? DEFAULT_MODEL_BASE_URL;\n const fileNames = { ...DEFAULT_MODEL_FILE_NAMES, ...this.options.modelFileNames };\n const fileName = toothType === \"adult\" ? fileNames.adult : fileNames.child;\n return `${base.replace(/\\/$/, \"\")}/${fileName}`;\n }\n\n /** Re-apply a (partial) theme to lights, background and every tooth currently in its default state. */\n applyTheme(theme: OdontogramTheme) {\n this.theme = mergeTheme({ ...this.theme, ...theme });\n this.applyCanvasBackground();\n\n if (this.lights) {\n this.lights.ambient.color.setHex(toHexColor(this.theme.ambientLightColor));\n this.lights.ambient.intensity = this.theme.ambientLightIntensity;\n this.lights.key.color.setHex(toHexColor(this.theme.keyLightColor));\n this.lights.key.intensity = this.theme.keyLightIntensity;\n this.lights.fill.color.setHex(toHexColor(this.theme.fillLightColor));\n this.lights.fill.intensity = this.theme.fillLightIntensity;\n }\n\n this.toothMaterialTemplate.color.setHex(toHexColor(this.theme.toothColor));\n\n const mappedToothIds = new Set(this.mappedPoints.map((p) => p.toothId));\n this.forEachToothGroup((group) => {\n const isMapped = Boolean(group.userData.isMapped);\n const color = isMapped ? this.theme.highlightColor : this.theme.toothColor;\n this.setGroupColor(group, toHexColor(color));\n });\n\n // Markers use their own small sphere material, update those too.\n this.scene.children\n .filter((child) => child.name.startsWith(\"Marker-\"))\n .forEach((marker) => {\n if (marker instanceof THREE.Mesh && marker.material instanceof THREE.MeshBasicMaterial) {\n marker.material.color.setHex(toHexColor(this.theme.markerColor));\n }\n });\n\n void mappedToothIds; // kept for clarity/future use, no-op reference\n }\n\n private forEachToothGroup(callback: (group: THREE.Object3D) => void) {\n const seen = new Set<THREE.Object3D>();\n this.toothMeshes.forEach((mesh) => {\n const group = mesh.userData.parentGroup as THREE.Object3D | undefined;\n if (group && !seen.has(group)) {\n seen.add(group);\n callback(group);\n }\n });\n }\n\n private setGroupColor(group: THREE.Object3D, hex: number) {\n group.traverse((child) => {\n if (\n child instanceof THREE.Mesh &&\n child.userData.parentGroup === group &&\n child.material instanceof THREE.MeshPhongMaterial\n ) {\n child.material.color.setHex(hex);\n }\n });\n }\n\n async loadModel(toothType: ToothType) {\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) {\n this.scene.remove(oldArchGroup);\n oldArchGroup.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material.dispose();\n }\n });\n }\n\n this.toothMeshes = [];\n this.mappedPoints = [];\n this.options.onPointsChange?.([]);\n\n const loader = new GLTFLoader();\n const modelUrl = this.resolveModelUrl(toothType);\n this.options.onLoadingChange?.(true);\n\n try {\n const gltf = await loader.loadAsync(modelUrl);\n if (this.disposed) return;\n\n const loadedModel = gltf.scene;\n loadedModel.scale.set(1.0, 1.0, 1.0);\n loadedModel.position.set(0, -2, 0);\n loadedModel.rotation.set(0, 0, 0);\n loadedModel.name = \"ArchGroup\";\n\n const nameMap = mapForToothType(toothType);\n\n loadedModel.traverse((child) => {\n if (!(child instanceof THREE.Mesh)) return;\n\n child.geometry.computeVertexNormals();\n if (child.geometry.attributes.color) {\n child.geometry.deleteAttribute(\"color\");\n }\n\n let mappedName = nameMap[child.name];\n let targetObject: THREE.Object3D = child;\n\n if (!mappedName && child.parent && child.parent.type !== \"Scene\") {\n if (nameMap[child.parent.name]) {\n mappedName = nameMap[child.parent.name];\n targetObject = child.parent;\n }\n }\n\n child.material = this.toothMaterialTemplate.clone();\n\n if (!targetObject.userData.isTooth) {\n targetObject.userData = {\n toothId: mappedName ?? child.name,\n isTooth: true,\n isMapped: false,\n markerIds: [] as number[],\n };\n }\n\n child.userData.parentGroup = targetObject;\n this.toothMeshes.push(child);\n });\n\n this.scene.add(loadedModel);\n } catch (error) {\n console.error(`[odontogram-3d] Failed to load model at ${modelUrl}:`, error);\n } finally {\n this.options.onLoadingChange?.(false);\n }\n }\n\n private removeMarkers(markerIds: number[]) {\n markerIds.forEach((id) => {\n const marker = this.scene.getObjectByName(`Marker-${id}`);\n if (marker instanceof THREE.Mesh) {\n this.scene.remove(marker);\n marker.geometry.dispose();\n marker.material.dispose();\n }\n });\n }\n\n private addMarker(id: number, position: THREE.Vector3) {\n const geometry = new THREE.SphereGeometry(0.05, 8, 8);\n const material = new THREE.MeshBasicMaterial({ color: toHexColor(this.theme.markerColor) });\n const marker = new THREE.Mesh(geometry, material);\n marker.position.copy(position);\n marker.name = `Marker-${id}`;\n this.scene.add(marker);\n }\n\n private emitPointsChange() {\n this.options.onPointsChange?.([...this.mappedPoints]);\n }\n\n /** Handle a click/tap on the canvas (expects coordinates relative to the viewport, like a MouseEvent). */\n handlePointerDown(clientX: number, clientY: number) {\n const rect = this.canvas.getBoundingClientRect();\n this.pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;\n this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;\n\n this.raycaster.setFromCamera(this.pointer, this.camera);\n const intersects = this.raycaster.intersectObjects(this.toothMeshes, false);\n if (intersects.length === 0) return;\n\n const clickedMesh = intersects[0].object;\n const toothGroup = clickedMesh.userData.parentGroup as THREE.Object3D | undefined;\n if (!toothGroup || !toothGroup.userData.isTooth) return;\n\n const toothId: string = toothGroup.userData.toothId;\n const point = intersects[0].point;\n const isCurrentlyMapped = Boolean(toothGroup.userData.isMapped);\n\n if (isCurrentlyMapped) {\n this.removeMarkers(toothGroup.userData.markerIds ?? []);\n this.setGroupColor(toothGroup, toHexColor(this.theme.toothColor));\n toothGroup.userData.isMapped = false;\n toothGroup.userData.markerIds = [];\n this.mappedPoints = this.mappedPoints.filter((p) => p.toothId !== toothId);\n this.emitPointsChange();\n } else {\n const newMarkerId = Date.now();\n this.setGroupColor(toothGroup, toHexColor(this.theme.highlightColor));\n toothGroup.userData.isMapped = true;\n toothGroup.userData.markerIds = [newMarkerId];\n this.addMarker(newMarkerId, point);\n\n const newPoint: MappedPoint = {\n id: newMarkerId,\n toothId,\n x: point.x.toFixed(2),\n y: point.y.toFixed(2),\n z: point.z.toFixed(2),\n type: \"Karies\",\n description: \"\",\n };\n this.mappedPoints = [...this.mappedPoints, newPoint];\n this.emitPointsChange();\n }\n }\n\n getMappedPoints(): MappedPoint[] {\n return [...this.mappedPoints];\n }\n\n /** Patch metadata (e.g. description) on an existing point without touching the 3D scene. */\n updatePoint(id: number, patch: Partial<MappedPoint>) {\n this.mappedPoints = this.mappedPoints.map((p) => (p.id === id ? { ...p, ...patch } : p));\n this.emitPointsChange();\n }\n\n resetMapping() {\n const existingMarkerIds = this.scene.children\n .filter((c) => c.name.startsWith(\"Marker-\"))\n .map((c) => Number(c.name.replace(\"Marker-\", \"\")));\n this.removeMarkers(existingMarkerIds);\n\n this.forEachToothGroup((group) => {\n this.setGroupColor(group, toHexColor(this.theme.toothColor));\n group.userData.isMapped = false;\n group.userData.markerIds = [];\n });\n\n this.mappedPoints = [];\n this.emitPointsChange();\n }\n\n /** Replace all diagnostic points and redraw markers/highlights to match (e.g. after importing JSON). */\n setMappedPoints(points: MappedPoint[]) {\n this.resetMapping();\n\n points.forEach((point) => {\n const group = this.toothMeshes.find(\n (mesh) => (mesh.userData.parentGroup as THREE.Object3D | undefined)?.userData.toothId === point.toothId\n )?.userData.parentGroup as THREE.Object3D | undefined;\n\n if (!group) return;\n\n this.setGroupColor(group, toHexColor(this.theme.highlightColor));\n group.userData.isMapped = true;\n group.userData.markerIds = [...(group.userData.markerIds ?? []), point.id];\n\n this.addMarker(point.id, new THREE.Vector3(parseFloat(point.x), parseFloat(point.y), parseFloat(point.z)));\n });\n\n this.mappedPoints = [...points];\n this.emitPointsChange();\n }\n\n resize() {\n const container = this.canvas.parentElement;\n if (!container) return;\n const { clientWidth, clientHeight } = container;\n if (clientWidth === 0 || clientHeight === 0) return;\n\n this.camera.aspect = clientWidth / clientHeight;\n this.camera.updateProjectionMatrix();\n this.renderer.setSize(clientWidth, clientHeight, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n }\n\n dispose() {\n this.disposed = true;\n if (this.animationFrameId !== null) cancelAnimationFrame(this.animationFrameId);\n\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) this.scene.remove(oldArchGroup);\n\n this.scene.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material?.dispose();\n }\n });\n\n this.toothMaterialTemplate.dispose();\n this.controls.dispose();\n this.renderer.dispose();\n }\n}\n\nexport { DEFAULT_THEME };\n","import * as THREE from \"three\";\nimport type { ColorInput, OdontogramTheme } from \"./types\";\n\nexport const DEFAULT_THEME: Required<OdontogramTheme> = {\n toothColor: 0xf5f5dc, // Ivory\n highlightColor: 0x3b82f6, // Tailwind blue-500\n markerColor: 0xff0000, // Red\n canvasBackground: 0xecf0f1,\n canvasBackgroundOpacity: 0,\n ambientLightColor: 0xffffff,\n ambientLightIntensity: 0.8,\n keyLightColor: 0xffffff,\n keyLightIntensity: 0.6,\n fillLightColor: 0xffffff,\n fillLightIntensity: 0.4,\n};\n\nexport function mergeTheme(theme?: OdontogramTheme): Required<OdontogramTheme> {\n return { ...DEFAULT_THEME, ...theme };\n}\n\n/** Three.js color helpers accept numbers (0xff0000) or strings (\"#ff0000\"); normalize both to a hex number. */\nexport function toHexColor(input: ColorInput): number {\n if (typeof input === \"number\") return input;\n return new THREE.Color(input).getHex();\n}\n","export const TOOTH_MESH_NAME_MAP_CHILD: { [key: string]: string } = {\n Gums_Maxilla001_0: \"Gums Maxilla\",\n Gums_Mandibula001_0: \"Gums Mandibulla\",\n A_0: \"(Left Top) Molar\",\n B_0: \"(Left Top) Premolar\",\n C_0: \"(Left Top) Canine\",\n D_0: \"(Left Top) Lateral\",\n E_0: \"(Left Top) Central\",\n F_0: \"(Right Top) Central\",\n G_0: \"(Right Top) Lateral\",\n H_0: \"(Right Top) Canine\",\n I_0: \"(Right Top) Premolar\",\n J_0: \"(Right Top) Molar\",\n K_0: \"(Left Bottom) Molar\",\n L_0: \"(Left Bottom) Premolar\",\n M_0: \"(Left Bottom) Canine\",\n N_0: \"(Left Bottom) Lateral\",\n O_0: \"(Left Bottom) Central\",\n P_0: \"(Right Bottom) Central\",\n Q_0: \"(Right Bottom) Lateral\",\n R_0: \"(Right Bottom) Canine\",\n S_0: \"(Right Bottom) Premolar\",\n T_0: \"(Right Bottom) Molar\",\n};\n\nexport const TOOTH_MESH_NAME_MAP_ADULT: { [key: string]: string } = {\n // --- Rahang Atas Kanan (Quadrant 1) ---\n \"Box023_01_-_Default_0\": \"11 (Right Top) Central Incisor\",\n \"Box024_01_-_Default_0\": \"12 (Right Top) Lateral Incisor\",\n \"Box025_01_-_Default_0\": \"13 (Right Top) Canine\",\n \"Box031_01_-_Default_0\": \"14 (Right Top) First Premolar\",\n \"Box033_01_-_Default_0\": \"15 (Right Top) Second Premolar\",\n \"Box032_01_-_Default_0\": \"16 (Right Top) First Molar\",\n \"Box035_01_-_Default_0\": \"17 (Right Top) Second Molar\",\n \"Box034_01_-_Default_0\": \"18 (Right Top) Third Molar\",\n\n // --- Rahang Atas Kiri (Quadrant 2) ---\n \"Box020_01_-_Default_0\": \"21 (Left Top) Central Incisor\",\n \"Box021_01_-_Default_0\": \"22 (Left Top) Lateral Incisor\",\n \"Box022_01_-_Default_0\": \"23 (Left Top) Canine\",\n \"Box026_01_-_Default_0\": \"24 (Left Top) First Premolar\",\n \"Box028_01_-_Default_0\": \"25 (Left Top) Second Premolar\",\n \"Box027_01_-_Default_0\": \"26 (Left Top) First Molar\",\n \"Box030_01_-_Default_0\": \"27 (Left Top) Second Molar\",\n \"Box029_01_-_Default_0\": \"28 (Left Top) Third Molar\",\n\n // --- Rahang Bawah Kiri (Quadrant 3) ---\n \"Box011_01_-_Default_0\": \"38 (Left Bottom) Third Molar\",\n \"Box013_01_-_Default_0\": \"37 (Left Bottom) Second Molar\",\n \"Box008_01_-_Default_0\": \"36 (Left Bottom) First Molar\",\n \"Box009_01_-_Default_0\": \"35 (Left Bottom) Second Premolar\",\n \"Box007_01_-_Default_0\": \"34 (Left Bottom) First Premolar\",\n \"Box003_01_-_Default_0\": \"33 (Left Bottom) Canine\",\n \"Box002_01_-_Default_0\": \"32 (Left Bottom) Lateral Incisor\",\n \"Box001_01_-_Default_0\": \"31 (Left Bottom) Central Incisor\",\n\n // --- Rahang Bawah Kanan (Quadrant 4) ---\n \"Box004_01_-_Default_0\": \"41 (Right Bottom) Central Incisor\",\n \"Box005_01_-_Default_0\": \"42 (Right Bottom) Lateral Incisor\",\n \"Box006_01_-_Default_0\": \"43 (Right Bottom) Canine\",\n \"Box015_01_-_Default_0\": \"44 (Right Bottom) First Premolar\",\n \"Box017_01_-_Default_0\": \"45 (Right Bottom) Second Premolar\",\n \"Box016_01_-_Default_0\": \"46 (Right Bottom) First Molar\",\n \"Box019_01_-_Default_0\": \"47 (Right Bottom) Second Molar\",\n \"GeoSphere001_01_-_Default_0\": \"48 (Right Bottom) Third Molar\",\n\n // --- Lain-lain ---\n \"Tube002_07_-_Default_0\": \"(Top Tube) Gums Maxilla\",\n \"Tube001_07_-_Default_0\": \"(Bottom Tube) Gums Mandibula\",\n \"lingua_02_-_Default_0\": \"(Tongue) Lingua\",\n};\n\nexport function mapForToothType(toothType: string): { [key: string]: string } {\n return toothType === \"child\" || !toothType\n ? TOOTH_MESH_NAME_MAP_CHILD\n : TOOTH_MESH_NAME_MAP_ADULT;\n}\n"],"mappings":"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","names":["THREE","_a","Odontogram3D"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -78,7 +78,11 @@ declare function toHexColor(input: ColorInput): number;
|
|
|
78
78
|
|
|
79
79
|
interface OdontogramEngineOptions {
|
|
80
80
|
theme?: OdontogramTheme;
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Base path where `<toothType>` model files are served from.
|
|
83
|
+
* Default: a public CDN (jsDelivr, serving the files straight from this package's GitHub repo)
|
|
84
|
+
* so the component works with zero setup. Pass your own path/URL to self-host the models instead.
|
|
85
|
+
*/
|
|
82
86
|
modelBaseUrl?: string;
|
|
83
87
|
/** Override the model file name per tooth type. Defaults to "ip1.bin.gz" (child) / "ip3.bin.gz" (adult). */
|
|
84
88
|
modelFileNames?: {
|
package/dist/index.d.ts
CHANGED
|
@@ -78,7 +78,11 @@ declare function toHexColor(input: ColorInput): number;
|
|
|
78
78
|
|
|
79
79
|
interface OdontogramEngineOptions {
|
|
80
80
|
theme?: OdontogramTheme;
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Base path where `<toothType>` model files are served from.
|
|
83
|
+
* Default: a public CDN (jsDelivr, serving the files straight from this package's GitHub repo)
|
|
84
|
+
* so the component works with zero setup. Pass your own path/URL to self-host the models instead.
|
|
85
|
+
*/
|
|
82
86
|
modelBaseUrl?: string;
|
|
83
87
|
/** Override the model file name per tooth type. Defaults to "ip1.bin.gz" (child) / "ip3.bin.gz" (adult). */
|
|
84
88
|
modelFileNames?: {
|
package/dist/index.js
CHANGED
|
@@ -128,6 +128,7 @@ function mapForToothType(toothType) {
|
|
|
128
128
|
|
|
129
129
|
// src/engine.ts
|
|
130
130
|
var DEFAULT_MODEL_FILE_NAMES = { child: "ip1.bin.gz", adult: "ip3.bin.gz" };
|
|
131
|
+
var DEFAULT_MODEL_BASE_URL = "https://cdn.jsdelivr.net/gh/jivimuz/Jivident_3D_Odontogram@main/public/assets/bin";
|
|
131
132
|
var OdontogramEngine = class {
|
|
132
133
|
constructor(canvas, options = {}) {
|
|
133
134
|
this.raycaster = new THREE2.Raycaster();
|
|
@@ -204,7 +205,7 @@ var OdontogramEngine = class {
|
|
|
204
205
|
}
|
|
205
206
|
resolveModelUrl(toothType) {
|
|
206
207
|
var _a;
|
|
207
|
-
const base = (_a = this.options.modelBaseUrl) != null ? _a :
|
|
208
|
+
const base = (_a = this.options.modelBaseUrl) != null ? _a : DEFAULT_MODEL_BASE_URL;
|
|
208
209
|
const fileNames = __spreadValues(__spreadValues({}, DEFAULT_MODEL_FILE_NAMES), this.options.modelFileNames);
|
|
209
210
|
const fileName = toothType === "adult" ? fileNames.adult : fileNames.child;
|
|
210
211
|
return `${base.replace(/\/$/, "")}/${fileName}`;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Odontogram3D.tsx","../src/engine.ts","../src/theme.ts","../src/toothNameMaps.ts"],"sourcesContent":["\"use client\";\n\nimport React, {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from \"react\";\nimport { OdontogramEngine } from \"./engine\";\nimport type {\n MappedPoint,\n Odontogram3DHandle,\n OdontogramTheme,\n ToothType,\n} from \"./types\";\n\nexport interface Odontogram3DProps {\n /** \"child\" (20 teeth) or \"adult\" (32 teeth). Default: \"child\". */\n toothType?: ToothType;\n /** Colors for the tooth, highlight, marker, canvas background and lighting. */\n theme?: OdontogramTheme;\n /** Base path where the `.bin.gz` model files are served from. Default: \"/assets/bin\". */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. */\n modelFileNames?: { child?: string; adult?: string };\n /** Diagnostic points to render as soon as the model finishes loading (e.g. from imported JSON). */\n initialPoints?: MappedPoint[];\n /** Fires whenever a point is added/removed/replaced. */\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Fires when the model starts/finishes loading. */\n onLoadingChange?: (loading: boolean) => void;\n minDistance?: number;\n maxDistance?: number;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Self-contained 3D odontogram canvas: owns its own Three.js scene/renderer/model instance,\n * so multiple instances can be mounted on the same page without colliding.\n */\nexport const Odontogram3D = forwardRef<Odontogram3DHandle, Odontogram3DProps>(\n function Odontogram3D(\n {\n toothType = \"child\",\n theme,\n modelBaseUrl,\n modelFileNames,\n initialPoints,\n onPointsChange,\n onLoadingChange,\n minDistance,\n maxDistance,\n className,\n style,\n },\n ref\n ) {\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const engineRef = useRef<OdontogramEngine | null>(null);\n const isLoadingRef = useRef(true);\n const onPointsChangeRef = useRef(onPointsChange);\n const onLoadingChangeRef = useRef(onLoadingChange);\n onPointsChangeRef.current = onPointsChange;\n onLoadingChangeRef.current = onLoadingChange;\n\n // Mount once: create the engine instance for this component's lifetime.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const engine = new OdontogramEngine(canvas, {\n theme,\n modelBaseUrl,\n modelFileNames,\n minDistance,\n maxDistance,\n onLoadingChange: (loading) => {\n isLoadingRef.current = loading;\n onLoadingChangeRef.current?.(loading);\n },\n onPointsChange: (points) => onPointsChangeRef.current?.(points),\n });\n engineRef.current = engine;\n\n engine.loadModel(toothType).then(() => {\n if (initialPoints && initialPoints.length > 0) {\n engine.setMappedPoints(initialPoints);\n }\n });\n\n const handleResize = () => engine.resize();\n window.addEventListener(\"resize\", handleResize);\n\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n engine.dispose();\n engineRef.current = null;\n };\n // Intentionally mount/unmount only: toothType/theme changes below are handled by\n // dedicated effects instead of recreating the whole scene.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Reload the model whenever toothType changes (but not on the very first mount).\n const previousToothType = useRef(toothType);\n useEffect(() => {\n if (previousToothType.current === toothType) return;\n previousToothType.current = toothType;\n engineRef.current?.loadModel(toothType);\n }, [toothType]);\n\n // Re-apply theme changes live, without reloading the model.\n useEffect(() => {\n if (!theme) return;\n engineRef.current?.applyTheme(theme);\n }, [theme]);\n\n const handlePointerDown = useCallback((event: React.MouseEvent<HTMLCanvasElement>) => {\n if (isLoadingRef.current) return;\n engineRef.current?.handlePointerDown(event.clientX, event.clientY);\n }, []);\n\n useImperativeHandle(\n ref,\n () => ({\n resetMapping: () => engineRef.current?.resetMapping(),\n getMappedPoints: () => engineRef.current?.getMappedPoints() ?? [],\n setMappedPoints: (points: MappedPoint[]) => engineRef.current?.setMappedPoints(points),\n updatePoint: (id: number, patch: Partial<MappedPoint>) => engineRef.current?.updatePoint(id, patch),\n applyTheme: (nextTheme: OdontogramTheme) => engineRef.current?.applyTheme(nextTheme),\n }),\n []\n );\n\n return (\n <canvas\n ref={canvasRef}\n className={className}\n style={{ width: \"100%\", height: \"100%\", ...style }}\n onMouseDown={handlePointerDown}\n />\n );\n }\n);\n","import * as THREE from \"three\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\nimport { GLTFLoader } from \"three/examples/jsm/loaders/GLTFLoader.js\";\nimport { DEFAULT_THEME, mergeTheme, toHexColor } from \"./theme\";\nimport { mapForToothType } from \"./toothNameMaps\";\nimport type { MappedPoint, OdontogramTheme, ToothType } from \"./types\";\n\nexport interface OdontogramEngineOptions {\n theme?: OdontogramTheme;\n /** Base path where `<toothType>` model files are served from. Default: \"/assets/bin\". */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. Defaults to \"ip1.bin.gz\" (child) / \"ip3.bin.gz\" (adult). */\n modelFileNames?: { child?: string; adult?: string };\n onLoadingChange?: (loading: boolean) => void;\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Camera orbit distance limits. */\n minDistance?: number;\n maxDistance?: number;\n}\n\nconst DEFAULT_MODEL_FILE_NAMES = { child: \"ip1.bin.gz\", adult: \"ip3.bin.gz\" };\n\n/**\n * Encapsulates one Three.js scene/renderer/model instance. Every instance owns its own\n * state (no module-level globals), so multiple <Odontogram3D /> can be mounted at once.\n */\nexport class OdontogramEngine {\n private canvas: HTMLCanvasElement;\n private scene: THREE.Scene;\n private camera: THREE.PerspectiveCamera;\n private renderer: THREE.WebGLRenderer;\n private controls: OrbitControls;\n private raycaster = new THREE.Raycaster();\n private pointer = new THREE.Vector2();\n private toothMeshes: THREE.Object3D[] = [];\n private animationFrameId: number | null = null;\n private theme: Required<OdontogramTheme>;\n private toothMaterialTemplate: THREE.MeshPhongMaterial;\n private mappedPoints: MappedPoint[] = [];\n private options: OdontogramEngineOptions;\n private disposed = false;\n\n constructor(canvas: HTMLCanvasElement, options: OdontogramEngineOptions = {}) {\n this.canvas = canvas;\n this.options = options;\n this.theme = mergeTheme(options.theme);\n\n this.scene = new THREE.Scene();\n\n this.camera = new THREE.PerspectiveCamera(\n 50,\n canvas.clientWidth / canvas.clientHeight || 1,\n 0.1,\n 200\n );\n this.camera.position.set(0, 2.5, 100);\n\n this.renderer = new THREE.WebGLRenderer({ antialias: true, canvas, alpha: true });\n this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.applyCanvasBackground();\n\n this.controls = new OrbitControls(this.camera, this.renderer.domElement);\n this.controls.enableDamping = true;\n this.controls.dampingFactor = 0.05;\n this.controls.screenSpacePanning = false;\n this.controls.minDistance = options.minDistance ?? 100;\n this.controls.maxDistance = options.maxDistance ?? 130;\n this.controls.target.set(4, 8, 0);\n\n this.toothMaterialTemplate = new THREE.MeshPhongMaterial({\n color: toHexColor(this.theme.toothColor),\n specular: 0x555555,\n shininess: 30,\n flatShading: false,\n side: THREE.DoubleSide,\n });\n\n this.setupLights();\n\n const animate = () => {\n if (this.disposed) return;\n this.animationFrameId = requestAnimationFrame(animate);\n this.controls.update();\n this.renderer.render(this.scene, this.camera);\n };\n animate();\n }\n\n private lights: {\n ambient: THREE.AmbientLight;\n key: THREE.DirectionalLight;\n fill: THREE.DirectionalLight;\n } | null = null;\n\n private setupLights() {\n const ambient = new THREE.AmbientLight(\n toHexColor(this.theme.ambientLightColor),\n this.theme.ambientLightIntensity\n );\n this.scene.add(ambient);\n\n const key = new THREE.DirectionalLight(\n toHexColor(this.theme.keyLightColor),\n this.theme.keyLightIntensity\n );\n key.position.set(10, 10, 10);\n this.scene.add(key);\n\n const fill = new THREE.DirectionalLight(\n toHexColor(this.theme.fillLightColor),\n this.theme.fillLightIntensity\n );\n fill.position.set(-10, -10, -5);\n this.scene.add(fill);\n\n this.lights = { ambient, key, fill };\n }\n\n private applyCanvasBackground() {\n this.renderer.setClearColor(\n toHexColor(this.theme.canvasBackground),\n this.theme.canvasBackgroundOpacity\n );\n }\n\n private resolveModelUrl(toothType: ToothType) {\n const base = this.options.modelBaseUrl ?? \"/assets/bin\";\n const fileNames = { ...DEFAULT_MODEL_FILE_NAMES, ...this.options.modelFileNames };\n const fileName = toothType === \"adult\" ? fileNames.adult : fileNames.child;\n return `${base.replace(/\\/$/, \"\")}/${fileName}`;\n }\n\n /** Re-apply a (partial) theme to lights, background and every tooth currently in its default state. */\n applyTheme(theme: OdontogramTheme) {\n this.theme = mergeTheme({ ...this.theme, ...theme });\n this.applyCanvasBackground();\n\n if (this.lights) {\n this.lights.ambient.color.setHex(toHexColor(this.theme.ambientLightColor));\n this.lights.ambient.intensity = this.theme.ambientLightIntensity;\n this.lights.key.color.setHex(toHexColor(this.theme.keyLightColor));\n this.lights.key.intensity = this.theme.keyLightIntensity;\n this.lights.fill.color.setHex(toHexColor(this.theme.fillLightColor));\n this.lights.fill.intensity = this.theme.fillLightIntensity;\n }\n\n this.toothMaterialTemplate.color.setHex(toHexColor(this.theme.toothColor));\n\n const mappedToothIds = new Set(this.mappedPoints.map((p) => p.toothId));\n this.forEachToothGroup((group) => {\n const isMapped = Boolean(group.userData.isMapped);\n const color = isMapped ? this.theme.highlightColor : this.theme.toothColor;\n this.setGroupColor(group, toHexColor(color));\n });\n\n // Markers use their own small sphere material, update those too.\n this.scene.children\n .filter((child) => child.name.startsWith(\"Marker-\"))\n .forEach((marker) => {\n if (marker instanceof THREE.Mesh && marker.material instanceof THREE.MeshBasicMaterial) {\n marker.material.color.setHex(toHexColor(this.theme.markerColor));\n }\n });\n\n void mappedToothIds; // kept for clarity/future use, no-op reference\n }\n\n private forEachToothGroup(callback: (group: THREE.Object3D) => void) {\n const seen = new Set<THREE.Object3D>();\n this.toothMeshes.forEach((mesh) => {\n const group = mesh.userData.parentGroup as THREE.Object3D | undefined;\n if (group && !seen.has(group)) {\n seen.add(group);\n callback(group);\n }\n });\n }\n\n private setGroupColor(group: THREE.Object3D, hex: number) {\n group.traverse((child) => {\n if (\n child instanceof THREE.Mesh &&\n child.userData.parentGroup === group &&\n child.material instanceof THREE.MeshPhongMaterial\n ) {\n child.material.color.setHex(hex);\n }\n });\n }\n\n async loadModel(toothType: ToothType) {\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) {\n this.scene.remove(oldArchGroup);\n oldArchGroup.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material.dispose();\n }\n });\n }\n\n this.toothMeshes = [];\n this.mappedPoints = [];\n this.options.onPointsChange?.([]);\n\n const loader = new GLTFLoader();\n const modelUrl = this.resolveModelUrl(toothType);\n this.options.onLoadingChange?.(true);\n\n try {\n const gltf = await loader.loadAsync(modelUrl);\n if (this.disposed) return;\n\n const loadedModel = gltf.scene;\n loadedModel.scale.set(1.0, 1.0, 1.0);\n loadedModel.position.set(0, -2, 0);\n loadedModel.rotation.set(0, 0, 0);\n loadedModel.name = \"ArchGroup\";\n\n const nameMap = mapForToothType(toothType);\n\n loadedModel.traverse((child) => {\n if (!(child instanceof THREE.Mesh)) return;\n\n child.geometry.computeVertexNormals();\n if (child.geometry.attributes.color) {\n child.geometry.deleteAttribute(\"color\");\n }\n\n let mappedName = nameMap[child.name];\n let targetObject: THREE.Object3D = child;\n\n if (!mappedName && child.parent && child.parent.type !== \"Scene\") {\n if (nameMap[child.parent.name]) {\n mappedName = nameMap[child.parent.name];\n targetObject = child.parent;\n }\n }\n\n child.material = this.toothMaterialTemplate.clone();\n\n if (!targetObject.userData.isTooth) {\n targetObject.userData = {\n toothId: mappedName ?? child.name,\n isTooth: true,\n isMapped: false,\n markerIds: [] as number[],\n };\n }\n\n child.userData.parentGroup = targetObject;\n this.toothMeshes.push(child);\n });\n\n this.scene.add(loadedModel);\n } catch (error) {\n console.error(`[odontogram-3d] Failed to load model at ${modelUrl}:`, error);\n } finally {\n this.options.onLoadingChange?.(false);\n }\n }\n\n private removeMarkers(markerIds: number[]) {\n markerIds.forEach((id) => {\n const marker = this.scene.getObjectByName(`Marker-${id}`);\n if (marker instanceof THREE.Mesh) {\n this.scene.remove(marker);\n marker.geometry.dispose();\n marker.material.dispose();\n }\n });\n }\n\n private addMarker(id: number, position: THREE.Vector3) {\n const geometry = new THREE.SphereGeometry(0.05, 8, 8);\n const material = new THREE.MeshBasicMaterial({ color: toHexColor(this.theme.markerColor) });\n const marker = new THREE.Mesh(geometry, material);\n marker.position.copy(position);\n marker.name = `Marker-${id}`;\n this.scene.add(marker);\n }\n\n private emitPointsChange() {\n this.options.onPointsChange?.([...this.mappedPoints]);\n }\n\n /** Handle a click/tap on the canvas (expects coordinates relative to the viewport, like a MouseEvent). */\n handlePointerDown(clientX: number, clientY: number) {\n const rect = this.canvas.getBoundingClientRect();\n this.pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;\n this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;\n\n this.raycaster.setFromCamera(this.pointer, this.camera);\n const intersects = this.raycaster.intersectObjects(this.toothMeshes, false);\n if (intersects.length === 0) return;\n\n const clickedMesh = intersects[0].object;\n const toothGroup = clickedMesh.userData.parentGroup as THREE.Object3D | undefined;\n if (!toothGroup || !toothGroup.userData.isTooth) return;\n\n const toothId: string = toothGroup.userData.toothId;\n const point = intersects[0].point;\n const isCurrentlyMapped = Boolean(toothGroup.userData.isMapped);\n\n if (isCurrentlyMapped) {\n this.removeMarkers(toothGroup.userData.markerIds ?? []);\n this.setGroupColor(toothGroup, toHexColor(this.theme.toothColor));\n toothGroup.userData.isMapped = false;\n toothGroup.userData.markerIds = [];\n this.mappedPoints = this.mappedPoints.filter((p) => p.toothId !== toothId);\n this.emitPointsChange();\n } else {\n const newMarkerId = Date.now();\n this.setGroupColor(toothGroup, toHexColor(this.theme.highlightColor));\n toothGroup.userData.isMapped = true;\n toothGroup.userData.markerIds = [newMarkerId];\n this.addMarker(newMarkerId, point);\n\n const newPoint: MappedPoint = {\n id: newMarkerId,\n toothId,\n x: point.x.toFixed(2),\n y: point.y.toFixed(2),\n z: point.z.toFixed(2),\n type: \"Karies\",\n description: \"\",\n };\n this.mappedPoints = [...this.mappedPoints, newPoint];\n this.emitPointsChange();\n }\n }\n\n getMappedPoints(): MappedPoint[] {\n return [...this.mappedPoints];\n }\n\n /** Patch metadata (e.g. description) on an existing point without touching the 3D scene. */\n updatePoint(id: number, patch: Partial<MappedPoint>) {\n this.mappedPoints = this.mappedPoints.map((p) => (p.id === id ? { ...p, ...patch } : p));\n this.emitPointsChange();\n }\n\n resetMapping() {\n const existingMarkerIds = this.scene.children\n .filter((c) => c.name.startsWith(\"Marker-\"))\n .map((c) => Number(c.name.replace(\"Marker-\", \"\")));\n this.removeMarkers(existingMarkerIds);\n\n this.forEachToothGroup((group) => {\n this.setGroupColor(group, toHexColor(this.theme.toothColor));\n group.userData.isMapped = false;\n group.userData.markerIds = [];\n });\n\n this.mappedPoints = [];\n this.emitPointsChange();\n }\n\n /** Replace all diagnostic points and redraw markers/highlights to match (e.g. after importing JSON). */\n setMappedPoints(points: MappedPoint[]) {\n this.resetMapping();\n\n points.forEach((point) => {\n const group = this.toothMeshes.find(\n (mesh) => (mesh.userData.parentGroup as THREE.Object3D | undefined)?.userData.toothId === point.toothId\n )?.userData.parentGroup as THREE.Object3D | undefined;\n\n if (!group) return;\n\n this.setGroupColor(group, toHexColor(this.theme.highlightColor));\n group.userData.isMapped = true;\n group.userData.markerIds = [...(group.userData.markerIds ?? []), point.id];\n\n this.addMarker(point.id, new THREE.Vector3(parseFloat(point.x), parseFloat(point.y), parseFloat(point.z)));\n });\n\n this.mappedPoints = [...points];\n this.emitPointsChange();\n }\n\n resize() {\n const container = this.canvas.parentElement;\n if (!container) return;\n const { clientWidth, clientHeight } = container;\n if (clientWidth === 0 || clientHeight === 0) return;\n\n this.camera.aspect = clientWidth / clientHeight;\n this.camera.updateProjectionMatrix();\n this.renderer.setSize(clientWidth, clientHeight, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n }\n\n dispose() {\n this.disposed = true;\n if (this.animationFrameId !== null) cancelAnimationFrame(this.animationFrameId);\n\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) this.scene.remove(oldArchGroup);\n\n this.scene.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material?.dispose();\n }\n });\n\n this.toothMaterialTemplate.dispose();\n this.controls.dispose();\n this.renderer.dispose();\n }\n}\n\nexport { DEFAULT_THEME };\n","import * as THREE from \"three\";\nimport type { ColorInput, OdontogramTheme } from \"./types\";\n\nexport const DEFAULT_THEME: Required<OdontogramTheme> = {\n toothColor: 0xf5f5dc, // Ivory\n highlightColor: 0x3b82f6, // Tailwind blue-500\n markerColor: 0xff0000, // Red\n canvasBackground: 0xecf0f1,\n canvasBackgroundOpacity: 0,\n ambientLightColor: 0xffffff,\n ambientLightIntensity: 0.8,\n keyLightColor: 0xffffff,\n keyLightIntensity: 0.6,\n fillLightColor: 0xffffff,\n fillLightIntensity: 0.4,\n};\n\nexport function mergeTheme(theme?: OdontogramTheme): Required<OdontogramTheme> {\n return { ...DEFAULT_THEME, ...theme };\n}\n\n/** Three.js color helpers accept numbers (0xff0000) or strings (\"#ff0000\"); normalize both to a hex number. */\nexport function toHexColor(input: ColorInput): number {\n if (typeof input === \"number\") return input;\n return new THREE.Color(input).getHex();\n}\n","export const TOOTH_MESH_NAME_MAP_CHILD: { [key: string]: string } = {\n Gums_Maxilla001_0: \"Gums Maxilla\",\n Gums_Mandibula001_0: \"Gums Mandibulla\",\n A_0: \"(Left Top) Molar\",\n B_0: \"(Left Top) Premolar\",\n C_0: \"(Left Top) Canine\",\n D_0: \"(Left Top) Lateral\",\n E_0: \"(Left Top) Central\",\n F_0: \"(Right Top) Central\",\n G_0: \"(Right Top) Lateral\",\n H_0: \"(Right Top) Canine\",\n I_0: \"(Right Top) Premolar\",\n J_0: \"(Right Top) Molar\",\n K_0: \"(Left Bottom) Molar\",\n L_0: \"(Left Bottom) Premolar\",\n M_0: \"(Left Bottom) Canine\",\n N_0: \"(Left Bottom) Lateral\",\n O_0: \"(Left Bottom) Central\",\n P_0: \"(Right Bottom) Central\",\n Q_0: \"(Right Bottom) Lateral\",\n R_0: \"(Right Bottom) Canine\",\n S_0: \"(Right Bottom) Premolar\",\n T_0: \"(Right Bottom) Molar\",\n};\n\nexport const TOOTH_MESH_NAME_MAP_ADULT: { [key: string]: string } = {\n // --- Rahang Atas Kanan (Quadrant 1) ---\n \"Box023_01_-_Default_0\": \"11 (Right Top) Central Incisor\",\n \"Box024_01_-_Default_0\": \"12 (Right Top) Lateral Incisor\",\n \"Box025_01_-_Default_0\": \"13 (Right Top) Canine\",\n \"Box031_01_-_Default_0\": \"14 (Right Top) First Premolar\",\n \"Box033_01_-_Default_0\": \"15 (Right Top) Second Premolar\",\n \"Box032_01_-_Default_0\": \"16 (Right Top) First Molar\",\n \"Box035_01_-_Default_0\": \"17 (Right Top) Second Molar\",\n \"Box034_01_-_Default_0\": \"18 (Right Top) Third Molar\",\n\n // --- Rahang Atas Kiri (Quadrant 2) ---\n \"Box020_01_-_Default_0\": \"21 (Left Top) Central Incisor\",\n \"Box021_01_-_Default_0\": \"22 (Left Top) Lateral Incisor\",\n \"Box022_01_-_Default_0\": \"23 (Left Top) Canine\",\n \"Box026_01_-_Default_0\": \"24 (Left Top) First Premolar\",\n \"Box028_01_-_Default_0\": \"25 (Left Top) Second Premolar\",\n \"Box027_01_-_Default_0\": \"26 (Left Top) First Molar\",\n \"Box030_01_-_Default_0\": \"27 (Left Top) Second Molar\",\n \"Box029_01_-_Default_0\": \"28 (Left Top) Third Molar\",\n\n // --- Rahang Bawah Kiri (Quadrant 3) ---\n \"Box011_01_-_Default_0\": \"38 (Left Bottom) Third Molar\",\n \"Box013_01_-_Default_0\": \"37 (Left Bottom) Second Molar\",\n \"Box008_01_-_Default_0\": \"36 (Left Bottom) First Molar\",\n \"Box009_01_-_Default_0\": \"35 (Left Bottom) Second Premolar\",\n \"Box007_01_-_Default_0\": \"34 (Left Bottom) First Premolar\",\n \"Box003_01_-_Default_0\": \"33 (Left Bottom) Canine\",\n \"Box002_01_-_Default_0\": \"32 (Left Bottom) Lateral Incisor\",\n \"Box001_01_-_Default_0\": \"31 (Left Bottom) Central Incisor\",\n\n // --- Rahang Bawah Kanan (Quadrant 4) ---\n \"Box004_01_-_Default_0\": \"41 (Right Bottom) Central Incisor\",\n \"Box005_01_-_Default_0\": \"42 (Right Bottom) Lateral Incisor\",\n \"Box006_01_-_Default_0\": \"43 (Right Bottom) Canine\",\n \"Box015_01_-_Default_0\": \"44 (Right Bottom) First Premolar\",\n \"Box017_01_-_Default_0\": \"45 (Right Bottom) Second Premolar\",\n \"Box016_01_-_Default_0\": \"46 (Right Bottom) First Molar\",\n \"Box019_01_-_Default_0\": \"47 (Right Bottom) Second Molar\",\n \"GeoSphere001_01_-_Default_0\": \"48 (Right Bottom) Third Molar\",\n\n // --- Lain-lain ---\n \"Tube002_07_-_Default_0\": \"(Top Tube) Gums Maxilla\",\n \"Tube001_07_-_Default_0\": \"(Bottom Tube) Gums Mandibula\",\n \"lingua_02_-_Default_0\": \"(Tongue) Lingua\",\n};\n\nexport function mapForToothType(toothType: string): { [key: string]: string } {\n return toothType === \"child\" || !toothType\n ? TOOTH_MESH_NAME_MAP_CHILD\n : TOOTH_MESH_NAME_MAP_ADULT;\n}\n"],"mappings":"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","names":["THREE","_a","Odontogram3D"]}
|
|
1
|
+
{"version":3,"sources":["../src/Odontogram3D.tsx","../src/engine.ts","../src/theme.ts","../src/toothNameMaps.ts"],"sourcesContent":["\"use client\";\n\nimport React, {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from \"react\";\nimport { OdontogramEngine } from \"./engine\";\nimport type {\n MappedPoint,\n Odontogram3DHandle,\n OdontogramTheme,\n ToothType,\n} from \"./types\";\n\nexport interface Odontogram3DProps {\n /** \"child\" (20 teeth) or \"adult\" (32 teeth). Default: \"child\". */\n toothType?: ToothType;\n /** Colors for the tooth, highlight, marker, canvas background and lighting. */\n theme?: OdontogramTheme;\n /** Base path where the `.bin.gz` model files are served from. Default: \"/assets/bin\". */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. */\n modelFileNames?: { child?: string; adult?: string };\n /** Diagnostic points to render as soon as the model finishes loading (e.g. from imported JSON). */\n initialPoints?: MappedPoint[];\n /** Fires whenever a point is added/removed/replaced. */\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Fires when the model starts/finishes loading. */\n onLoadingChange?: (loading: boolean) => void;\n minDistance?: number;\n maxDistance?: number;\n className?: string;\n style?: React.CSSProperties;\n}\n\n/**\n * Self-contained 3D odontogram canvas: owns its own Three.js scene/renderer/model instance,\n * so multiple instances can be mounted on the same page without colliding.\n */\nexport const Odontogram3D = forwardRef<Odontogram3DHandle, Odontogram3DProps>(\n function Odontogram3D(\n {\n toothType = \"child\",\n theme,\n modelBaseUrl,\n modelFileNames,\n initialPoints,\n onPointsChange,\n onLoadingChange,\n minDistance,\n maxDistance,\n className,\n style,\n },\n ref\n ) {\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const engineRef = useRef<OdontogramEngine | null>(null);\n const isLoadingRef = useRef(true);\n const onPointsChangeRef = useRef(onPointsChange);\n const onLoadingChangeRef = useRef(onLoadingChange);\n onPointsChangeRef.current = onPointsChange;\n onLoadingChangeRef.current = onLoadingChange;\n\n // Mount once: create the engine instance for this component's lifetime.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const engine = new OdontogramEngine(canvas, {\n theme,\n modelBaseUrl,\n modelFileNames,\n minDistance,\n maxDistance,\n onLoadingChange: (loading) => {\n isLoadingRef.current = loading;\n onLoadingChangeRef.current?.(loading);\n },\n onPointsChange: (points) => onPointsChangeRef.current?.(points),\n });\n engineRef.current = engine;\n\n engine.loadModel(toothType).then(() => {\n if (initialPoints && initialPoints.length > 0) {\n engine.setMappedPoints(initialPoints);\n }\n });\n\n const handleResize = () => engine.resize();\n window.addEventListener(\"resize\", handleResize);\n\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n engine.dispose();\n engineRef.current = null;\n };\n // Intentionally mount/unmount only: toothType/theme changes below are handled by\n // dedicated effects instead of recreating the whole scene.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Reload the model whenever toothType changes (but not on the very first mount).\n const previousToothType = useRef(toothType);\n useEffect(() => {\n if (previousToothType.current === toothType) return;\n previousToothType.current = toothType;\n engineRef.current?.loadModel(toothType);\n }, [toothType]);\n\n // Re-apply theme changes live, without reloading the model.\n useEffect(() => {\n if (!theme) return;\n engineRef.current?.applyTheme(theme);\n }, [theme]);\n\n const handlePointerDown = useCallback((event: React.MouseEvent<HTMLCanvasElement>) => {\n if (isLoadingRef.current) return;\n engineRef.current?.handlePointerDown(event.clientX, event.clientY);\n }, []);\n\n useImperativeHandle(\n ref,\n () => ({\n resetMapping: () => engineRef.current?.resetMapping(),\n getMappedPoints: () => engineRef.current?.getMappedPoints() ?? [],\n setMappedPoints: (points: MappedPoint[]) => engineRef.current?.setMappedPoints(points),\n updatePoint: (id: number, patch: Partial<MappedPoint>) => engineRef.current?.updatePoint(id, patch),\n applyTheme: (nextTheme: OdontogramTheme) => engineRef.current?.applyTheme(nextTheme),\n }),\n []\n );\n\n return (\n <canvas\n ref={canvasRef}\n className={className}\n style={{ width: \"100%\", height: \"100%\", ...style }}\n onMouseDown={handlePointerDown}\n />\n );\n }\n);\n","import * as THREE from \"three\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\nimport { GLTFLoader } from \"three/examples/jsm/loaders/GLTFLoader.js\";\nimport { DEFAULT_THEME, mergeTheme, toHexColor } from \"./theme\";\nimport { mapForToothType } from \"./toothNameMaps\";\nimport type { MappedPoint, OdontogramTheme, ToothType } from \"./types\";\n\nexport interface OdontogramEngineOptions {\n theme?: OdontogramTheme;\n /**\n * Base path where `<toothType>` model files are served from.\n * Default: a public CDN (jsDelivr, serving the files straight from this package's GitHub repo)\n * so the component works with zero setup. Pass your own path/URL to self-host the models instead.\n */\n modelBaseUrl?: string;\n /** Override the model file name per tooth type. Defaults to \"ip1.bin.gz\" (child) / \"ip3.bin.gz\" (adult). */\n modelFileNames?: { child?: string; adult?: string };\n onLoadingChange?: (loading: boolean) => void;\n onPointsChange?: (points: MappedPoint[]) => void;\n /** Camera orbit distance limits. */\n minDistance?: number;\n maxDistance?: number;\n}\n\nconst DEFAULT_MODEL_FILE_NAMES = { child: \"ip1.bin.gz\", adult: \"ip3.bin.gz\" };\n\n/** Served straight from the GitHub repo via jsDelivr — no separate asset hosting needed. */\nconst DEFAULT_MODEL_BASE_URL =\n \"https://cdn.jsdelivr.net/gh/jivimuz/Jivident_3D_Odontogram@main/public/assets/bin\";\n\n/**\n * Encapsulates one Three.js scene/renderer/model instance. Every instance owns its own\n * state (no module-level globals), so multiple <Odontogram3D /> can be mounted at once.\n */\nexport class OdontogramEngine {\n private canvas: HTMLCanvasElement;\n private scene: THREE.Scene;\n private camera: THREE.PerspectiveCamera;\n private renderer: THREE.WebGLRenderer;\n private controls: OrbitControls;\n private raycaster = new THREE.Raycaster();\n private pointer = new THREE.Vector2();\n private toothMeshes: THREE.Object3D[] = [];\n private animationFrameId: number | null = null;\n private theme: Required<OdontogramTheme>;\n private toothMaterialTemplate: THREE.MeshPhongMaterial;\n private mappedPoints: MappedPoint[] = [];\n private options: OdontogramEngineOptions;\n private disposed = false;\n\n constructor(canvas: HTMLCanvasElement, options: OdontogramEngineOptions = {}) {\n this.canvas = canvas;\n this.options = options;\n this.theme = mergeTheme(options.theme);\n\n this.scene = new THREE.Scene();\n\n this.camera = new THREE.PerspectiveCamera(\n 50,\n canvas.clientWidth / canvas.clientHeight || 1,\n 0.1,\n 200\n );\n this.camera.position.set(0, 2.5, 100);\n\n this.renderer = new THREE.WebGLRenderer({ antialias: true, canvas, alpha: true });\n this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.applyCanvasBackground();\n\n this.controls = new OrbitControls(this.camera, this.renderer.domElement);\n this.controls.enableDamping = true;\n this.controls.dampingFactor = 0.05;\n this.controls.screenSpacePanning = false;\n this.controls.minDistance = options.minDistance ?? 100;\n this.controls.maxDistance = options.maxDistance ?? 130;\n this.controls.target.set(4, 8, 0);\n\n this.toothMaterialTemplate = new THREE.MeshPhongMaterial({\n color: toHexColor(this.theme.toothColor),\n specular: 0x555555,\n shininess: 30,\n flatShading: false,\n side: THREE.DoubleSide,\n });\n\n this.setupLights();\n\n const animate = () => {\n if (this.disposed) return;\n this.animationFrameId = requestAnimationFrame(animate);\n this.controls.update();\n this.renderer.render(this.scene, this.camera);\n };\n animate();\n }\n\n private lights: {\n ambient: THREE.AmbientLight;\n key: THREE.DirectionalLight;\n fill: THREE.DirectionalLight;\n } | null = null;\n\n private setupLights() {\n const ambient = new THREE.AmbientLight(\n toHexColor(this.theme.ambientLightColor),\n this.theme.ambientLightIntensity\n );\n this.scene.add(ambient);\n\n const key = new THREE.DirectionalLight(\n toHexColor(this.theme.keyLightColor),\n this.theme.keyLightIntensity\n );\n key.position.set(10, 10, 10);\n this.scene.add(key);\n\n const fill = new THREE.DirectionalLight(\n toHexColor(this.theme.fillLightColor),\n this.theme.fillLightIntensity\n );\n fill.position.set(-10, -10, -5);\n this.scene.add(fill);\n\n this.lights = { ambient, key, fill };\n }\n\n private applyCanvasBackground() {\n this.renderer.setClearColor(\n toHexColor(this.theme.canvasBackground),\n this.theme.canvasBackgroundOpacity\n );\n }\n\n private resolveModelUrl(toothType: ToothType) {\n const base = this.options.modelBaseUrl ?? DEFAULT_MODEL_BASE_URL;\n const fileNames = { ...DEFAULT_MODEL_FILE_NAMES, ...this.options.modelFileNames };\n const fileName = toothType === \"adult\" ? fileNames.adult : fileNames.child;\n return `${base.replace(/\\/$/, \"\")}/${fileName}`;\n }\n\n /** Re-apply a (partial) theme to lights, background and every tooth currently in its default state. */\n applyTheme(theme: OdontogramTheme) {\n this.theme = mergeTheme({ ...this.theme, ...theme });\n this.applyCanvasBackground();\n\n if (this.lights) {\n this.lights.ambient.color.setHex(toHexColor(this.theme.ambientLightColor));\n this.lights.ambient.intensity = this.theme.ambientLightIntensity;\n this.lights.key.color.setHex(toHexColor(this.theme.keyLightColor));\n this.lights.key.intensity = this.theme.keyLightIntensity;\n this.lights.fill.color.setHex(toHexColor(this.theme.fillLightColor));\n this.lights.fill.intensity = this.theme.fillLightIntensity;\n }\n\n this.toothMaterialTemplate.color.setHex(toHexColor(this.theme.toothColor));\n\n const mappedToothIds = new Set(this.mappedPoints.map((p) => p.toothId));\n this.forEachToothGroup((group) => {\n const isMapped = Boolean(group.userData.isMapped);\n const color = isMapped ? this.theme.highlightColor : this.theme.toothColor;\n this.setGroupColor(group, toHexColor(color));\n });\n\n // Markers use their own small sphere material, update those too.\n this.scene.children\n .filter((child) => child.name.startsWith(\"Marker-\"))\n .forEach((marker) => {\n if (marker instanceof THREE.Mesh && marker.material instanceof THREE.MeshBasicMaterial) {\n marker.material.color.setHex(toHexColor(this.theme.markerColor));\n }\n });\n\n void mappedToothIds; // kept for clarity/future use, no-op reference\n }\n\n private forEachToothGroup(callback: (group: THREE.Object3D) => void) {\n const seen = new Set<THREE.Object3D>();\n this.toothMeshes.forEach((mesh) => {\n const group = mesh.userData.parentGroup as THREE.Object3D | undefined;\n if (group && !seen.has(group)) {\n seen.add(group);\n callback(group);\n }\n });\n }\n\n private setGroupColor(group: THREE.Object3D, hex: number) {\n group.traverse((child) => {\n if (\n child instanceof THREE.Mesh &&\n child.userData.parentGroup === group &&\n child.material instanceof THREE.MeshPhongMaterial\n ) {\n child.material.color.setHex(hex);\n }\n });\n }\n\n async loadModel(toothType: ToothType) {\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) {\n this.scene.remove(oldArchGroup);\n oldArchGroup.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material.dispose();\n }\n });\n }\n\n this.toothMeshes = [];\n this.mappedPoints = [];\n this.options.onPointsChange?.([]);\n\n const loader = new GLTFLoader();\n const modelUrl = this.resolveModelUrl(toothType);\n this.options.onLoadingChange?.(true);\n\n try {\n const gltf = await loader.loadAsync(modelUrl);\n if (this.disposed) return;\n\n const loadedModel = gltf.scene;\n loadedModel.scale.set(1.0, 1.0, 1.0);\n loadedModel.position.set(0, -2, 0);\n loadedModel.rotation.set(0, 0, 0);\n loadedModel.name = \"ArchGroup\";\n\n const nameMap = mapForToothType(toothType);\n\n loadedModel.traverse((child) => {\n if (!(child instanceof THREE.Mesh)) return;\n\n child.geometry.computeVertexNormals();\n if (child.geometry.attributes.color) {\n child.geometry.deleteAttribute(\"color\");\n }\n\n let mappedName = nameMap[child.name];\n let targetObject: THREE.Object3D = child;\n\n if (!mappedName && child.parent && child.parent.type !== \"Scene\") {\n if (nameMap[child.parent.name]) {\n mappedName = nameMap[child.parent.name];\n targetObject = child.parent;\n }\n }\n\n child.material = this.toothMaterialTemplate.clone();\n\n if (!targetObject.userData.isTooth) {\n targetObject.userData = {\n toothId: mappedName ?? child.name,\n isTooth: true,\n isMapped: false,\n markerIds: [] as number[],\n };\n }\n\n child.userData.parentGroup = targetObject;\n this.toothMeshes.push(child);\n });\n\n this.scene.add(loadedModel);\n } catch (error) {\n console.error(`[odontogram-3d] Failed to load model at ${modelUrl}:`, error);\n } finally {\n this.options.onLoadingChange?.(false);\n }\n }\n\n private removeMarkers(markerIds: number[]) {\n markerIds.forEach((id) => {\n const marker = this.scene.getObjectByName(`Marker-${id}`);\n if (marker instanceof THREE.Mesh) {\n this.scene.remove(marker);\n marker.geometry.dispose();\n marker.material.dispose();\n }\n });\n }\n\n private addMarker(id: number, position: THREE.Vector3) {\n const geometry = new THREE.SphereGeometry(0.05, 8, 8);\n const material = new THREE.MeshBasicMaterial({ color: toHexColor(this.theme.markerColor) });\n const marker = new THREE.Mesh(geometry, material);\n marker.position.copy(position);\n marker.name = `Marker-${id}`;\n this.scene.add(marker);\n }\n\n private emitPointsChange() {\n this.options.onPointsChange?.([...this.mappedPoints]);\n }\n\n /** Handle a click/tap on the canvas (expects coordinates relative to the viewport, like a MouseEvent). */\n handlePointerDown(clientX: number, clientY: number) {\n const rect = this.canvas.getBoundingClientRect();\n this.pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;\n this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;\n\n this.raycaster.setFromCamera(this.pointer, this.camera);\n const intersects = this.raycaster.intersectObjects(this.toothMeshes, false);\n if (intersects.length === 0) return;\n\n const clickedMesh = intersects[0].object;\n const toothGroup = clickedMesh.userData.parentGroup as THREE.Object3D | undefined;\n if (!toothGroup || !toothGroup.userData.isTooth) return;\n\n const toothId: string = toothGroup.userData.toothId;\n const point = intersects[0].point;\n const isCurrentlyMapped = Boolean(toothGroup.userData.isMapped);\n\n if (isCurrentlyMapped) {\n this.removeMarkers(toothGroup.userData.markerIds ?? []);\n this.setGroupColor(toothGroup, toHexColor(this.theme.toothColor));\n toothGroup.userData.isMapped = false;\n toothGroup.userData.markerIds = [];\n this.mappedPoints = this.mappedPoints.filter((p) => p.toothId !== toothId);\n this.emitPointsChange();\n } else {\n const newMarkerId = Date.now();\n this.setGroupColor(toothGroup, toHexColor(this.theme.highlightColor));\n toothGroup.userData.isMapped = true;\n toothGroup.userData.markerIds = [newMarkerId];\n this.addMarker(newMarkerId, point);\n\n const newPoint: MappedPoint = {\n id: newMarkerId,\n toothId,\n x: point.x.toFixed(2),\n y: point.y.toFixed(2),\n z: point.z.toFixed(2),\n type: \"Karies\",\n description: \"\",\n };\n this.mappedPoints = [...this.mappedPoints, newPoint];\n this.emitPointsChange();\n }\n }\n\n getMappedPoints(): MappedPoint[] {\n return [...this.mappedPoints];\n }\n\n /** Patch metadata (e.g. description) on an existing point without touching the 3D scene. */\n updatePoint(id: number, patch: Partial<MappedPoint>) {\n this.mappedPoints = this.mappedPoints.map((p) => (p.id === id ? { ...p, ...patch } : p));\n this.emitPointsChange();\n }\n\n resetMapping() {\n const existingMarkerIds = this.scene.children\n .filter((c) => c.name.startsWith(\"Marker-\"))\n .map((c) => Number(c.name.replace(\"Marker-\", \"\")));\n this.removeMarkers(existingMarkerIds);\n\n this.forEachToothGroup((group) => {\n this.setGroupColor(group, toHexColor(this.theme.toothColor));\n group.userData.isMapped = false;\n group.userData.markerIds = [];\n });\n\n this.mappedPoints = [];\n this.emitPointsChange();\n }\n\n /** Replace all diagnostic points and redraw markers/highlights to match (e.g. after importing JSON). */\n setMappedPoints(points: MappedPoint[]) {\n this.resetMapping();\n\n points.forEach((point) => {\n const group = this.toothMeshes.find(\n (mesh) => (mesh.userData.parentGroup as THREE.Object3D | undefined)?.userData.toothId === point.toothId\n )?.userData.parentGroup as THREE.Object3D | undefined;\n\n if (!group) return;\n\n this.setGroupColor(group, toHexColor(this.theme.highlightColor));\n group.userData.isMapped = true;\n group.userData.markerIds = [...(group.userData.markerIds ?? []), point.id];\n\n this.addMarker(point.id, new THREE.Vector3(parseFloat(point.x), parseFloat(point.y), parseFloat(point.z)));\n });\n\n this.mappedPoints = [...points];\n this.emitPointsChange();\n }\n\n resize() {\n const container = this.canvas.parentElement;\n if (!container) return;\n const { clientWidth, clientHeight } = container;\n if (clientWidth === 0 || clientHeight === 0) return;\n\n this.camera.aspect = clientWidth / clientHeight;\n this.camera.updateProjectionMatrix();\n this.renderer.setSize(clientWidth, clientHeight, false);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n }\n\n dispose() {\n this.disposed = true;\n if (this.animationFrameId !== null) cancelAnimationFrame(this.animationFrameId);\n\n const oldArchGroup = this.scene.getObjectByName(\"ArchGroup\");\n if (oldArchGroup) this.scene.remove(oldArchGroup);\n\n this.scene.traverse((child) => {\n if (child instanceof THREE.Mesh) {\n child.geometry.dispose();\n const material = child.material;\n if (Array.isArray(material)) material.forEach((m) => m.dispose());\n else material?.dispose();\n }\n });\n\n this.toothMaterialTemplate.dispose();\n this.controls.dispose();\n this.renderer.dispose();\n }\n}\n\nexport { DEFAULT_THEME };\n","import * as THREE from \"three\";\nimport type { ColorInput, OdontogramTheme } from \"./types\";\n\nexport const DEFAULT_THEME: Required<OdontogramTheme> = {\n toothColor: 0xf5f5dc, // Ivory\n highlightColor: 0x3b82f6, // Tailwind blue-500\n markerColor: 0xff0000, // Red\n canvasBackground: 0xecf0f1,\n canvasBackgroundOpacity: 0,\n ambientLightColor: 0xffffff,\n ambientLightIntensity: 0.8,\n keyLightColor: 0xffffff,\n keyLightIntensity: 0.6,\n fillLightColor: 0xffffff,\n fillLightIntensity: 0.4,\n};\n\nexport function mergeTheme(theme?: OdontogramTheme): Required<OdontogramTheme> {\n return { ...DEFAULT_THEME, ...theme };\n}\n\n/** Three.js color helpers accept numbers (0xff0000) or strings (\"#ff0000\"); normalize both to a hex number. */\nexport function toHexColor(input: ColorInput): number {\n if (typeof input === \"number\") return input;\n return new THREE.Color(input).getHex();\n}\n","export const TOOTH_MESH_NAME_MAP_CHILD: { [key: string]: string } = {\n Gums_Maxilla001_0: \"Gums Maxilla\",\n Gums_Mandibula001_0: \"Gums Mandibulla\",\n A_0: \"(Left Top) Molar\",\n B_0: \"(Left Top) Premolar\",\n C_0: \"(Left Top) Canine\",\n D_0: \"(Left Top) Lateral\",\n E_0: \"(Left Top) Central\",\n F_0: \"(Right Top) Central\",\n G_0: \"(Right Top) Lateral\",\n H_0: \"(Right Top) Canine\",\n I_0: \"(Right Top) Premolar\",\n J_0: \"(Right Top) Molar\",\n K_0: \"(Left Bottom) Molar\",\n L_0: \"(Left Bottom) Premolar\",\n M_0: \"(Left Bottom) Canine\",\n N_0: \"(Left Bottom) Lateral\",\n O_0: \"(Left Bottom) Central\",\n P_0: \"(Right Bottom) Central\",\n Q_0: \"(Right Bottom) Lateral\",\n R_0: \"(Right Bottom) Canine\",\n S_0: \"(Right Bottom) Premolar\",\n T_0: \"(Right Bottom) Molar\",\n};\n\nexport const TOOTH_MESH_NAME_MAP_ADULT: { [key: string]: string } = {\n // --- Rahang Atas Kanan (Quadrant 1) ---\n \"Box023_01_-_Default_0\": \"11 (Right Top) Central Incisor\",\n \"Box024_01_-_Default_0\": \"12 (Right Top) Lateral Incisor\",\n \"Box025_01_-_Default_0\": \"13 (Right Top) Canine\",\n \"Box031_01_-_Default_0\": \"14 (Right Top) First Premolar\",\n \"Box033_01_-_Default_0\": \"15 (Right Top) Second Premolar\",\n \"Box032_01_-_Default_0\": \"16 (Right Top) First Molar\",\n \"Box035_01_-_Default_0\": \"17 (Right Top) Second Molar\",\n \"Box034_01_-_Default_0\": \"18 (Right Top) Third Molar\",\n\n // --- Rahang Atas Kiri (Quadrant 2) ---\n \"Box020_01_-_Default_0\": \"21 (Left Top) Central Incisor\",\n \"Box021_01_-_Default_0\": \"22 (Left Top) Lateral Incisor\",\n \"Box022_01_-_Default_0\": \"23 (Left Top) Canine\",\n \"Box026_01_-_Default_0\": \"24 (Left Top) First Premolar\",\n \"Box028_01_-_Default_0\": \"25 (Left Top) Second Premolar\",\n \"Box027_01_-_Default_0\": \"26 (Left Top) First Molar\",\n \"Box030_01_-_Default_0\": \"27 (Left Top) Second Molar\",\n \"Box029_01_-_Default_0\": \"28 (Left Top) Third Molar\",\n\n // --- Rahang Bawah Kiri (Quadrant 3) ---\n \"Box011_01_-_Default_0\": \"38 (Left Bottom) Third Molar\",\n \"Box013_01_-_Default_0\": \"37 (Left Bottom) Second Molar\",\n \"Box008_01_-_Default_0\": \"36 (Left Bottom) First Molar\",\n \"Box009_01_-_Default_0\": \"35 (Left Bottom) Second Premolar\",\n \"Box007_01_-_Default_0\": \"34 (Left Bottom) First Premolar\",\n \"Box003_01_-_Default_0\": \"33 (Left Bottom) Canine\",\n \"Box002_01_-_Default_0\": \"32 (Left Bottom) Lateral Incisor\",\n \"Box001_01_-_Default_0\": \"31 (Left Bottom) Central Incisor\",\n\n // --- Rahang Bawah Kanan (Quadrant 4) ---\n \"Box004_01_-_Default_0\": \"41 (Right Bottom) Central Incisor\",\n \"Box005_01_-_Default_0\": \"42 (Right Bottom) Lateral Incisor\",\n \"Box006_01_-_Default_0\": \"43 (Right Bottom) Canine\",\n \"Box015_01_-_Default_0\": \"44 (Right Bottom) First Premolar\",\n \"Box017_01_-_Default_0\": \"45 (Right Bottom) Second Premolar\",\n \"Box016_01_-_Default_0\": \"46 (Right Bottom) First Molar\",\n \"Box019_01_-_Default_0\": \"47 (Right Bottom) Second Molar\",\n \"GeoSphere001_01_-_Default_0\": \"48 (Right Bottom) Third Molar\",\n\n // --- Lain-lain ---\n \"Tube002_07_-_Default_0\": \"(Top Tube) Gums Maxilla\",\n \"Tube001_07_-_Default_0\": \"(Bottom Tube) Gums Mandibula\",\n \"lingua_02_-_Default_0\": \"(Tongue) Lingua\",\n};\n\nexport function mapForToothType(toothType: string): { [key: string]: string } {\n return toothType === \"child\" || !toothType\n ? TOOTH_MESH_NAME_MAP_CHILD\n : TOOTH_MESH_NAME_MAP_ADULT;\n}\n"],"mappings":"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","names":["THREE","_a","Odontogram3D"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "3d-odontogram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Customizable React + Three.js 3D odontogram (dental chart) canvas: theme the tooth, highlight and marker colors, and the canvas background.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jivi <jivirasgal@gmail.com>",
|