2218 lines
67 KiB
TypeScript
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
|
|
};
|
|
}
|