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 = {}; active_geometry: any = null; teeth: Record = {}; 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(null); const canvas = ref(null); const mode = ref(ODONTOGRAM_MODE_DEFAULT); const geometry = reactive>({}); 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 = {}; 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 = { 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 = {}; 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 }; }