3d-odontogram 0.0.0-stage → 0.1.1
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 +122 -2
- package/dist/index.cjs +601 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +567 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
+
var __spreadValues = (a, b) => {
|
|
12
|
+
for (var prop in b || (b = {}))
|
|
13
|
+
if (__hasOwnProp.call(b, prop))
|
|
14
|
+
__defNormalProp(a, prop, b[prop]);
|
|
15
|
+
if (__getOwnPropSymbols)
|
|
16
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
+
if (__propIsEnum.call(b, prop))
|
|
18
|
+
__defNormalProp(a, prop, b[prop]);
|
|
19
|
+
}
|
|
20
|
+
return a;
|
|
21
|
+
};
|
|
22
|
+
var __export = (target, all) => {
|
|
23
|
+
for (var name in all)
|
|
24
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
|
+
};
|
|
26
|
+
var __copyProps = (to, from, except, desc) => {
|
|
27
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
28
|
+
for (let key of __getOwnPropNames(from))
|
|
29
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
30
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
31
|
+
}
|
|
32
|
+
return to;
|
|
33
|
+
};
|
|
34
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
35
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
36
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
37
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
38
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
39
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
40
|
+
mod
|
|
41
|
+
));
|
|
42
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
43
|
+
|
|
44
|
+
// src/index.ts
|
|
45
|
+
var index_exports = {};
|
|
46
|
+
__export(index_exports, {
|
|
47
|
+
DEFAULT_THEME: () => DEFAULT_THEME,
|
|
48
|
+
Odontogram3D: () => Odontogram3D,
|
|
49
|
+
OdontogramEngine: () => OdontogramEngine,
|
|
50
|
+
TOOTH_MESH_NAME_MAP_ADULT: () => TOOTH_MESH_NAME_MAP_ADULT,
|
|
51
|
+
TOOTH_MESH_NAME_MAP_CHILD: () => TOOTH_MESH_NAME_MAP_CHILD,
|
|
52
|
+
mergeTheme: () => mergeTheme,
|
|
53
|
+
toHexColor: () => toHexColor
|
|
54
|
+
});
|
|
55
|
+
module.exports = __toCommonJS(index_exports);
|
|
56
|
+
|
|
57
|
+
// src/Odontogram3D.tsx
|
|
58
|
+
var import_react = require("react");
|
|
59
|
+
|
|
60
|
+
// src/engine.ts
|
|
61
|
+
var THREE2 = __toESM(require("three"), 1);
|
|
62
|
+
var import_OrbitControls = require("three/examples/jsm/controls/OrbitControls.js");
|
|
63
|
+
var import_GLTFLoader = require("three/examples/jsm/loaders/GLTFLoader.js");
|
|
64
|
+
|
|
65
|
+
// src/theme.ts
|
|
66
|
+
var THREE = __toESM(require("three"), 1);
|
|
67
|
+
var DEFAULT_THEME = {
|
|
68
|
+
toothColor: 16119260,
|
|
69
|
+
// Ivory
|
|
70
|
+
highlightColor: 3900150,
|
|
71
|
+
// Tailwind blue-500
|
|
72
|
+
markerColor: 16711680,
|
|
73
|
+
// Red
|
|
74
|
+
canvasBackground: 15528177,
|
|
75
|
+
canvasBackgroundOpacity: 0,
|
|
76
|
+
ambientLightColor: 16777215,
|
|
77
|
+
ambientLightIntensity: 0.8,
|
|
78
|
+
keyLightColor: 16777215,
|
|
79
|
+
keyLightIntensity: 0.6,
|
|
80
|
+
fillLightColor: 16777215,
|
|
81
|
+
fillLightIntensity: 0.4
|
|
82
|
+
};
|
|
83
|
+
function mergeTheme(theme) {
|
|
84
|
+
return __spreadValues(__spreadValues({}, DEFAULT_THEME), theme);
|
|
85
|
+
}
|
|
86
|
+
function toHexColor(input) {
|
|
87
|
+
if (typeof input === "number") return input;
|
|
88
|
+
return new THREE.Color(input).getHex();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/toothNameMaps.ts
|
|
92
|
+
var TOOTH_MESH_NAME_MAP_CHILD = {
|
|
93
|
+
Gums_Maxilla001_0: "Gums Maxilla",
|
|
94
|
+
Gums_Mandibula001_0: "Gums Mandibulla",
|
|
95
|
+
A_0: "(Left Top) Molar",
|
|
96
|
+
B_0: "(Left Top) Premolar",
|
|
97
|
+
C_0: "(Left Top) Canine",
|
|
98
|
+
D_0: "(Left Top) Lateral",
|
|
99
|
+
E_0: "(Left Top) Central",
|
|
100
|
+
F_0: "(Right Top) Central",
|
|
101
|
+
G_0: "(Right Top) Lateral",
|
|
102
|
+
H_0: "(Right Top) Canine",
|
|
103
|
+
I_0: "(Right Top) Premolar",
|
|
104
|
+
J_0: "(Right Top) Molar",
|
|
105
|
+
K_0: "(Left Bottom) Molar",
|
|
106
|
+
L_0: "(Left Bottom) Premolar",
|
|
107
|
+
M_0: "(Left Bottom) Canine",
|
|
108
|
+
N_0: "(Left Bottom) Lateral",
|
|
109
|
+
O_0: "(Left Bottom) Central",
|
|
110
|
+
P_0: "(Right Bottom) Central",
|
|
111
|
+
Q_0: "(Right Bottom) Lateral",
|
|
112
|
+
R_0: "(Right Bottom) Canine",
|
|
113
|
+
S_0: "(Right Bottom) Premolar",
|
|
114
|
+
T_0: "(Right Bottom) Molar"
|
|
115
|
+
};
|
|
116
|
+
var TOOTH_MESH_NAME_MAP_ADULT = {
|
|
117
|
+
// --- Rahang Atas Kanan (Quadrant 1) ---
|
|
118
|
+
"Box023_01_-_Default_0": "11 (Right Top) Central Incisor",
|
|
119
|
+
"Box024_01_-_Default_0": "12 (Right Top) Lateral Incisor",
|
|
120
|
+
"Box025_01_-_Default_0": "13 (Right Top) Canine",
|
|
121
|
+
"Box031_01_-_Default_0": "14 (Right Top) First Premolar",
|
|
122
|
+
"Box033_01_-_Default_0": "15 (Right Top) Second Premolar",
|
|
123
|
+
"Box032_01_-_Default_0": "16 (Right Top) First Molar",
|
|
124
|
+
"Box035_01_-_Default_0": "17 (Right Top) Second Molar",
|
|
125
|
+
"Box034_01_-_Default_0": "18 (Right Top) Third Molar",
|
|
126
|
+
// --- Rahang Atas Kiri (Quadrant 2) ---
|
|
127
|
+
"Box020_01_-_Default_0": "21 (Left Top) Central Incisor",
|
|
128
|
+
"Box021_01_-_Default_0": "22 (Left Top) Lateral Incisor",
|
|
129
|
+
"Box022_01_-_Default_0": "23 (Left Top) Canine",
|
|
130
|
+
"Box026_01_-_Default_0": "24 (Left Top) First Premolar",
|
|
131
|
+
"Box028_01_-_Default_0": "25 (Left Top) Second Premolar",
|
|
132
|
+
"Box027_01_-_Default_0": "26 (Left Top) First Molar",
|
|
133
|
+
"Box030_01_-_Default_0": "27 (Left Top) Second Molar",
|
|
134
|
+
"Box029_01_-_Default_0": "28 (Left Top) Third Molar",
|
|
135
|
+
// --- Rahang Bawah Kiri (Quadrant 3) ---
|
|
136
|
+
"Box011_01_-_Default_0": "38 (Left Bottom) Third Molar",
|
|
137
|
+
"Box013_01_-_Default_0": "37 (Left Bottom) Second Molar",
|
|
138
|
+
"Box008_01_-_Default_0": "36 (Left Bottom) First Molar",
|
|
139
|
+
"Box009_01_-_Default_0": "35 (Left Bottom) Second Premolar",
|
|
140
|
+
"Box007_01_-_Default_0": "34 (Left Bottom) First Premolar",
|
|
141
|
+
"Box003_01_-_Default_0": "33 (Left Bottom) Canine",
|
|
142
|
+
"Box002_01_-_Default_0": "32 (Left Bottom) Lateral Incisor",
|
|
143
|
+
"Box001_01_-_Default_0": "31 (Left Bottom) Central Incisor",
|
|
144
|
+
// --- Rahang Bawah Kanan (Quadrant 4) ---
|
|
145
|
+
"Box004_01_-_Default_0": "41 (Right Bottom) Central Incisor",
|
|
146
|
+
"Box005_01_-_Default_0": "42 (Right Bottom) Lateral Incisor",
|
|
147
|
+
"Box006_01_-_Default_0": "43 (Right Bottom) Canine",
|
|
148
|
+
"Box015_01_-_Default_0": "44 (Right Bottom) First Premolar",
|
|
149
|
+
"Box017_01_-_Default_0": "45 (Right Bottom) Second Premolar",
|
|
150
|
+
"Box016_01_-_Default_0": "46 (Right Bottom) First Molar",
|
|
151
|
+
"Box019_01_-_Default_0": "47 (Right Bottom) Second Molar",
|
|
152
|
+
"GeoSphere001_01_-_Default_0": "48 (Right Bottom) Third Molar",
|
|
153
|
+
// --- Lain-lain ---
|
|
154
|
+
"Tube002_07_-_Default_0": "(Top Tube) Gums Maxilla",
|
|
155
|
+
"Tube001_07_-_Default_0": "(Bottom Tube) Gums Mandibula",
|
|
156
|
+
"lingua_02_-_Default_0": "(Tongue) Lingua"
|
|
157
|
+
};
|
|
158
|
+
function mapForToothType(toothType) {
|
|
159
|
+
return toothType === "child" || !toothType ? TOOTH_MESH_NAME_MAP_CHILD : TOOTH_MESH_NAME_MAP_ADULT;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/engine.ts
|
|
163
|
+
var DEFAULT_MODEL_FILE_NAMES = { child: "ip1.bin.gz", adult: "ip3.bin.gz" };
|
|
164
|
+
var OdontogramEngine = class {
|
|
165
|
+
constructor(canvas, options = {}) {
|
|
166
|
+
this.raycaster = new THREE2.Raycaster();
|
|
167
|
+
this.pointer = new THREE2.Vector2();
|
|
168
|
+
this.toothMeshes = [];
|
|
169
|
+
this.animationFrameId = null;
|
|
170
|
+
this.mappedPoints = [];
|
|
171
|
+
this.disposed = false;
|
|
172
|
+
this.lights = null;
|
|
173
|
+
var _a, _b;
|
|
174
|
+
this.canvas = canvas;
|
|
175
|
+
this.options = options;
|
|
176
|
+
this.theme = mergeTheme(options.theme);
|
|
177
|
+
this.scene = new THREE2.Scene();
|
|
178
|
+
this.camera = new THREE2.PerspectiveCamera(
|
|
179
|
+
50,
|
|
180
|
+
canvas.clientWidth / canvas.clientHeight || 1,
|
|
181
|
+
0.1,
|
|
182
|
+
200
|
|
183
|
+
);
|
|
184
|
+
this.camera.position.set(0, 2.5, 100);
|
|
185
|
+
this.renderer = new THREE2.WebGLRenderer({ antialias: true, canvas, alpha: true });
|
|
186
|
+
this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
|
187
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
188
|
+
this.applyCanvasBackground();
|
|
189
|
+
this.controls = new import_OrbitControls.OrbitControls(this.camera, this.renderer.domElement);
|
|
190
|
+
this.controls.enableDamping = true;
|
|
191
|
+
this.controls.dampingFactor = 0.05;
|
|
192
|
+
this.controls.screenSpacePanning = false;
|
|
193
|
+
this.controls.minDistance = (_a = options.minDistance) != null ? _a : 100;
|
|
194
|
+
this.controls.maxDistance = (_b = options.maxDistance) != null ? _b : 130;
|
|
195
|
+
this.controls.target.set(4, 8, 0);
|
|
196
|
+
this.toothMaterialTemplate = new THREE2.MeshPhongMaterial({
|
|
197
|
+
color: toHexColor(this.theme.toothColor),
|
|
198
|
+
specular: 5592405,
|
|
199
|
+
shininess: 30,
|
|
200
|
+
flatShading: false,
|
|
201
|
+
side: THREE2.DoubleSide
|
|
202
|
+
});
|
|
203
|
+
this.setupLights();
|
|
204
|
+
const animate = () => {
|
|
205
|
+
if (this.disposed) return;
|
|
206
|
+
this.animationFrameId = requestAnimationFrame(animate);
|
|
207
|
+
this.controls.update();
|
|
208
|
+
this.renderer.render(this.scene, this.camera);
|
|
209
|
+
};
|
|
210
|
+
animate();
|
|
211
|
+
}
|
|
212
|
+
setupLights() {
|
|
213
|
+
const ambient = new THREE2.AmbientLight(
|
|
214
|
+
toHexColor(this.theme.ambientLightColor),
|
|
215
|
+
this.theme.ambientLightIntensity
|
|
216
|
+
);
|
|
217
|
+
this.scene.add(ambient);
|
|
218
|
+
const key = new THREE2.DirectionalLight(
|
|
219
|
+
toHexColor(this.theme.keyLightColor),
|
|
220
|
+
this.theme.keyLightIntensity
|
|
221
|
+
);
|
|
222
|
+
key.position.set(10, 10, 10);
|
|
223
|
+
this.scene.add(key);
|
|
224
|
+
const fill = new THREE2.DirectionalLight(
|
|
225
|
+
toHexColor(this.theme.fillLightColor),
|
|
226
|
+
this.theme.fillLightIntensity
|
|
227
|
+
);
|
|
228
|
+
fill.position.set(-10, -10, -5);
|
|
229
|
+
this.scene.add(fill);
|
|
230
|
+
this.lights = { ambient, key, fill };
|
|
231
|
+
}
|
|
232
|
+
applyCanvasBackground() {
|
|
233
|
+
this.renderer.setClearColor(
|
|
234
|
+
toHexColor(this.theme.canvasBackground),
|
|
235
|
+
this.theme.canvasBackgroundOpacity
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
resolveModelUrl(toothType) {
|
|
239
|
+
var _a;
|
|
240
|
+
const base = (_a = this.options.modelBaseUrl) != null ? _a : "/assets/bin";
|
|
241
|
+
const fileNames = __spreadValues(__spreadValues({}, DEFAULT_MODEL_FILE_NAMES), this.options.modelFileNames);
|
|
242
|
+
const fileName = toothType === "adult" ? fileNames.adult : fileNames.child;
|
|
243
|
+
return `${base.replace(/\/$/, "")}/${fileName}`;
|
|
244
|
+
}
|
|
245
|
+
/** Re-apply a (partial) theme to lights, background and every tooth currently in its default state. */
|
|
246
|
+
applyTheme(theme) {
|
|
247
|
+
this.theme = mergeTheme(__spreadValues(__spreadValues({}, this.theme), theme));
|
|
248
|
+
this.applyCanvasBackground();
|
|
249
|
+
if (this.lights) {
|
|
250
|
+
this.lights.ambient.color.setHex(toHexColor(this.theme.ambientLightColor));
|
|
251
|
+
this.lights.ambient.intensity = this.theme.ambientLightIntensity;
|
|
252
|
+
this.lights.key.color.setHex(toHexColor(this.theme.keyLightColor));
|
|
253
|
+
this.lights.key.intensity = this.theme.keyLightIntensity;
|
|
254
|
+
this.lights.fill.color.setHex(toHexColor(this.theme.fillLightColor));
|
|
255
|
+
this.lights.fill.intensity = this.theme.fillLightIntensity;
|
|
256
|
+
}
|
|
257
|
+
this.toothMaterialTemplate.color.setHex(toHexColor(this.theme.toothColor));
|
|
258
|
+
const mappedToothIds = new Set(this.mappedPoints.map((p) => p.toothId));
|
|
259
|
+
this.forEachToothGroup((group) => {
|
|
260
|
+
const isMapped = Boolean(group.userData.isMapped);
|
|
261
|
+
const color = isMapped ? this.theme.highlightColor : this.theme.toothColor;
|
|
262
|
+
this.setGroupColor(group, toHexColor(color));
|
|
263
|
+
});
|
|
264
|
+
this.scene.children.filter((child) => child.name.startsWith("Marker-")).forEach((marker) => {
|
|
265
|
+
if (marker instanceof THREE2.Mesh && marker.material instanceof THREE2.MeshBasicMaterial) {
|
|
266
|
+
marker.material.color.setHex(toHexColor(this.theme.markerColor));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
void mappedToothIds;
|
|
270
|
+
}
|
|
271
|
+
forEachToothGroup(callback) {
|
|
272
|
+
const seen = /* @__PURE__ */ new Set();
|
|
273
|
+
this.toothMeshes.forEach((mesh) => {
|
|
274
|
+
const group = mesh.userData.parentGroup;
|
|
275
|
+
if (group && !seen.has(group)) {
|
|
276
|
+
seen.add(group);
|
|
277
|
+
callback(group);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
setGroupColor(group, hex) {
|
|
282
|
+
group.traverse((child) => {
|
|
283
|
+
if (child instanceof THREE2.Mesh && child.userData.parentGroup === group && child.material instanceof THREE2.MeshPhongMaterial) {
|
|
284
|
+
child.material.color.setHex(hex);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async loadModel(toothType) {
|
|
289
|
+
var _a, _b, _c, _d, _e, _f;
|
|
290
|
+
const oldArchGroup = this.scene.getObjectByName("ArchGroup");
|
|
291
|
+
if (oldArchGroup) {
|
|
292
|
+
this.scene.remove(oldArchGroup);
|
|
293
|
+
oldArchGroup.traverse((child) => {
|
|
294
|
+
if (child instanceof THREE2.Mesh) {
|
|
295
|
+
child.geometry.dispose();
|
|
296
|
+
const material = child.material;
|
|
297
|
+
if (Array.isArray(material)) material.forEach((m) => m.dispose());
|
|
298
|
+
else material.dispose();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
this.toothMeshes = [];
|
|
303
|
+
this.mappedPoints = [];
|
|
304
|
+
(_b = (_a = this.options).onPointsChange) == null ? void 0 : _b.call(_a, []);
|
|
305
|
+
const loader = new import_GLTFLoader.GLTFLoader();
|
|
306
|
+
const modelUrl = this.resolveModelUrl(toothType);
|
|
307
|
+
(_d = (_c = this.options).onLoadingChange) == null ? void 0 : _d.call(_c, true);
|
|
308
|
+
try {
|
|
309
|
+
const gltf = await loader.loadAsync(modelUrl);
|
|
310
|
+
if (this.disposed) return;
|
|
311
|
+
const loadedModel = gltf.scene;
|
|
312
|
+
loadedModel.scale.set(1, 1, 1);
|
|
313
|
+
loadedModel.position.set(0, -2, 0);
|
|
314
|
+
loadedModel.rotation.set(0, 0, 0);
|
|
315
|
+
loadedModel.name = "ArchGroup";
|
|
316
|
+
const nameMap = mapForToothType(toothType);
|
|
317
|
+
loadedModel.traverse((child) => {
|
|
318
|
+
if (!(child instanceof THREE2.Mesh)) return;
|
|
319
|
+
child.geometry.computeVertexNormals();
|
|
320
|
+
if (child.geometry.attributes.color) {
|
|
321
|
+
child.geometry.deleteAttribute("color");
|
|
322
|
+
}
|
|
323
|
+
let mappedName = nameMap[child.name];
|
|
324
|
+
let targetObject = child;
|
|
325
|
+
if (!mappedName && child.parent && child.parent.type !== "Scene") {
|
|
326
|
+
if (nameMap[child.parent.name]) {
|
|
327
|
+
mappedName = nameMap[child.parent.name];
|
|
328
|
+
targetObject = child.parent;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
child.material = this.toothMaterialTemplate.clone();
|
|
332
|
+
if (!targetObject.userData.isTooth) {
|
|
333
|
+
targetObject.userData = {
|
|
334
|
+
toothId: mappedName != null ? mappedName : child.name,
|
|
335
|
+
isTooth: true,
|
|
336
|
+
isMapped: false,
|
|
337
|
+
markerIds: []
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
child.userData.parentGroup = targetObject;
|
|
341
|
+
this.toothMeshes.push(child);
|
|
342
|
+
});
|
|
343
|
+
this.scene.add(loadedModel);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error(`[odontogram-3d] Failed to load model at ${modelUrl}:`, error);
|
|
346
|
+
} finally {
|
|
347
|
+
(_f = (_e = this.options).onLoadingChange) == null ? void 0 : _f.call(_e, false);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
removeMarkers(markerIds) {
|
|
351
|
+
markerIds.forEach((id) => {
|
|
352
|
+
const marker = this.scene.getObjectByName(`Marker-${id}`);
|
|
353
|
+
if (marker instanceof THREE2.Mesh) {
|
|
354
|
+
this.scene.remove(marker);
|
|
355
|
+
marker.geometry.dispose();
|
|
356
|
+
marker.material.dispose();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
addMarker(id, position) {
|
|
361
|
+
const geometry = new THREE2.SphereGeometry(0.05, 8, 8);
|
|
362
|
+
const material = new THREE2.MeshBasicMaterial({ color: toHexColor(this.theme.markerColor) });
|
|
363
|
+
const marker = new THREE2.Mesh(geometry, material);
|
|
364
|
+
marker.position.copy(position);
|
|
365
|
+
marker.name = `Marker-${id}`;
|
|
366
|
+
this.scene.add(marker);
|
|
367
|
+
}
|
|
368
|
+
emitPointsChange() {
|
|
369
|
+
var _a, _b;
|
|
370
|
+
(_b = (_a = this.options).onPointsChange) == null ? void 0 : _b.call(_a, [...this.mappedPoints]);
|
|
371
|
+
}
|
|
372
|
+
/** Handle a click/tap on the canvas (expects coordinates relative to the viewport, like a MouseEvent). */
|
|
373
|
+
handlePointerDown(clientX, clientY) {
|
|
374
|
+
var _a;
|
|
375
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
376
|
+
this.pointer.x = (clientX - rect.left) / rect.width * 2 - 1;
|
|
377
|
+
this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
|
378
|
+
this.raycaster.setFromCamera(this.pointer, this.camera);
|
|
379
|
+
const intersects = this.raycaster.intersectObjects(this.toothMeshes, false);
|
|
380
|
+
if (intersects.length === 0) return;
|
|
381
|
+
const clickedMesh = intersects[0].object;
|
|
382
|
+
const toothGroup = clickedMesh.userData.parentGroup;
|
|
383
|
+
if (!toothGroup || !toothGroup.userData.isTooth) return;
|
|
384
|
+
const toothId = toothGroup.userData.toothId;
|
|
385
|
+
const point = intersects[0].point;
|
|
386
|
+
const isCurrentlyMapped = Boolean(toothGroup.userData.isMapped);
|
|
387
|
+
if (isCurrentlyMapped) {
|
|
388
|
+
this.removeMarkers((_a = toothGroup.userData.markerIds) != null ? _a : []);
|
|
389
|
+
this.setGroupColor(toothGroup, toHexColor(this.theme.toothColor));
|
|
390
|
+
toothGroup.userData.isMapped = false;
|
|
391
|
+
toothGroup.userData.markerIds = [];
|
|
392
|
+
this.mappedPoints = this.mappedPoints.filter((p) => p.toothId !== toothId);
|
|
393
|
+
this.emitPointsChange();
|
|
394
|
+
} else {
|
|
395
|
+
const newMarkerId = Date.now();
|
|
396
|
+
this.setGroupColor(toothGroup, toHexColor(this.theme.highlightColor));
|
|
397
|
+
toothGroup.userData.isMapped = true;
|
|
398
|
+
toothGroup.userData.markerIds = [newMarkerId];
|
|
399
|
+
this.addMarker(newMarkerId, point);
|
|
400
|
+
const newPoint = {
|
|
401
|
+
id: newMarkerId,
|
|
402
|
+
toothId,
|
|
403
|
+
x: point.x.toFixed(2),
|
|
404
|
+
y: point.y.toFixed(2),
|
|
405
|
+
z: point.z.toFixed(2),
|
|
406
|
+
type: "Karies",
|
|
407
|
+
description: ""
|
|
408
|
+
};
|
|
409
|
+
this.mappedPoints = [...this.mappedPoints, newPoint];
|
|
410
|
+
this.emitPointsChange();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
getMappedPoints() {
|
|
414
|
+
return [...this.mappedPoints];
|
|
415
|
+
}
|
|
416
|
+
/** Patch metadata (e.g. description) on an existing point without touching the 3D scene. */
|
|
417
|
+
updatePoint(id, patch) {
|
|
418
|
+
this.mappedPoints = this.mappedPoints.map((p) => p.id === id ? __spreadValues(__spreadValues({}, p), patch) : p);
|
|
419
|
+
this.emitPointsChange();
|
|
420
|
+
}
|
|
421
|
+
resetMapping() {
|
|
422
|
+
const existingMarkerIds = this.scene.children.filter((c) => c.name.startsWith("Marker-")).map((c) => Number(c.name.replace("Marker-", "")));
|
|
423
|
+
this.removeMarkers(existingMarkerIds);
|
|
424
|
+
this.forEachToothGroup((group) => {
|
|
425
|
+
this.setGroupColor(group, toHexColor(this.theme.toothColor));
|
|
426
|
+
group.userData.isMapped = false;
|
|
427
|
+
group.userData.markerIds = [];
|
|
428
|
+
});
|
|
429
|
+
this.mappedPoints = [];
|
|
430
|
+
this.emitPointsChange();
|
|
431
|
+
}
|
|
432
|
+
/** Replace all diagnostic points and redraw markers/highlights to match (e.g. after importing JSON). */
|
|
433
|
+
setMappedPoints(points) {
|
|
434
|
+
this.resetMapping();
|
|
435
|
+
points.forEach((point) => {
|
|
436
|
+
var _a, _b;
|
|
437
|
+
const group = (_a = this.toothMeshes.find(
|
|
438
|
+
(mesh) => {
|
|
439
|
+
var _a2;
|
|
440
|
+
return ((_a2 = mesh.userData.parentGroup) == null ? void 0 : _a2.userData.toothId) === point.toothId;
|
|
441
|
+
}
|
|
442
|
+
)) == null ? void 0 : _a.userData.parentGroup;
|
|
443
|
+
if (!group) return;
|
|
444
|
+
this.setGroupColor(group, toHexColor(this.theme.highlightColor));
|
|
445
|
+
group.userData.isMapped = true;
|
|
446
|
+
group.userData.markerIds = [...(_b = group.userData.markerIds) != null ? _b : [], point.id];
|
|
447
|
+
this.addMarker(point.id, new THREE2.Vector3(parseFloat(point.x), parseFloat(point.y), parseFloat(point.z)));
|
|
448
|
+
});
|
|
449
|
+
this.mappedPoints = [...points];
|
|
450
|
+
this.emitPointsChange();
|
|
451
|
+
}
|
|
452
|
+
resize() {
|
|
453
|
+
const container = this.canvas.parentElement;
|
|
454
|
+
if (!container) return;
|
|
455
|
+
const { clientWidth, clientHeight } = container;
|
|
456
|
+
if (clientWidth === 0 || clientHeight === 0) return;
|
|
457
|
+
this.camera.aspect = clientWidth / clientHeight;
|
|
458
|
+
this.camera.updateProjectionMatrix();
|
|
459
|
+
this.renderer.setSize(clientWidth, clientHeight, false);
|
|
460
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
461
|
+
}
|
|
462
|
+
dispose() {
|
|
463
|
+
this.disposed = true;
|
|
464
|
+
if (this.animationFrameId !== null) cancelAnimationFrame(this.animationFrameId);
|
|
465
|
+
const oldArchGroup = this.scene.getObjectByName("ArchGroup");
|
|
466
|
+
if (oldArchGroup) this.scene.remove(oldArchGroup);
|
|
467
|
+
this.scene.traverse((child) => {
|
|
468
|
+
if (child instanceof THREE2.Mesh) {
|
|
469
|
+
child.geometry.dispose();
|
|
470
|
+
const material = child.material;
|
|
471
|
+
if (Array.isArray(material)) material.forEach((m) => m.dispose());
|
|
472
|
+
else material == null ? void 0 : material.dispose();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
this.toothMaterialTemplate.dispose();
|
|
476
|
+
this.controls.dispose();
|
|
477
|
+
this.renderer.dispose();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/Odontogram3D.tsx
|
|
482
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
483
|
+
var Odontogram3D = (0, import_react.forwardRef)(
|
|
484
|
+
function Odontogram3D2({
|
|
485
|
+
toothType = "child",
|
|
486
|
+
theme,
|
|
487
|
+
modelBaseUrl,
|
|
488
|
+
modelFileNames,
|
|
489
|
+
initialPoints,
|
|
490
|
+
onPointsChange,
|
|
491
|
+
onLoadingChange,
|
|
492
|
+
minDistance,
|
|
493
|
+
maxDistance,
|
|
494
|
+
className,
|
|
495
|
+
style
|
|
496
|
+
}, ref) {
|
|
497
|
+
const canvasRef = (0, import_react.useRef)(null);
|
|
498
|
+
const engineRef = (0, import_react.useRef)(null);
|
|
499
|
+
const isLoadingRef = (0, import_react.useRef)(true);
|
|
500
|
+
const onPointsChangeRef = (0, import_react.useRef)(onPointsChange);
|
|
501
|
+
const onLoadingChangeRef = (0, import_react.useRef)(onLoadingChange);
|
|
502
|
+
onPointsChangeRef.current = onPointsChange;
|
|
503
|
+
onLoadingChangeRef.current = onLoadingChange;
|
|
504
|
+
(0, import_react.useEffect)(() => {
|
|
505
|
+
const canvas = canvasRef.current;
|
|
506
|
+
if (!canvas) return;
|
|
507
|
+
const engine = new OdontogramEngine(canvas, {
|
|
508
|
+
theme,
|
|
509
|
+
modelBaseUrl,
|
|
510
|
+
modelFileNames,
|
|
511
|
+
minDistance,
|
|
512
|
+
maxDistance,
|
|
513
|
+
onLoadingChange: (loading) => {
|
|
514
|
+
var _a;
|
|
515
|
+
isLoadingRef.current = loading;
|
|
516
|
+
(_a = onLoadingChangeRef.current) == null ? void 0 : _a.call(onLoadingChangeRef, loading);
|
|
517
|
+
},
|
|
518
|
+
onPointsChange: (points) => {
|
|
519
|
+
var _a;
|
|
520
|
+
return (_a = onPointsChangeRef.current) == null ? void 0 : _a.call(onPointsChangeRef, points);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
engineRef.current = engine;
|
|
524
|
+
engine.loadModel(toothType).then(() => {
|
|
525
|
+
if (initialPoints && initialPoints.length > 0) {
|
|
526
|
+
engine.setMappedPoints(initialPoints);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const handleResize = () => engine.resize();
|
|
530
|
+
window.addEventListener("resize", handleResize);
|
|
531
|
+
return () => {
|
|
532
|
+
window.removeEventListener("resize", handleResize);
|
|
533
|
+
engine.dispose();
|
|
534
|
+
engineRef.current = null;
|
|
535
|
+
};
|
|
536
|
+
}, []);
|
|
537
|
+
const previousToothType = (0, import_react.useRef)(toothType);
|
|
538
|
+
(0, import_react.useEffect)(() => {
|
|
539
|
+
var _a;
|
|
540
|
+
if (previousToothType.current === toothType) return;
|
|
541
|
+
previousToothType.current = toothType;
|
|
542
|
+
(_a = engineRef.current) == null ? void 0 : _a.loadModel(toothType);
|
|
543
|
+
}, [toothType]);
|
|
544
|
+
(0, import_react.useEffect)(() => {
|
|
545
|
+
var _a;
|
|
546
|
+
if (!theme) return;
|
|
547
|
+
(_a = engineRef.current) == null ? void 0 : _a.applyTheme(theme);
|
|
548
|
+
}, [theme]);
|
|
549
|
+
const handlePointerDown = (0, import_react.useCallback)((event) => {
|
|
550
|
+
var _a;
|
|
551
|
+
if (isLoadingRef.current) return;
|
|
552
|
+
(_a = engineRef.current) == null ? void 0 : _a.handlePointerDown(event.clientX, event.clientY);
|
|
553
|
+
}, []);
|
|
554
|
+
(0, import_react.useImperativeHandle)(
|
|
555
|
+
ref,
|
|
556
|
+
() => ({
|
|
557
|
+
resetMapping: () => {
|
|
558
|
+
var _a;
|
|
559
|
+
return (_a = engineRef.current) == null ? void 0 : _a.resetMapping();
|
|
560
|
+
},
|
|
561
|
+
getMappedPoints: () => {
|
|
562
|
+
var _a, _b;
|
|
563
|
+
return (_b = (_a = engineRef.current) == null ? void 0 : _a.getMappedPoints()) != null ? _b : [];
|
|
564
|
+
},
|
|
565
|
+
setMappedPoints: (points) => {
|
|
566
|
+
var _a;
|
|
567
|
+
return (_a = engineRef.current) == null ? void 0 : _a.setMappedPoints(points);
|
|
568
|
+
},
|
|
569
|
+
updatePoint: (id, patch) => {
|
|
570
|
+
var _a;
|
|
571
|
+
return (_a = engineRef.current) == null ? void 0 : _a.updatePoint(id, patch);
|
|
572
|
+
},
|
|
573
|
+
applyTheme: (nextTheme) => {
|
|
574
|
+
var _a;
|
|
575
|
+
return (_a = engineRef.current) == null ? void 0 : _a.applyTheme(nextTheme);
|
|
576
|
+
}
|
|
577
|
+
}),
|
|
578
|
+
[]
|
|
579
|
+
);
|
|
580
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
581
|
+
"canvas",
|
|
582
|
+
{
|
|
583
|
+
ref: canvasRef,
|
|
584
|
+
className,
|
|
585
|
+
style: __spreadValues({ width: "100%", height: "100%" }, style),
|
|
586
|
+
onMouseDown: handlePointerDown
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
592
|
+
0 && (module.exports = {
|
|
593
|
+
DEFAULT_THEME,
|
|
594
|
+
Odontogram3D,
|
|
595
|
+
OdontogramEngine,
|
|
596
|
+
TOOTH_MESH_NAME_MAP_ADULT,
|
|
597
|
+
TOOTH_MESH_NAME_MAP_CHILD,
|
|
598
|
+
mergeTheme,
|
|
599
|
+
toHexColor
|
|
600
|
+
});
|
|
601
|
+
//# sourceMappingURL=index.cjs.map
|