Files
general-template/composables/apps/medical/useOdontogram.ts
T
Yusron alamsyah 6bb6a1d430 first commit
2026-03-13 10:45:28 +07:00

2218 lines
67 KiB
TypeScript

import { ref, reactive, onMounted } from "vue";
import { useOdontogramStore } from "~/store/apps/medical/odontogram";
import { OdontogramMode } from "~/types/apps/medical/odontogram";
import {
Polygon,
AMF,
COF,
FIS,
NVT,
RCT,
NON,
UNE,
PRE,
ANO,
CARIES,
CFR,
FMC,
POC,
RRX,
MIS,
IPX,
FRM_ACR,
BRIDGE,
ARROW_TOP_LEFT,
ARROW_TOP_RIGHT,
ARROW_TOP_TURN_LEFT,
ARROW_TOP_TURN_RIGHT,
ARROW_BOTTOM_LEFT,
ARROW_BOTTOM_RIGHT,
ARROW_BOTTOM_TURN_LEFT,
ARROW_BOTTOM_TURN_RIGHT,
HAPUS
} from "./useToothRenderer";
const ODONTOGRAM_MODE_HAPUS = 100;
const ODONTOGRAM_MODE_DEFAULT = 0;
const ODONTOGRAM_MODE_AMF = 1;
const ODONTOGRAM_MODE_COF = 2;
const ODONTOGRAM_MODE_FIS = 3;
const ODONTOGRAM_MODE_NVT = 4;
const ODONTOGRAM_MODE_RCT = 5;
const ODONTOGRAM_MODE_NON = 6;
const ODONTOGRAM_MODE_UNE = 7;
const ODONTOGRAM_MODE_PRE = 8;
const ODONTOGRAM_MODE_ANO = 9;
const ODONTOGRAM_MODE_CARIES = 10;
const ODONTOGRAM_MODE_CFR = 11;
const ODONTOGRAM_MODE_FMC = 12;
const ODONTOGRAM_MODE_POC = 13;
const ODONTOGRAM_MODE_RRX = 14;
const ODONTOGRAM_MODE_MIS = 15;
const ODONTOGRAM_MODE_IPX = 16;
const ODONTOGRAM_MODE_FRM_ACR = 17;
const ODONTOGRAM_MODE_BRIDGE = 18;
// Add arrow modes
const ODONTOGRAM_MODE_ARROW_TOP_LEFT = 19;
const ODONTOGRAM_MODE_ARROW_TOP_RIGHT = 20;
const ODONTOGRAM_MODE_ARROW_TOP_TURN_LEFT = 21;
const ODONTOGRAM_MODE_ARROW_TOP_TURN_RIGHT = 22;
const ODONTOGRAM_MODE_ARROW_BOTTOM_LEFT = 23;
const ODONTOGRAM_MODE_ARROW_BOTTOM_RIGHT = 24;
const ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_LEFT = 25;
const ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_RIGHT = 26;
class Odontogram {
canvas: HTMLCanvasElement | null = null;
context: CanvasRenderingContext2D | null = null;
mode = ODONTOGRAM_MODE_DEFAULT;
hoverGeoms: any[] = [];
geometry: Record<string, any[]> = {};
active_geometry: any = null;
teeth: Record<string, any> = {};
background: any = null;
initialize(canvas: HTMLCanvasElement, width: number, height: number) {
this.canvas = canvas;
this.canvas.width = width;
this.canvas.height = height;
this.context = canvas.getContext("2d");
this._drawBackground();
}
setMode(mode: number) {
this.mode = mode;
}
redraw() {
if (!this.context || !this.background) return;
// Clear canvas and redraw background
this.context.putImageData(this.background.image, 0, 0);
// Draw existing geometry
for (const keyCoord in this.geometry) {
const geoms = this.geometry[keyCoord];
if (!geoms) continue;
// Separate bridge and non-bridge geometries
const bridgeGeoms = geoms.filter(
(g) => g && g.mode === ODONTOGRAM_MODE_BRIDGE
);
const nonBridgeGeoms = geoms.filter(
(g) => g && g.mode !== ODONTOGRAM_MODE_BRIDGE
);
// Draw non-bridge geometries first
for (const geom of nonBridgeGeoms) {
if (geom && typeof geom.render === "function") {
geom.render(this.context);
}
}
// Draw bridge geometries last (on top)
for (const geom of bridgeGeoms) {
if (geom && typeof geom.render === "function") {
geom.render(this.context);
}
}
}
// Draw hover geometry
for (const hoverGeom of this.hoverGeoms) {
if (hoverGeom && typeof hoverGeom.render === "function") {
hoverGeom.render(this.context);
}
}
// Draw quadrant dividing lines
const ctx = this.context;
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(0, 0, 0, 0.3)";
// Vertical dividing line between teeth 14 and 21
const verticalX =
55 + // pl - 20px left shift
((ctx.canvas.width - (85 + 10 + 5 * 16 + 75)) / 16) * 8 + // half width of 8 teeth
5 * 8 + // gap_per * 8
75 / 2; // half gap_bag
ctx.moveTo(verticalX, 75);
ctx.lineTo(verticalX, ctx.canvas.height - 75);
// Horizontal dividing line between upper and lower teeth rows
const horizontalY = 75 + (ctx.canvas.height - 75 - 10) / 2 - 35;
ctx.moveTo(85, horizontalY);
ctx.lineTo(ctx.canvas.width - 75, horizontalY);
ctx.stroke();
}
_sideTeeth(
ctx: CanvasRenderingContext2D,
numbers: string[],
bigBoxSize: number,
smallBoxSize: number,
xpos: number,
ypos: number,
options: { numberPosition?: "above" | "below" } = {}
) {
const offsetX = (options as any).offsetX || 0;
const offsetY = (options as any).offsetY || 0;
const scale = (options as any).scale || 1;
const adjXpos = xpos + offsetX;
const adjYpos = ypos + offsetY;
const adjBigBoxSize = bigBoxSize * scale;
const adjSmallBoxSize = smallBoxSize * scale;
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#303030ff";
ctx.rect(
adjXpos + adjSmallBoxSize / 2,
adjYpos + adjSmallBoxSize / 2,
adjSmallBoxSize,
adjSmallBoxSize
);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos, adjYpos);
ctx.lineTo(adjXpos + adjSmallBoxSize / 2, adjYpos + adjSmallBoxSize / 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos + adjBigBoxSize, adjYpos);
ctx.lineTo(
adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
adjYpos + adjSmallBoxSize / 2
);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos, adjYpos + adjBigBoxSize);
ctx.lineTo(
adjXpos + adjSmallBoxSize / 2,
adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos + adjBigBoxSize, adjYpos + adjBigBoxSize);
ctx.lineTo(
adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
);
ctx.stroke();
const num = numbers.shift();
ctx.font = `${14 * scale}px Arial Black`;
ctx.fillStyle = "#303030ff"; // Set text color to black explicitly
ctx.textBaseline = options.numberPosition === "above" ? "top" : "bottom";
ctx.textAlign = "center";
if (num) {
ctx.fillText(
num,
adjXpos + adjBigBoxSize / 2,
options.numberPosition === "above"
? adjYpos - bigBoxSize * 0.2 - 10
: adjYpos + bigBoxSize * 1.4
);
}
const x1 = adjXpos;
const y1 = adjYpos;
const x2 = adjXpos + adjBigBoxSize;
const y2 = adjYpos + adjBigBoxSize;
const cx = adjXpos + adjBigBoxSize / 2;
const cy = adjYpos + adjBigBoxSize / 2;
const key = `${x1}:${y1};${x2}:${y2};${cx}:${cy}`;
this.teeth[key] = {
num: num || "",
bigBoxSize: adjBigBoxSize,
smallBoxSize: adjSmallBoxSize,
x1,
y1,
x2,
y2,
cx,
cy,
top: {
tl: { x: adjXpos, y: adjYpos },
tr: { x: adjXpos + adjBigBoxSize, y: adjYpos },
br: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
y: adjYpos + adjSmallBoxSize / 2
},
bl: {
x: adjXpos + adjSmallBoxSize / 2,
y: adjYpos + adjSmallBoxSize / 2
}
},
right: {
tl: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
y: adjYpos + adjSmallBoxSize / 2
},
tr: { x: adjXpos + adjBigBoxSize, y: adjYpos },
br: { x: adjXpos + adjBigBoxSize, y: adjYpos + adjBigBoxSize },
bl: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
}
},
bottom: {
tl: {
x: adjXpos + adjSmallBoxSize / 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
},
tr: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
},
br: { x: adjXpos + adjBigBoxSize, y: adjYpos + adjBigBoxSize },
bl: { x: adjXpos, y: adjYpos + adjBigBoxSize }
},
left: {
tl: { x: adjXpos, y: adjYpos },
tr: {
x: adjXpos + adjSmallBoxSize / 2,
y: adjYpos + adjSmallBoxSize / 2
},
br: {
x: adjXpos + adjSmallBoxSize / 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
},
bl: { x: adjXpos, y: adjYpos + adjBigBoxSize }
},
middle: {
tl: {
x: adjXpos + adjSmallBoxSize / 2,
y: adjYpos + adjSmallBoxSize / 2
},
tr: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
y: adjYpos + adjSmallBoxSize / 2
},
br: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
},
bl: {
x: adjXpos + adjSmallBoxSize / 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize / 2
}
}
};
}
_centerTeeth(
ctx: CanvasRenderingContext2D,
numbers: string[],
bigBoxSize: number,
smallBoxSize: number,
xpos: number,
ypos: number,
options: { numberPosition?: "above" | "below" } = {}
) {
const offsetX = (options as any).offsetX || 0;
const offsetY = (options as any).offsetY || 0;
const scale = (options as any).scale || 1;
const adjXpos = xpos + offsetX;
const adjYpos = ypos + offsetY;
const adjBigBoxSize = bigBoxSize * scale;
const adjSmallBoxSize = smallBoxSize * scale;
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#303030ff";
ctx.rect(
adjXpos + adjSmallBoxSize / 2 + 3,
adjYpos + adjSmallBoxSize - 3,
adjSmallBoxSize - 6,
0
);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos, adjYpos);
ctx.lineTo(
adjXpos + adjSmallBoxSize / 2 + 3,
adjYpos + adjSmallBoxSize - 3
);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos + adjBigBoxSize, adjYpos);
ctx.lineTo(
adjXpos + adjBigBoxSize - adjSmallBoxSize / 2 - 3,
adjYpos + adjSmallBoxSize - 3
);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos, adjYpos + adjBigBoxSize);
ctx.lineTo(
adjXpos + adjSmallBoxSize / 2 + 3,
adjYpos + adjBigBoxSize - adjSmallBoxSize - 3
);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(adjXpos + adjBigBoxSize, adjYpos + adjBigBoxSize);
ctx.lineTo(
adjXpos + adjBigBoxSize - adjSmallBoxSize / 2 - 3,
adjYpos + adjBigBoxSize - adjSmallBoxSize - 3
);
ctx.stroke();
const num = numbers.shift();
ctx.font = `${14 * scale}px Arial Black`;
ctx.fillStyle = "#303030ff"; // Set text color to black explicitly
ctx.textBaseline = options.numberPosition === "above" ? "top" : "bottom";
ctx.textAlign = "center";
if (num) {
ctx.fillText(
num,
adjXpos + adjBigBoxSize / 2,
options.numberPosition === "above"
? adjYpos - bigBoxSize * 0.2 - 10
: adjYpos + bigBoxSize * 1.4
);
}
const x1 = adjXpos;
const y1 = adjYpos;
const x2 = adjXpos + adjBigBoxSize;
const y2 = adjYpos + adjBigBoxSize;
const cx = adjXpos + adjBigBoxSize / 2;
const cy = adjYpos + adjBigBoxSize / 2;
const key = `${x1}:${y1};${x2}:${y2};${cx}:${cy}`;
this.teeth[key] = {
num: num || "",
bigBoxSize: adjBigBoxSize,
smallBoxSize: adjSmallBoxSize,
x1,
y1,
x2,
y2,
cx,
cy,
top: {
tl: { x: adjXpos + 1, y: adjYpos },
tr: { x: adjXpos + adjBigBoxSize, y: adjYpos },
br: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2 - 3,
y: adjYpos + adjSmallBoxSize - 3
},
bl: {
x: adjXpos + adjSmallBoxSize / 2 + 3,
y: adjYpos + adjSmallBoxSize - 3
}
},
right: {
tl: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2 - 2,
y: adjYpos + adjSmallBoxSize - 3
},
tr: { x: adjXpos + adjBigBoxSize, y: adjYpos },
br: { x: adjXpos + adjBigBoxSize, y: adjYpos + adjBigBoxSize },
bl: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2 - 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize - 3
}
},
bottom: {
tl: {
x: adjXpos + adjSmallBoxSize / 2 + 3,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize - 3
},
tr: {
x: adjXpos + adjBigBoxSize - adjSmallBoxSize / 2 - 3,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize - 3
},
br: { x: adjXpos + adjBigBoxSize - 1, y: adjYpos + adjBigBoxSize },
bl: { x: adjXpos + 1, y: adjYpos + adjBigBoxSize }
},
left: {
tl: { x: adjXpos, y: adjYpos },
tr: {
x: adjXpos + adjSmallBoxSize / 2 + 2,
y: adjYpos + adjSmallBoxSize - 3
},
br: {
x: adjXpos + adjSmallBoxSize / 2 + 2,
y: adjYpos + adjBigBoxSize - adjSmallBoxSize - 3
},
bl: { x: adjXpos, y: adjYpos + adjBigBoxSize }
},
middle: {
tl: { x: 0, y: 0 },
tr: { x: 0, y: 0 },
br: { x: 0, y: 0 },
bl: { x: 0, y: 0 }
}
};
}
_drawBackground() {
if (!this.context) return;
const ctx = this.context;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const width = ctx.canvas.width;
const height = ctx.canvas.height;
const pl = 85,
pr = 70,
pt = 75,
pb = 10,
gap_per = 7,
gap_bag = 75;
const bigBoxSize = (width - (pl + pr + gap_per * 16 + gap_bag)) / 16;
const smallBoxSize = bigBoxSize / 2;
const numbers = [
"18",
"17",
"16",
"15",
"14",
"13",
"12",
"11",
"21",
"22",
"23",
"24",
"25",
"26",
"27",
"28",
"55",
"54",
"53",
"52",
"51",
"61",
"62",
"63",
"64",
"65",
"85",
"84",
"83",
"82",
"81",
"71",
"72",
"73",
"74",
"75",
"48",
"47",
"46",
"45",
"44",
"43",
"42",
"41",
"31",
"32",
"33",
"34",
"35",
"36",
"37",
"38"
];
let xpos, ypos;
let sec = 0;
for (let y = 0; y < 4; y++) {
sec = 0;
for (let x = 0; x < 16; x++) {
if (x % 8 === 0 && x !== 0) sec++;
if (y % 3 !== 0 && (x < 8 ? (x % 8) - 2 <= 0 : x % 8 >= 5)) continue;
xpos = x * bigBoxSize + pl + x * gap_per + sec * gap_bag;
ypos = y * bigBoxSize + pt + pt * y;
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#303030ff";
ctx.rect(xpos, ypos, bigBoxSize, bigBoxSize);
ctx.stroke();
// Determine numberPosition for rows 2 and 3 always "below"
let numberPosition: "above" | "below" = y < 2 ? "above" : "below";
if (x >= 5 && x <= 10) {
this._centerTeeth(
ctx,
numbers,
bigBoxSize,
smallBoxSize,
xpos,
ypos,
{
numberPosition
}
);
// Swap top and bottom polygons for rows 2 and 3
if (y >= 2) {
const x1 = xpos;
const y1 = ypos;
const x2 = xpos + bigBoxSize;
const y2 = ypos + bigBoxSize;
const cx = xpos + bigBoxSize / 2;
const cy = ypos + bigBoxSize / 2;
const key = `${x1}:${y1};${x2}:${y2};${cx}:${cy}`;
if (this.teeth[key]) {
const temp = this.teeth[key].top;
this.teeth[key].top = this.teeth[key].bottom;
this.teeth[key].bottom = temp;
}
}
} else {
this._sideTeeth(ctx, numbers, bigBoxSize, smallBoxSize, xpos, ypos, {
numberPosition
});
// Swap top and bottom polygons for rows 2 and 3
if (y >= 2) {
const x1 = xpos;
const y1 = ypos;
const x2 = xpos + bigBoxSize;
const y2 = ypos + bigBoxSize;
const cx = xpos + bigBoxSize / 2;
const cy = ypos + bigBoxSize / 2;
const key = `${x1}:${y1};${x2}:${y2};${cx}:${cy}`;
if (this.teeth[key]) {
const temp = this.teeth[key].top;
this.teeth[key].top = this.teeth[key].bottom;
this.teeth[key].bottom = temp;
}
}
}
}
}
this.background = {
image: ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height),
x: 1,
y: 1,
w: ctx.canvas.width,
h: ctx.canvas.height
};
// Draw quadrant dividing lines
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#000";
// Vertical dividing line between teeth 14 and 21
const verticalX =
85 + // pl
((ctx.canvas.width - (85 + 10 + 5 * 16 + 75)) / 16) * 8 + // half width of 8 teeth
5 * 8 + // gap_per * 8
75 / 2; // half gap_bag
ctx.moveTo(verticalX, 75);
ctx.lineTo(verticalX, ctx.canvas.height - 10);
// Horizontal dividing line between upper and lower teeth rows
const horizontalY = 75 + (ctx.canvas.height - 75 - 10) / 2;
ctx.moveTo(0, horizontalY);
ctx.lineTo(ctx.canvas.width, horizontalY);
ctx.stroke();
this.redraw();
}
}
// Composable
const odontogramInstance = ref<Odontogram | null>(null);
const canvas = ref<HTMLCanvasElement | null>(null);
const mode = ref(ODONTOGRAM_MODE_DEFAULT);
const geometry = reactive<Record<string, any[]>>({});
const width = ref(1500);
const height = ref(675);
function initialize(
canvasElement: HTMLCanvasElement,
w: number = 1500,
h: number = 675
) {
// width.value = w;
// height.value = h;
canvas.value = canvasElement;
const instance = new Odontogram();
instance.initialize(canvasElement, w, h);
odontogramInstance.value = instance;
// Sync geometry
Object.assign(geometry, instance.geometry);
}
function downloadImage(callback?: (dataUrl: string) => void) {
if (!odontogramInstance.value || !odontogramInstance.value.canvas) return;
const dataUrl = odontogramInstance.value.canvas.toDataURL();
if (callback) {
callback(dataUrl);
} else {
const link = document.createElement("a");
link.download = "odontogram.png";
link.href = dataUrl;
link.click();
}
}
export function convertGeom(geom: any, mode: number): any {
const vertices = geom.vertices || [];
const options = geom.options || {};
// Override fillStyle for specific modes to ensure visibility
let geomObj;
switch (mode) {
case ODONTOGRAM_MODE_AMF:
options.fillStyle = "rgba(255, 0, 0, 0.7)";
geomObj = new AMF(vertices, options);
break;
case ODONTOGRAM_MODE_COF:
options.fillStyle = "rgba(0, 255, 0, 0.7)";
geomObj = new COF(vertices, options);
break;
case ODONTOGRAM_MODE_FIS:
options.fillStyle = "rgba(255, 0, 255, 0.7)";
geomObj = new FIS(vertices, options);
break;
case ODONTOGRAM_MODE_NVT:
geomObj = new NVT(vertices, options);
break;
case ODONTOGRAM_MODE_RCT:
geomObj = new RCT(vertices, options);
break;
case ODONTOGRAM_MODE_NON:
geomObj = new NON(vertices, options);
break;
case ODONTOGRAM_MODE_UNE:
geomObj = new UNE(vertices, options);
break;
case ODONTOGRAM_MODE_PRE:
geomObj = new PRE(vertices, options);
break;
case ODONTOGRAM_MODE_ANO:
geomObj = new ANO(vertices, options);
break;
case ODONTOGRAM_MODE_CARIES:
geomObj = new CARIES(vertices, options);
break;
case ODONTOGRAM_MODE_CFR:
geomObj = new CFR(vertices, options);
break;
case ODONTOGRAM_MODE_FMC:
geomObj = new FMC(vertices, options);
break;
case ODONTOGRAM_MODE_POC:
geomObj = new POC(vertices, options);
break;
case ODONTOGRAM_MODE_RRX:
geomObj = new RRX(vertices, options);
break;
case ODONTOGRAM_MODE_MIS:
geomObj = new MIS(vertices, options);
break;
case ODONTOGRAM_MODE_IPX:
geomObj = new IPX(vertices, options);
break;
case ODONTOGRAM_MODE_FRM_ACR:
geomObj = new FRM_ACR(vertices, options);
break;
case ODONTOGRAM_MODE_BRIDGE:
geomObj = new BRIDGE(geom.startVert, geom.endVert, options);
break;
case ODONTOGRAM_MODE_ARROW_TOP_LEFT:
geomObj = new ARROW_TOP_LEFT(vertices, options);
break;
case ODONTOGRAM_MODE_ARROW_TOP_RIGHT:
geomObj = new ARROW_TOP_RIGHT(vertices, options);
break;
case ODONTOGRAM_MODE_ARROW_BOTTOM_LEFT:
geomObj = new ARROW_BOTTOM_LEFT(vertices, options);
break;
case ODONTOGRAM_MODE_ARROW_BOTTOM_RIGHT:
geomObj = new ARROW_BOTTOM_RIGHT(vertices, options);
break;
case ODONTOGRAM_MODE_ARROW_TOP_TURN_LEFT:
geomObj = new ARROW_TOP_TURN_LEFT(vertices, options);
break;
case ODONTOGRAM_MODE_ARROW_TOP_TURN_RIGHT:
geomObj = new ARROW_TOP_TURN_RIGHT(vertices, options);
break;
case ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_LEFT:
geomObj = new ARROW_BOTTOM_TURN_LEFT(vertices, options);
break;
case ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_RIGHT:
geomObj = new ARROW_BOTTOM_TURN_RIGHT(vertices, options);
break;
case ODONTOGRAM_MODE_HAPUS:
geomObj = new HAPUS(vertices, options);
break;
default:
geomObj = new Polygon(vertices, options);
}
geomObj.mode = mode;
// Preserve pos property from input geom, cast to any to avoid TS error
(geomObj as any).pos = geom.pos;
return geomObj;
}
function onMouseMove(
event: MouseEvent,
emit?: (event: string, data: any) => void
) {
// console.log("onMouseMove called with mode:", mode.value);
// if (mode.value === ODONTOGRAM_MODE_IPX) {
// console.log("IPX mode detected in onMouseMove");
// }
if (!odontogramInstance.value || !odontogramInstance.value.canvas) return;
const canvas = odontogramInstance.value.canvas;
const rect = canvas.getBoundingClientRect();
// Calculate scale factors for X and Y
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// Adjust mouse coordinates to canvas internal resolution
const mouse = {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY
};
odontogramInstance.value.hoverGeoms = [];
for (const keyCoord in odontogramInstance.value.teeth) {
const teeth = odontogramInstance.value.teeth[keyCoord];
const coord = parseKeyCoord(keyCoord);
switch (mode.value) {
case ODONTOGRAM_MODE_DEFAULT:
case ODONTOGRAM_MODE_AMF:
case ODONTOGRAM_MODE_COF:
case ODONTOGRAM_MODE_FIS:
case ODONTOGRAM_MODE_CARIES:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
const hoverGeoms = getHoverShapeOnTeeth(mouse, teeth);
const convertedHoverGeoms = hoverGeoms.map((geom) =>
convertGeom(geom, mode.value)
);
odontogramInstance.value.hoverGeoms =
odontogramInstance.value.hoverGeoms.concat(convertedHoverGeoms);
}
break;
case ODONTOGRAM_MODE_NVT:
case ODONTOGRAM_MODE_RCT:
case ODONTOGRAM_MODE_NON:
case ODONTOGRAM_MODE_UNE:
case ODONTOGRAM_MODE_PRE:
case ODONTOGRAM_MODE_ANO:
case ODONTOGRAM_MODE_CFR:
case ODONTOGRAM_MODE_FMC:
case ODONTOGRAM_MODE_POC:
case ODONTOGRAM_MODE_RRX:
case ODONTOGRAM_MODE_MIS:
case ODONTOGRAM_MODE_IPX:
case ODONTOGRAM_MODE_FRM_ACR:
case ODONTOGRAM_MODE_HAPUS:
case ODONTOGRAM_MODE_ARROW_TOP_LEFT:
case ODONTOGRAM_MODE_ARROW_TOP_RIGHT:
case ODONTOGRAM_MODE_ARROW_TOP_TURN_LEFT:
case ODONTOGRAM_MODE_ARROW_TOP_TURN_RIGHT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_LEFT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_RIGHT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_LEFT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_RIGHT:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
// if (mode.value === ODONTOGRAM_MODE_IPX) {
// console.log("IPX hover detected on tooth:", teeth.num);
// }
// For IPX mode, determine surface from mouse position
let surfaceOptions = {};
if (
mode.value === ODONTOGRAM_MODE_IPX ||
mode.value === ODONTOGRAM_MODE_FRM_ACR ||
mode.value === ODONTOGRAM_MODE_ANO ||
mode.value === ODONTOGRAM_MODE_UNE ||
mode.value === ODONTOGRAM_MODE_PRE ||
mode.value === ODONTOGRAM_MODE_NON ||
mode.value === ODONTOGRAM_MODE_NVT ||
mode.value === ODONTOGRAM_MODE_RCT
) {
const hoverShapes = getHoverShapeOnTeeth(mouse, teeth);
if (hoverShapes.length > 0) {
// Get the first shape's name as surface
const surfaceName = hoverShapes[0].name.charAt(0).toUpperCase();
surfaceOptions = { surface: surfaceName };
}
}
// Determine row based on tooth position
let row = 0;
if (teeth.num) {
const toothNum = parseInt(teeth.num);
if (!isNaN(toothNum)) {
if (toothNum >= 11 && toothNum <= 18) {
row = 1; // Upper right
} else if (toothNum >= 21 && toothNum <= 28) {
row = 2; // Upper left
} else if (toothNum >= 31 && toothNum <= 38) {
row = 4; // Lower left
} else if (toothNum >= 41 && toothNum <= 48) {
row = 3; // Lower right
} else if (toothNum >= 51 && toothNum <= 55) {
row = 1; // Upper right (deciduous)
} else if (toothNum >= 61 && toothNum <= 65) {
row = 2; // Upper left (deciduous)
} else if (toothNum >= 71 && toothNum <= 75) {
row = 4; // Lower left (deciduous)
} else if (toothNum >= 81 && toothNum <= 85) {
row = 3; // Lower right (deciduous)
}
}
}
odontogramInstance.value.hoverGeoms.push(
convertGeom(
{
vertices: [
{ x: coord.x1, y: coord.y1 },
{ x: coord.x2, y: coord.y2 }
],
pos: teeth.num,
options: { ...surfaceOptions, row, pos: teeth.num }
},
mode.value
)
);
// Add hover shapes for sub-positions (top, right, bottom, left, middle)
}
break;
case ODONTOGRAM_MODE_BRIDGE:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
// For BRIDGE mode, determine surface from mouse position
let surfaceOptions = {};
const hoverShapes = getHoverShapeOnTeeth(mouse, teeth);
if (hoverShapes.length > 0) {
// Get the first shape's name as surface
const surfaceName = hoverShapes[0].name.charAt(0).toUpperCase();
surfaceOptions = { surface: surfaceName };
}
// Determine row based on tooth position
let row = 0;
if (teeth.num) {
const toothNum = parseInt(teeth.num);
if (!isNaN(toothNum)) {
if (toothNum >= 11 && toothNum <= 18) {
row = 1; // Upper right
} else if (toothNum >= 21 && toothNum <= 28) {
row = 2; // Upper left
} else if (toothNum >= 31 && toothNum <= 38) {
row = 4; // Lower left
} else if (toothNum >= 41 && toothNum <= 48) {
row = 3; // Lower right
} else if (toothNum >= 51 && toothNum <= 55) {
row = 1; // Upper right (deciduous)
} else if (toothNum >= 61 && toothNum <= 65) {
row = 2; // Upper left (deciduous)
} else if (toothNum >= 71 && toothNum <= 75) {
row = 4; // Lower left (deciduous)
} else if (toothNum >= 81 && toothNum <= 85) {
row = 3; // Lower right (deciduous)
}
}
}
const hoverGeoms = [
{ x: coord.x1, y: coord.y1 },
{ x: coord.x2, y: coord.y2 }
];
if (odontogramInstance.value.active_geometry) {
odontogramInstance.value.hoverGeoms = [
new BRIDGE(
odontogramInstance.value.active_geometry.startVert,
hoverGeoms,
{ ...surfaceOptions, row, pos: teeth.num }
)
];
} else {
odontogramInstance.value.hoverGeoms = [
new BRIDGE(hoverGeoms, hoverGeoms, {
...surfaceOptions,
row,
pos: teeth.num
})
];
}
}
break;
default:
break;
}
}
if (odontogramInstance.value.hoverGeoms.length > 0) {
odontogramInstance.value.canvas.style.cursor =
mode.value === ODONTOGRAM_MODE_HAPUS ? "pointer" : "pointer";
// mode.value === ODONTOGRAM_MODE_HAPUS ? "default" : "default";
} else {
odontogramInstance.value.canvas.style.cursor = "default";
}
odontogramInstance.value.redraw();
}
function parseKeyCoord(key: string) {
const keyChunks = key.split(";");
let x1 = 0,
y1 = 0,
x2 = 0,
y2 = 0,
cx = 0,
cy = 0;
for (let i = 0; i < 3; i++) {
const temp = keyChunks[i].split(":");
if (i === 0) {
x1 = parseFloat(temp[0]);
y1 = parseFloat(temp[1]);
} else if (i === 1) {
x2 = parseFloat(temp[0]);
y2 = parseFloat(temp[1]);
} else {
cx = parseFloat(temp[0]);
cy = parseFloat(temp[1]);
}
}
return { x1, y1, x2, y2, cx, cy };
}
function isRectIntersect(rectA: any, rectB: any) {
return (
rectA.x1 < rectB.x2 &&
rectA.x2 > rectB.x1 &&
rectA.y1 < rectB.y2 &&
rectA.y2 > rectB.y1
);
}
function getHoverShapeOnTeeth(mouse: any, teeth: any) {
const geoms = [];
for (const key in teeth) {
switch (key) {
case "middle":
case "top":
case "bottom":
case "left":
case "right":
if (isPolyIntersect(teeth[key], mouse)) {
geoms.push({ name: key, coord: teeth[key] });
}
break;
}
}
const polygonOpt = {
fillStyle: "rgba(55, 55, 55, 0.2)"
};
const polygons = [];
for (let i = 0; i < geoms.length; i++) {
const vertices = [];
for (const key in geoms[i].coord) {
vertices.push(geoms[i].coord[key]);
}
const pol = new Polygon(vertices, polygonOpt);
pol.name = geoms[i].name;
polygons.push(pol);
}
return polygons;
}
function isPolyIntersect(polygon: any, point: any) {
const { x, y } = point;
const vertices = Object.values(polygon) as { x: number; y: number }[];
let intersectCount = 0;
for (let i = 0; i < vertices.length; i++) {
const v1 = vertices[i];
const v2 = vertices[(i + 1) % vertices.length];
if (v1.y > y !== v2.y > y) {
const intersectionX = v1.x + ((y - v1.y) * (v2.x - v1.x)) / (v2.y - v1.y);
if (x < intersectionX) {
intersectCount++;
}
}
}
return intersectCount % 2 !== 0;
}
function joinShapeTeeth(geoms1: any, geoms2: any) {
const geometry = JSON.parse(JSON.stringify(geoms1));
let geom1, geom2;
for (const keyCoord in geoms2) {
geom1 = geoms1[keyCoord];
geom2 = geoms2[keyCoord];
if (geom1 == null) {
geometry[keyCoord] = geom2;
} else {
geometry[keyCoord] = _joinShapeTeeth(geom1, geom2);
}
}
return geometry;
}
function _joinShapeTeeth(geoms1: any, geoms2: any) {
let geometry: any[] = [];
for (const geom2 of geoms2) {
geometry = [geom2];
for (const geom1 of geoms1) {
switch (true) {
case geom2 instanceof AMF:
if (geom1 instanceof AMF || geom1 instanceof RCT)
geometry.push(geom1);
break;
case geom2 instanceof COF:
if (geom1 instanceof COF || geom1 instanceof RCT)
geometry.push(geom1);
break;
case geom2 instanceof FIS:
if (geom1 instanceof FIS) geometry.push(geom1);
break;
case geom2 instanceof NVT:
if (geom1 instanceof NVT) geometry.push(geom1);
break;
case geom2 instanceof RCT:
if (
geom1 instanceof AMF ||
geom1 instanceof COF ||
geom1 instanceof POC ||
geom1 instanceof FMC ||
geom1 instanceof BRIDGE
)
geometry.push(geom1);
break;
case geom2 instanceof NON:
if (geom1 instanceof NON) geometry.push(geom1);
break;
case geom2 instanceof UNE:
if (geom1 instanceof UNE) geometry.push(geom1);
break;
case geom2 instanceof PRE:
if (geom1 instanceof PRE) geometry.push(geom1);
break;
case geom2 instanceof ANO:
if (geom1 instanceof ANO) geometry.push(geom1);
break;
case geom2 instanceof CARIES:
if (geom1 instanceof CARIES) geometry.push(geom1);
break;
case geom2 instanceof CFR:
break;
case geom2 instanceof FMC:
if (
geom1 instanceof RCT ||
geom1 instanceof MIS ||
geom1 instanceof BRIDGE
)
geometry.push(geom1);
break;
case geom2 instanceof POC:
if (
geom1 instanceof POC ||
geom1 instanceof IPX ||
geom1 instanceof RCT ||
geom1 instanceof MIS ||
geom1 instanceof BRIDGE
)
geometry.push(geom1);
break;
case geom2 instanceof RRX:
break;
case geom2 instanceof MIS:
if (
geom1 instanceof POC ||
geom1 instanceof FMC ||
geom1 instanceof FRM_ACR ||
geom1 instanceof BRIDGE
)
geometry.push(geom1);
break;
case geom2 instanceof IPX:
if (geom1 instanceof POC || geom1 instanceof BRIDGE)
geometry.push(geom1);
break;
case geom2 instanceof FRM_ACR:
if (geom1 instanceof MIS || geom1 instanceof BRIDGE)
geometry.push(geom1);
break;
case geom2 instanceof BRIDGE:
if (
geom1 instanceof POC ||
geom1 instanceof FMC ||
geom1 instanceof FRM_ACR ||
geom1 instanceof RCT ||
geom1 instanceof MIS ||
geom1 instanceof IPX
)
geometry.push(geom1);
break;
default:
console.log("DEFAULT[POLYGON]");
break;
}
}
}
return geometry;
}
export function useOdontogram() {
const store = useOdontogramStore();
// Helper function to get all teeth keys in bridge range
function getTeethKeysInRange(startVert: any[], endVert: any[]) {
if (!odontogramInstance.value) return [];
const teethKeys = Object.keys(odontogramInstance.value.teeth);
teethKeys.sort((a, b) => {
const aCoord = parseKeyCoord(a);
const bCoord = parseKeyCoord(b);
return aCoord.x1 - bCoord.x1;
});
const startIndex = teethKeys.findIndex((key) => {
const coord = parseKeyCoord(key);
return (
coord.x1 === startVert[0].x &&
coord.y1 === startVert[0].y &&
coord.x2 === startVert[1].x &&
coord.y2 === startVert[1].y
);
});
const endIndex = teethKeys.findIndex((key) => {
const coord = parseKeyCoord(key);
return (
coord.x1 === endVert[0].x &&
coord.y1 === endVert[0].y &&
coord.x2 === endVert[1].x &&
coord.y2 === endVert[1].y
);
});
if (startIndex === -1 || endIndex === -1) return [];
return startIndex < endIndex
? teethKeys.slice(startIndex, endIndex + 1)
: teethKeys.slice(endIndex, startIndex + 1);
}
// Sync store conditions to odontogramInstance geometry and redraw
function syncGeometryFromStore() {
if (!odontogramInstance.value) return;
const newGeometry: Record<string, any[]> = {};
function getPolygonsForSurface(mouse: any, teeth: any, surfaceKey: string) {
const polygons = [];
for (const key in teeth) {
if (key === surfaceKey) {
const polygonOpt = {
fillStyle: "rgba(55, 55, 55, 0.2)"
};
const vertices = [];
for (const vertexKey in teeth[key]) {
vertices.push(teeth[key][vertexKey]);
}
const pol = new Polygon(vertices, polygonOpt);
pol.name = key;
polygons.push(pol);
}
}
return polygons;
}
// Map store conditions to geometry for all modes including bridge
for (const condition of store.conditions) {
// Find tooth key by toothNumber
const toothKey = Object.keys(odontogramInstance.value?.teeth).find(
(key) =>
odontogramInstance.value?.teeth[key].num === condition.toothNumber
);
if (!toothKey) continue;
const teeth = odontogramInstance.value.teeth[toothKey];
// Handle BRIDGE mode separately
if (condition.mode === ODONTOGRAM_MODE_BRIDGE) {
// For bridge conditions, we need to find the corresponding start/finish pair
// and create a single bridge geometry that spans between them
// Find all bridge conditions with the same group number
const bridgeGroup = store.conditions.filter(
(c) =>
c.mode === ODONTOGRAM_MODE_BRIDGE && c.group === condition.group
);
// Need at least two conditions (start and finish) to create a bridge
if (bridgeGroup.length < 2) continue;
// Find start and finish conditions in the group
const startCondition = bridgeGroup.find(
(c) => c.position && c.position.endsWith("start")
);
const finishCondition = bridgeGroup.find(
(c) => c.position && c.position.endsWith("finish")
);
if (!startCondition || !finishCondition) continue;
// Find tooth keys for start and finish conditions
const startToothKey = Object.keys(odontogramInstance.value.teeth).find(
(key) =>
odontogramInstance.value!.teeth[key].num ===
startCondition.toothNumber
);
const finishToothKey = Object.keys(odontogramInstance.value.teeth).find(
(key) =>
odontogramInstance.value!.teeth[key].num ===
finishCondition.toothNumber
);
if (!startToothKey || !finishToothKey) continue;
// Get teeth objects
const startTeeth = odontogramInstance.value.teeth[startToothKey];
const finishTeeth = odontogramInstance.value.teeth[finishToothKey];
// Determine row based on tooth position
let row = 0;
if (teeth.num) {
const toothNum = parseInt(teeth.num);
if (!isNaN(toothNum)) {
if (toothNum >= 11 && toothNum <= 18) {
row = 1; // Upper right
} else if (toothNum >= 21 && toothNum <= 28) {
row = 2; // Upper left
} else if (toothNum >= 31 && toothNum <= 38) {
row = 4; // Lower left
} else if (toothNum >= 41 && toothNum <= 48) {
row = 3; // Lower right
} else if (toothNum >= 51 && toothNum <= 55) {
row = 1; // Upper right (deciduous)
} else if (toothNum >= 61 && toothNum <= 65) {
row = 2; // Upper left (deciduous)
} else if (toothNum >= 71 && toothNum <= 75) {
row = 4; // Lower left (deciduous)
} else if (toothNum >= 81 && toothNum <= 85) {
row = 3; // Lower right (deciduous)
}
}
}
// Create start and end vertices
const startVert = [
{ x: startTeeth.x1, y: startTeeth.y1 },
{ x: startTeeth.x2, y: startTeeth.y2 }
];
const endVert = [
{ x: finishTeeth.x1, y: finishTeeth.y1 },
{ x: finishTeeth.x2, y: finishTeeth.y2 }
];
// Create bridge geometry
const bridgeGeom = convertGeom(
{
startVert,
endVert,
pos: `bridge-${condition.group}`,
options: { row, pos: teeth.num }
},
ODONTOGRAM_MODE_BRIDGE
);
// Assign bridge geometry to both start and finish teeth
if (!newGeometry[startToothKey]) {
newGeometry[startToothKey] = [];
}
// Only add the bridge geometry once per tooth
if (
!newGeometry[startToothKey].some(
(g) =>
g.mode === ODONTOGRAM_MODE_BRIDGE &&
g.pos === `bridge-${condition.group}`
)
) {
// Insert at the beginning for start tooth key
newGeometry[startToothKey].unshift(bridgeGeom);
}
if (!newGeometry[finishToothKey]) {
newGeometry[finishToothKey] = [];
}
// Only add the bridge geometry once per tooth
if (
!newGeometry[finishToothKey].some(
(g) =>
g.mode === ODONTOGRAM_MODE_BRIDGE &&
g.pos === `bridge-${condition.group}`
)
) {
// Insert at the end for finish tooth key (default push)
newGeometry[finishToothKey].push(bridgeGeom);
}
continue;
}
// Determine surface key from condition.surface or from pos if surface missing or invalid
const surfaceMap: Record<string, string> = {
T: "top",
R: "right",
B: "bottom",
L: "left",
M: "middle"
};
let surfaceKey = "middle";
if (
condition.surface &&
["T", "R", "B", "L", "M"].includes(condition.surface)
) {
surfaceKey = surfaceMap[condition.surface];
} else if (condition.position && typeof condition.position === "string") {
// Try to infer surface from position string (pos)
const parts = condition.position.split("-");
if (parts.length > 1) {
const surf = parts[1].toUpperCase();
if (["T", "R", "B", "L", "M"].includes(surf)) {
surfaceKey = surfaceMap[surf];
}
}
}
// For AMF, COF, FIS, CARIES, use polygons for surface
if (
condition.mode === 1 || // AMF
condition.mode === 2 || // COF
condition.mode === 3 || // FIS
condition.mode === 10 // CARIES
) {
const polygons = getPolygonsForSurface(null, teeth, surfaceKey);
if (polygons.length > 0) {
if (!newGeometry[toothKey]) {
newGeometry[toothKey] = [];
}
for (const pol of polygons) {
pol.pos =
condition.toothNumber + "-" + pol.name.charAt(0).toUpperCase();
const geom = convertGeom(pol, condition.mode);
newGeometry[toothKey].push(geom);
}
continue;
}
}
// Determine row based on tooth position (for NVT and RCT modes)
let row = 0;
if (teeth.num) {
const toothNum = parseInt(teeth.num);
if (!isNaN(toothNum)) {
if (toothNum >= 11 && toothNum <= 18) {
row = 1; // Upper right
} else if (toothNum >= 21 && toothNum <= 28) {
row = 2; // Upper left
} else if (toothNum >= 31 && toothNum <= 38) {
row = 4; // Lower left
} else if (toothNum >= 41 && toothNum <= 48) {
row = 3; // Lower right
} else if (toothNum >= 51 && toothNum <= 55) {
row = 1; // Upper right (deciduous)
} else if (toothNum >= 61 && toothNum <= 65) {
row = 2; // Upper left (deciduous)
} else if (toothNum >= 71 && toothNum <= 75) {
row = 4; // Lower left (deciduous)
} else if (toothNum >= 81 && toothNum <= 85) {
row = 3; // Lower right (deciduous)
}
}
}
// Fallback: use tooth rectangle vertices and pos as tooth number with options for row
const coord = odontogramInstance.value.teeth[toothKey];
if (!newGeometry[toothKey]) {
newGeometry[toothKey] = [];
}
if (coord) {
const geom = convertGeom(
{
vertices: [
{ x: coord.x1, y: coord.y1 },
{ x: coord.x2, y: coord.y2 }
],
pos: condition.toothNumber,
options: { row, pos: condition.toothNumber }
},
condition.mode
);
newGeometry[toothKey].push(geom);
}
}
odontogramInstance.value.geometry = newGeometry;
odontogramInstance.value.redraw();
}
function clearAll() {
if (odontogramInstance.value) {
odontogramInstance.value.geometry = {};
Object.keys(geometry).forEach((key) => {
delete geometry[key];
});
odontogramInstance.value._drawBackground();
}
store.clearAllConditions();
}
function setMode(newMode: number) {
mode.value = newMode;
if (odontogramInstance.value) {
odontogramInstance.value.setMode(newMode);
}
store.setMode(newMode);
}
// Watch store conditions and sync geometry on change
watch(
() => store.conditions,
() => {
syncGeometryFromStore();
},
{ deep: true, immediate: true }
);
// Watch store currentMode and update mode and odontogramInstance mode
// watch(
// () => store.currentMode,
// (newMode) => {
// setMode(newMode);
// },
// { immediate: true }
// );
function onMouseClick(
event: MouseEvent,
emit?: (event: string, data: any) => void
) {
// console.log("onMouseClick called with mode:", mode.value);
if (!odontogramInstance.value || !odontogramInstance.value.canvas) return;
if (mode.value === ODONTOGRAM_MODE_DEFAULT) return;
const canvas = odontogramInstance.value.canvas;
const rect = canvas.getBoundingClientRect();
// Calculate scale factors for X and Y
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// Adjust mouse coordinates to canvas internal resolution
const mouse = {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY
};
const tempGeoms: Record<string, any[]> = {};
for (const keyCoord in odontogramInstance.value.teeth) {
const teeth = odontogramInstance.value.teeth[keyCoord];
const coord = parseKeyCoord(keyCoord);
// Check if tooth already has MIS or RRX condition, block other mode changes
const existingGeoms = odontogramInstance.value.geometry[keyCoord] || [];
const hasMIS = existingGeoms.some((geom: any) => geom instanceof MIS);
const hasRRX = existingGeoms.some((geom: any) => geom instanceof RRX);
if (hasRRX) {
// RRX can only be changed to MIS or HAPUS
if (
mode.value !== ODONTOGRAM_MODE_MIS &&
mode.value !== ODONTOGRAM_MODE_HAPUS
) {
continue; // Skip this tooth, do not apply other modes
}
} else if (hasMIS) {
// MIS can only be changed to HAPUS
if (mode.value !== ODONTOGRAM_MODE_HAPUS) {
continue; // Skip this tooth, do not apply other modes
}
}
switch (mode.value) {
case ODONTOGRAM_MODE_AMF:
case ODONTOGRAM_MODE_COF:
case ODONTOGRAM_MODE_FIS:
case ODONTOGRAM_MODE_CARIES:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
if (!tempGeoms[keyCoord]) {
tempGeoms[keyCoord] = [];
}
// Use getHoverShapeOnTeeth to get sub-position polygons and assign pos accordingly
const hoverShapes = getHoverShapeOnTeeth(mouse, teeth);
if (hoverShapes.length > 0) {
for (const shape of hoverShapes) {
if (!shape.pos) {
shape.pos =
teeth.num + "-" + shape.name.charAt(0).toUpperCase();
}
tempGeoms[keyCoord].push(convertGeom(shape, mode.value));
}
} else {
// Fallback to tooth number only if no sub-position found
tempGeoms[keyCoord].push(
convertGeom(
{
vertices: [
{ x: coord.x1, y: coord.y1 },
{ x: coord.x2, y: coord.y2 }
],
pos: teeth.num + "-M" // Default to middle if no sub-position
},
mode.value
)
);
}
}
break;
case ODONTOGRAM_MODE_NVT:
case ODONTOGRAM_MODE_RCT:
case ODONTOGRAM_MODE_NON:
case ODONTOGRAM_MODE_UNE:
case ODONTOGRAM_MODE_PRE:
case ODONTOGRAM_MODE_ANO:
case ODONTOGRAM_MODE_CFR:
case ODONTOGRAM_MODE_FMC:
case ODONTOGRAM_MODE_POC:
case ODONTOGRAM_MODE_RRX:
case ODONTOGRAM_MODE_MIS:
case ODONTOGRAM_MODE_IPX:
case ODONTOGRAM_MODE_FRM_ACR:
case ODONTOGRAM_MODE_ARROW_TOP_LEFT:
case ODONTOGRAM_MODE_ARROW_TOP_RIGHT:
case ODONTOGRAM_MODE_ARROW_TOP_TURN_LEFT:
case ODONTOGRAM_MODE_ARROW_TOP_TURN_RIGHT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_LEFT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_RIGHT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_LEFT:
case ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_RIGHT:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
if (!tempGeoms[keyCoord]) {
tempGeoms[keyCoord] = [];
}
// For IPX mode, determine surface from mouse position
let surfaceOptions = {};
if (
mode.value === ODONTOGRAM_MODE_IPX ||
mode.value === ODONTOGRAM_MODE_FRM_ACR ||
mode.value === ODONTOGRAM_MODE_ANO ||
mode.value === ODONTOGRAM_MODE_UNE ||
mode.value === ODONTOGRAM_MODE_PRE ||
mode.value === ODONTOGRAM_MODE_NON ||
mode.value === ODONTOGRAM_MODE_NVT ||
mode.value === ODONTOGRAM_MODE_RCT
) {
const hoverShapes = getHoverShapeOnTeeth(mouse, teeth);
if (hoverShapes.length > 0) {
// Get the first shape's name as surface
const surfaceName = hoverShapes[0].name.charAt(0).toUpperCase();
surfaceOptions = { surface: surfaceName };
}
}
// Determine row based on tooth position
let row = 0;
if (teeth.num) {
const toothNum = parseInt(teeth.num);
if (!isNaN(toothNum)) {
if (toothNum >= 11 && toothNum <= 18) {
row = 1; // Upper right
} else if (toothNum >= 21 && toothNum <= 28) {
row = 2; // Upper left
} else if (toothNum >= 31 && toothNum <= 38) {
row = 4; // Lower left
} else if (toothNum >= 41 && toothNum <= 48) {
row = 3; // Lower right
} else if (toothNum >= 51 && toothNum <= 55) {
row = 1; // Upper right (deciduous)
} else if (toothNum >= 61 && toothNum <= 65) {
row = 2; // Upper left (deciduous)
} else if (toothNum >= 71 && toothNum <= 75) {
row = 4; // Lower left (deciduous)
} else if (toothNum >= 81 && toothNum <= 85) {
row = 3; // Lower right (deciduous)
}
}
}
tempGeoms[keyCoord].push(
convertGeom(
{
vertices: [
{ x: coord.x1, y: coord.y1 },
{ x: coord.x2, y: coord.y2 }
],
pos: teeth.num,
options: { ...surfaceOptions, row, pos: teeth.num }
},
mode.value
)
);
}
break;
case ODONTOGRAM_MODE_HAPUS:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
tempGeoms[keyCoord] = [];
}
break;
case ODONTOGRAM_MODE_BRIDGE:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
// For BRIDGE mode, determine surface from mouse position
let surfaceOptions = {};
const hoverShapes = getHoverShapeOnTeeth(mouse, teeth);
if (hoverShapes.length > 0) {
// Get the first shape's name as surface
const surfaceName = hoverShapes[0].name.charAt(0).toUpperCase();
surfaceOptions = { surface: surfaceName };
}
// Determine row based on tooth position
let row = 0;
if (teeth.num) {
const toothNum = parseInt(teeth.num);
if (!isNaN(toothNum)) {
if (toothNum >= 11 && toothNum <= 18) {
row = 1; // Upper right
} else if (toothNum >= 21 && toothNum <= 28) {
row = 2; // Upper left
} else if (toothNum >= 31 && toothNum <= 38) {
row = 4; // Lower left
} else if (toothNum >= 41 && toothNum <= 48) {
row = 3; // Lower right
} else if (toothNum >= 51 && toothNum <= 55) {
row = 1; // Upper right (deciduous)
} else if (toothNum >= 61 && toothNum <= 65) {
row = 2; // Upper left (deciduous)
} else if (toothNum >= 71 && toothNum <= 75) {
row = 4; // Lower left (deciduous)
} else if (toothNum >= 81 && toothNum <= 85) {
row = 3; // Lower right (deciduous)
}
}
}
if (!tempGeoms[keyCoord]) {
tempGeoms[keyCoord] = [];
}
if (odontogramInstance.value.active_geometry) {
// Multi-tooth bridge logic
const startVert =
odontogramInstance.value.active_geometry.startVert;
const endVert = [
{ x: coord.x1, y: coord.y1 },
{ x: coord.x2, y: coord.y2 }
];
// Add row info to startVert and endVert for row awareness
const startRow = Math.floor(startVert[0].y / 150); // Approximate row height
const endRow = Math.floor(endVert[0].y / 150);
// Filter teeth keys to only those in the same row as startVert and endVert
const teethKeys = Object.keys(
odontogramInstance.value.teeth
).filter((key) => {
const coord = parseKeyCoord(key);
const row = Math.floor(coord.y1 / 150);
return row === startRow && row === endRow;
});
// Sort keys by x1 coordinate ascending within the row
teethKeys.sort((a, b) => {
const aCoord = parseKeyCoord(a);
const bCoord = parseKeyCoord(b);
return aCoord.x1 - bCoord.x1;
});
// Find indices of start and end teeth within filtered keys
const startIndex = teethKeys.findIndex((key) => {
const coord = parseKeyCoord(key);
return (
coord.x1 === startVert[0].x &&
coord.y1 === startVert[0].y &&
coord.x2 === startVert[1].x &&
coord.y2 === startVert[1].y
);
});
const endIndex = teethKeys.findIndex((key) => {
const coord = parseKeyCoord(key);
return (
coord.x1 === endVert[0].x &&
coord.y1 === endVert[0].y &&
coord.x2 === endVert[1].x &&
coord.y2 === endVert[1].y
);
});
if (startIndex === -1 || endIndex === -1) {
// Fallback to single bridge if indices not found
odontogramInstance.value.active_geometry = convertGeom(
{
startVert,
endVert,
options: { ...surfaceOptions, row, pos: teeth.num }
},
mode.value
);
tempGeoms[keyCoord].push(
odontogramInstance.value.active_geometry
);
} else {
// Get range between start and end indices
const [from, to] =
startIndex < endIndex
? [startIndex, endIndex]
: [endIndex, startIndex];
// Collect vertices for all teeth in range
const bridgeStartVert = parseKeyCoord(teethKeys[from]);
const bridgeEndVert = parseKeyCoord(teethKeys[to]);
odontogramInstance.value.active_geometry = convertGeom(
{
startVert: [
{ x: bridgeStartVert.x1, y: bridgeStartVert.y1 },
{ x: bridgeStartVert.x2, y: bridgeStartVert.y2 }
],
endVert: [
{ x: bridgeEndVert.x1, y: bridgeEndVert.y1 },
{ x: bridgeEndVert.x2, y: bridgeEndVert.y2 }
],
pos: teeth.num, // Set pos to tooth number for bridge geometry
options: { ...surfaceOptions, row, pos: teeth.num }
},
mode.value
);
// Clear individual teeth geometries in the range
for (let i = from; i <= to; i++) {
if (!tempGeoms[teethKeys[i]]) {
tempGeoms[teethKeys[i]] = [];
}
}
// Assign the multi-tooth bridge geometry to all teeth in range
for (let i = from; i <= to; i++) {
tempGeoms[teethKeys[i]].push(
odontogramInstance.value.active_geometry
);
}
}
odontogramInstance.value.active_geometry = null;
} else {
odontogramInstance.value.active_geometry = {
startVert: [
{ x: coord.x1, y: coord.y1 },
{ x: coord.x2, y: coord.y2 }
]
};
}
}
break;
default:
if (
isRectIntersect(coord, {
x1: mouse.x,
y1: mouse.y,
x2: mouse.x,
y2: mouse.y
})
) {
if (!tempGeoms[keyCoord]) {
tempGeoms[keyCoord] = [];
}
const temp = getHoverShapeOnTeeth(mouse, teeth);
for (let i = 0; i < temp.length; i++) {
temp[i].pos =
teeth.num + "-" + temp[i].name.charAt(0).toUpperCase();
tempGeoms[keyCoord].push(convertGeom(temp[i], mode.value));
}
}
break;
}
}
if (mode.value === ODONTOGRAM_MODE_HAPUS) {
for (const keyCoord in tempGeoms) {
odontogramInstance.value.geometry[keyCoord] = [];
geometry[keyCoord] = [];
}
} else {
const newGeometry = { ...odontogramInstance.value.geometry };
for (const key in tempGeoms) {
// If mode is MIS or RRX, replace all existing geometries on the tooth
if (
mode.value === ODONTOGRAM_MODE_MIS ||
mode.value === ODONTOGRAM_MODE_RRX
) {
newGeometry[key] = tempGeoms[key];
} else {
if (newGeometry[key]) {
newGeometry[key] = [...newGeometry[key], ...tempGeoms[key]];
} else {
newGeometry[key] = tempGeoms[key];
}
}
geometry[key] = newGeometry[key];
}
odontogramInstance.value.geometry = newGeometry;
}
// Emit geometry change event
if (emit) {
emit("update:geometry", geometry);
}
// Update store conditions based on current geometry
const newConditions: import("~/types/apps/medical/odontogram").ToothCondition[] =
[];
// Collect all bridge geometries separately
const bridgeGeometries: any[] = [];
for (const key in odontogramInstance.value.geometry) {
const geoms = odontogramInstance.value.geometry[key];
for (const geom of geoms) {
if (geom && geom.mode === ODONTOGRAM_MODE_BRIDGE) {
bridgeGeometries.push({ key, geom });
}
}
}
// Group bridge geometries by distinct startVert and endVert to identify separate bridges
const groupedBridges: {
startVert: any[];
endVert: any[];
keys: string[];
}[] = [];
for (const { key, geom } of bridgeGeometries) {
let foundGroup = false;
for (const group of groupedBridges) {
if (
JSON.stringify(group.startVert) === JSON.stringify(geom.startVert) &&
JSON.stringify(group.endVert) === JSON.stringify(geom.endVert)
) {
group.keys.push(key);
foundGroup = true;
break;
}
}
if (!foundGroup) {
groupedBridges.push({
startVert: geom.startVert,
endVert: geom.endVert,
keys: [key]
});
}
}
// For each bridge group, assign start and finish positions with group numbers
let groupCounter = 1;
for (const group of groupedBridges) {
const keys = group.keys;
// Sort keys by x1 and y1 coordinate ascending to distinguish rows
keys.sort((a, b) => {
const aCoord = parseKeyCoord(a);
const bCoord = parseKeyCoord(b);
if (aCoord.y1 !== bCoord.y1) {
return aCoord.y1 - bCoord.y1;
}
return aCoord.x1 - bCoord.x1;
});
// Create conditions for start and finish positions with group numbers
if (keys.length >= 2) {
// First key (start)
const startKey = keys[0];
const startTeeth = odontogramInstance.value.teeth[startKey];
if (startTeeth) {
const startToothNumber = startTeeth.num;
let startPosition = startToothNumber + "-start";
// Check if this condition already exists
const startExists = newConditions.some(
(cond) =>
cond.toothNumber === startToothNumber &&
cond.position === startPosition
);
if (!startExists) {
const startCondition = {
toothNumber: startToothNumber,
surface: "M" as "T" | "R" | "B" | "L" | "M",
mode: ODONTOGRAM_MODE_BRIDGE,
position: startPosition,
group: groupCounter
};
newConditions.push(startCondition);
}
}
// Last key (finish)
const finishKey = keys[keys.length - 1];
const finishTeeth = odontogramInstance.value.teeth[finishKey];
if (finishTeeth) {
const finishToothNumber = finishTeeth.num;
let finishPosition = finishToothNumber + "-finish";
// Check if this condition already exists
const finishExists = newConditions.some(
(cond) =>
cond.toothNumber === finishToothNumber &&
cond.position === finishPosition
);
if (!finishExists) {
const finishCondition = {
toothNumber: finishToothNumber,
surface: "M" as "T" | "R" | "B" | "L" | "M",
mode: ODONTOGRAM_MODE_BRIDGE,
position: finishPosition,
group: groupCounter
};
newConditions.push(finishCondition);
}
}
groupCounter++;
} else if (keys.length === 1) {
// Single tooth bridge
const key = keys[0];
const teeth = odontogramInstance.value.teeth[key];
if (teeth) {
const toothNumber = teeth.num;
let position = toothNumber + "-start"; // Default to start for single tooth
// Check if this condition already exists
const exists = newConditions.some(
(cond) =>
cond.toothNumber === toothNumber && cond.position === position
);
if (!exists) {
const condition = {
toothNumber,
surface: "M" as "T" | "R" | "B" | "L" | "M",
mode: ODONTOGRAM_MODE_BRIDGE,
position,
group: groupCounter
};
newConditions.push(condition);
groupCounter++;
}
// Add bridge geometry for single tooth bridge
if (!geometry[key]) {
geometry[key] = [];
}
const startVert = [
{ x: teeth.x1, y: teeth.y1 },
{ x: teeth.x2, y: teeth.y2 }
];
const endVert = [
{ x: teeth.x1, y: teeth.y1 },
{ x: teeth.x2, y: teeth.y2 }
];
const bridgeGeom = convertGeom(
{
startVert,
endVert,
pos: toothNumber + "-start"
},
ODONTOGRAM_MODE_BRIDGE
);
geometry[key].push(bridgeGeom);
}
}
}
// Add non-bridge geometries as before
for (const key in odontogramInstance.value.geometry) {
const geoms = odontogramInstance.value.geometry[key];
const teeth = odontogramInstance.value.teeth[key];
if (!teeth) continue;
for (const geom of geoms) {
if (!geom || geom.mode === ODONTOGRAM_MODE_BRIDGE) continue;
const toothNumber = teeth.num;
let surface: "T" | "R" | "B" | "L" | "M" | undefined = undefined;
if (geom.pos && typeof geom.pos === "string") {
const parts = geom.pos.split("-");
if (parts.length > 1) {
const surf = parts[1].toUpperCase();
if (["T", "R", "B", "L", "M"].includes(surf)) {
surface = surf as "T" | "R" | "B" | "L" | "M";
}
} else {
surface = "M";
}
}
const position = typeof geom.pos === "string" ? geom.pos : "";
const condition = {
toothNumber,
surface,
mode: geom.mode !== undefined ? geom.mode : mode.value,
position
};
newConditions.push(condition);
}
}
// Additional log to verify all conditions
// console.log("DEBUG newConditions:", newConditions);
store.setConditions(newConditions);
odontogramInstance.value.redraw();
}
return {
odontogramInstance,
canvas,
mode,
geometry,
width,
height,
initialize,
setMode,
onMouseMove,
onMouseClick,
downloadImage,
joinShapeTeeth,
clearAll,
// Export mode constants
ODONTOGRAM_MODE_DEFAULT,
ODONTOGRAM_MODE_AMF,
ODONTOGRAM_MODE_COF,
ODONTOGRAM_MODE_FIS,
ODONTOGRAM_MODE_NVT,
ODONTOGRAM_MODE_RCT,
ODONTOGRAM_MODE_NON,
ODONTOGRAM_MODE_UNE,
ODONTOGRAM_MODE_PRE,
ODONTOGRAM_MODE_ANO,
ODONTOGRAM_MODE_CARIES,
ODONTOGRAM_MODE_CFR,
ODONTOGRAM_MODE_FMC,
ODONTOGRAM_MODE_POC,
ODONTOGRAM_MODE_RRX,
ODONTOGRAM_MODE_MIS,
ODONTOGRAM_MODE_IPX,
ODONTOGRAM_MODE_FRM_ACR,
ODONTOGRAM_MODE_BRIDGE,
ODONTOGRAM_MODE_ARROW_TOP_LEFT,
ODONTOGRAM_MODE_ARROW_TOP_RIGHT,
ODONTOGRAM_MODE_ARROW_BOTTOM_LEFT,
ODONTOGRAM_MODE_ARROW_BOTTOM_RIGHT,
ODONTOGRAM_MODE_ARROW_TOP_TURN_LEFT,
ODONTOGRAM_MODE_ARROW_TOP_TURN_RIGHT,
ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_LEFT,
ODONTOGRAM_MODE_ARROW_BOTTOM_TURN_RIGHT,
ODONTOGRAM_MODE_HAPUS
};
}