diff --git a/components/apps/medical/Odontogram/OdontogramCanvas.vue b/components/apps/medical/Odontogram/OdontogramCanvas.vue deleted file mode 100644 index cdfcd45..0000000 --- a/components/apps/medical/Odontogram/OdontogramCanvas.vue +++ /dev/null @@ -1,257 +0,0 @@ - - - - - diff --git a/components/apps/medical/Odontogram/OdontogramToolbar.vue b/components/apps/medical/Odontogram/OdontogramToolbar.vue deleted file mode 100644 index b708b22..0000000 --- a/components/apps/medical/Odontogram/OdontogramToolbar.vue +++ /dev/null @@ -1,170 +0,0 @@ - - - - - diff --git a/components/apps/medical/Odontogram/ToothElement.vue b/components/apps/medical/Odontogram/ToothElement.vue deleted file mode 100644 index f8b38d2..0000000 --- a/components/apps/medical/Odontogram/ToothElement.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/components/apps/medical/UI/DataManager.vue b/components/apps/medical/UI/DataManager.vue deleted file mode 100644 index 0ef4bdc..0000000 --- a/components/apps/medical/UI/DataManager.vue +++ /dev/null @@ -1,223 +0,0 @@ - - - - - diff --git a/components/apps/patient/form/AlamatSection.vue b/components/apps/patient/form/AlamatSection.vue deleted file mode 100644 index ca3dcdc..0000000 --- a/components/apps/patient/form/AlamatSection.vue +++ /dev/null @@ -1,236 +0,0 @@ - - - diff --git a/components/apps/patient/form/DataDiriSection.vue b/components/apps/patient/form/DataDiriSection.vue deleted file mode 100644 index f5691bc..0000000 --- a/components/apps/patient/form/DataDiriSection.vue +++ /dev/null @@ -1,516 +0,0 @@ - - - - - diff --git a/components/apps/patient/form/KesehatanSection.vue b/components/apps/patient/form/KesehatanSection.vue deleted file mode 100644 index 3106465..0000000 --- a/components/apps/patient/form/KesehatanSection.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - diff --git a/components/apps/patient/form/PembayaranSection.vue b/components/apps/patient/form/PembayaranSection.vue deleted file mode 100644 index b5ed96f..0000000 --- a/components/apps/patient/form/PembayaranSection.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/components/apps/patient/form/PenanggungJawabSection.vue b/components/apps/patient/form/PenanggungJawabSection.vue deleted file mode 100644 index c031bd3..0000000 --- a/components/apps/patient/form/PenanggungJawabSection.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - diff --git a/components/apps/patient/form/SosialSection.vue b/components/apps/patient/form/SosialSection.vue deleted file mode 100644 index eb3ce8d..0000000 --- a/components/apps/patient/form/SosialSection.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - diff --git a/components/apps/registration/form/AppointmentSchedule.vue b/components/apps/registration/form/AppointmentSchedule.vue deleted file mode 100644 index 3f19dad..0000000 --- a/components/apps/registration/form/AppointmentSchedule.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/components/apps/registration/form/PatientData.vue b/components/apps/registration/form/PatientData.vue deleted file mode 100644 index 156edf5..0000000 --- a/components/apps/registration/form/PatientData.vue +++ /dev/null @@ -1,249 +0,0 @@ - - - diff --git a/components/apps/registration/form/RegistrationType.vue b/components/apps/registration/form/RegistrationType.vue deleted file mode 100644 index 92ee36e..0000000 --- a/components/apps/registration/form/RegistrationType.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/components/apps/registration/form/RiskAssessment.vue b/components/apps/registration/form/RiskAssessment.vue deleted file mode 100644 index 3b56d89..0000000 --- a/components/apps/registration/form/RiskAssessment.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - diff --git a/components/content/MenuTreeItem.vue b/components/content/MenuTreeItem.vue deleted file mode 100644 index 3f8b0ba..0000000 --- a/components/content/MenuTreeItem.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/components/layout/full/vertical-sidebar/MinIconItems.ts b/components/layout/full/vertical-sidebar/MinIconItems.ts index 7db4714..8e9cb6a 100644 --- a/components/layout/full/vertical-sidebar/MinIconItems.ts +++ b/components/layout/full/vertical-sidebar/MinIconItems.ts @@ -11,14 +11,14 @@ const MiniSideIcons: minisidebar[] = [ id: 1 }, { - icon: 'layers-line-duotone', - tooltip:'Master', - id: 2, + icon: 'palette-round-line-duotone', + tooltip:'UI Elements', + id: 3 }, { - icon: 'palette-round-line-duotone', - tooltip:'Forms', - id: 3 + icon: 'layers-line-duotone', + tooltip:'Widgets', + id: 2, }, ] diff --git a/components/layout/full/vertical-sidebar/sidebarItem.ts b/components/layout/full/vertical-sidebar/sidebarItem.ts index d5fc006..e4f01a9 100644 --- a/components/layout/full/vertical-sidebar/sidebarItem.ts +++ b/components/layout/full/vertical-sidebar/sidebarItem.ts @@ -208,10 +208,24 @@ const sidebarItem: menu[] = [ }, { - header: 'Master', + header: 'Widgets', id: 2, children: [ - + { + title: 'Banners', + icon: 'gallery-wide-line-duotone', + to: '/widgets/banners' + }, + { + title: 'Cards', + icon: 'layers-minimalistic-line-duotone', + to: '/widgets/cards' + }, + { + title: 'Charts', + icon: 'chart-line-duotone', + to: '/widgets/charts' + }, ] }, @@ -286,6 +300,64 @@ const sidebarItem: menu[] = [ } ] }, + { + header: 'Style Components', + id: 3, + children: [ + { + title: 'Shadow', + icon: 'copy-line-duotone', + to: '/style-components/shadow' + }, + { + title: 'Typography', + icon: 'text-bold-circle-line-duotone', + to: '/style-components/typography' + } + ] + }, + + { + header: 'Shared Components', + id: 3, + children: [ + { + title: 'Overview', + icon: 'widget-5-line-duotone', + to: '/shared-components' + }, + { + title: 'UiParentCard & UiChildCard', + icon: 'layers-minimalistic-line-duotone', + to: '/shared-components/UiParentCard' + }, + { + title: 'WidgetCard & WidgetCardv2', + icon: 'chart-square-line-duotone', + to: '/shared-components/WidgetCards' + }, + { + title: 'Card Components', + icon: 'card-2-line-duotone', + to: '/shared-components/CardComponents' + }, + { + title: 'BaseBreadcrumb', + icon: 'route-line-duotone', + to: '/shared-components/BaseBreadcrumb' + }, + { + title: 'UiTextfieldPrimary', + icon: 'text-field-line-duotone', + to: '/shared-components/UiTextfieldPrimary' + }, + { + title: 'AppBaseCard', + icon: 'sidebar-minimalistic-line-duotone', + to: '/shared-components/AppBaseCard' + }, + ] + }, ]; diff --git a/components/shared/UiParentCardLogo.vue b/components/shared/UiParentCardLogo.vue index 0148532..f098657 100644 --- a/components/shared/UiParentCardLogo.vue +++ b/components/shared/UiParentCardLogo.vue @@ -1,6 +1,6 @@ // ===============================|| Ui Parent Card||=============================== // diff --git a/components/style-components/typography/Opacity.vue b/components/style-components/typography/Opacity.vue index dfb8491..d471a2e 100755 --- a/components/style-components/typography/Opacity.vue +++ b/components/style-components/typography/Opacity.vue @@ -1,7 +1,12 @@ diff --git a/components/style-components/typography/TextAlignment.vue b/components/style-components/typography/TextAlignment.vue index d593941..894a09d 100755 --- a/components/style-components/typography/TextAlignment.vue +++ b/components/style-components/typography/TextAlignment.vue @@ -1,11 +1,15 @@ diff --git a/components/style-components/typography/TextDecoration.vue b/components/style-components/typography/TextDecoration.vue index c0e2e44..7dbed21 100755 --- a/components/style-components/typography/TextDecoration.vue +++ b/components/style-components/typography/TextDecoration.vue @@ -1,8 +1,14 @@ diff --git a/composables/apps/medical/useDataStorage.ts b/composables/apps/medical/useDataStorage.ts deleted file mode 100644 index 58027cd..0000000 --- a/composables/apps/medical/useDataStorage.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ref } from "vue"; -import type { OdontogramData } from "~/types/apps/medical/odontogram"; - -const STORAGE_KEY = "odontogramData"; - -const savedData = ref(null); - -function saveData(data: OdontogramData) { - try { - // Convert reactive data to plain JS object before saving - const plainData = JSON.parse(JSON.stringify(data)); - console.log("Saving odontogram data to localStorage (plain):", plainData); - localStorage.setItem(STORAGE_KEY, JSON.stringify(plainData)); - savedData.value = plainData; - } catch (error) { - console.error("Failed to save odontogram data:", error); - } -} - -function loadData(): OdontogramData | null { - try { - const data = localStorage.getItem(STORAGE_KEY); - console.log("Loading odontogram data from localStorage:", data); - if (data) { - const parsed = JSON.parse(data); - if (isOdontogramData(parsed)) { - savedData.value = parsed; - return parsed; - } - } - } catch (error) { - console.error("Failed to load odontogram data:", error); - } - return null; -} - -const clearData = () => { - try { - localStorage.removeItem(STORAGE_KEY); - savedData.value = null; - return true; - } catch (error) { - console.error("Error clearing odontogram data:", error); - return false; - } -}; - -const exportData = (data: OdontogramData) => { - const blob = new Blob([JSON.stringify(data, null, 2)], { - type: "application/json" - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `odontogram_${new Date().toISOString().split("T")[0]}.json`; - a.click(); - URL.revokeObjectURL(url); -}; - -const importData = (file: File): Promise => { - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const data = JSON.parse(e.target?.result as string); - if (isOdontogramData(data)) { - resolve(data); - } else { - console.error("Imported data is not valid OdontogramData"); - resolve(null); - } - } catch (error) { - console.error("Error parsing imported data:", error); - resolve(null); - } - }; - reader.readAsText(file); - }); -}; - -function isOdontogramData(data: any): data is OdontogramData { - return ( - data && - typeof data === "object" && - Array.isArray(data.conditions) && - typeof data.metadata === "object" && - data.metadata !== null && - (data.currentMode === undefined || typeof data.currentMode === "number") - ); -} - -export function useDataStorage() { - return { - saveData, - loadData, - clearData, - exportData, - importData, - savedData - }; -} diff --git a/composables/apps/medical/useOdontogram.ts b/composables/apps/medical/useOdontogram.ts deleted file mode 100644 index 005574c..0000000 --- a/composables/apps/medical/useOdontogram.ts +++ /dev/null @@ -1,2217 +0,0 @@ -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 - }; -} diff --git a/composables/apps/medical/useToothRenderer.ts b/composables/apps/medical/useToothRenderer.ts deleted file mode 100644 index eda554f..0000000 --- a/composables/apps/medical/useToothRenderer.ts +++ /dev/null @@ -1,2646 +0,0 @@ -// Tooth condition classes - -class Polygon { - name = "Polygon"; - pos?: string; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = options; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length <= 0) { - // console.warn( - // "Polygon render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - ctx.fillStyle = this.options.fillStyle; - ctx.beginPath(); - - const vertices = [...this.vertices]; - const fpos = vertices.shift(); - if (!fpos || fpos.x === undefined || fpos.y === undefined) { - // console.warn( - // "Polygon render: first vertex missing required properties", - // fpos - // ); - return; - } - ctx.moveTo(fpos.x, fpos.y); - - while (vertices.length > 0) { - const pos = vertices.shift(); - if (pos && pos.x !== undefined && pos.y !== undefined) { - ctx.lineTo(pos.x, pos.y); - } - } - ctx.lineTo(fpos.x, fpos.y); - ctx.closePath(); - ctx.fill(); - } -} - -class AMF { - name = "AMF"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { fillStyle: "rgba(255, 0, 0, 0.7)", ...options }; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length <= 0) { - // console.warn( - // "AMF render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - // console.log("Rendering AMF with fillStyle:", this.options.fillStyle); - ctx.fillStyle = this.options.fillStyle; - ctx.beginPath(); - - const vertices = [...this.vertices]; - const fpos = vertices.shift(); - if (!fpos || fpos.x === undefined || fpos.y === undefined) { - // console.warn( - // "AMF render: first vertex missing required properties", - // fpos - // ); - return; - } - ctx.moveTo(fpos.x, fpos.y); - - while (vertices.length > 0) { - const pos = vertices.shift(); - if (pos && pos.x !== undefined && pos.y !== undefined) { - ctx.lineTo(pos.x, pos.y); - } - } - ctx.lineTo(fpos.x, fpos.y); - ctx.closePath(); - ctx.fill(); - } -} - -class COF { - name = "COF"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { fillStyle: "rgba(0, 255, 0, 0.7)", ...options }; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length <= 0) { - // console.warn( - // "COF render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - // console.log("Rendering COF with fillStyle:", this.options.fillStyle); - ctx.fillStyle = this.options.fillStyle; - ctx.beginPath(); - - const vertices = [...this.vertices]; - const fpos = vertices.shift(); - if (!fpos || fpos.x === undefined || fpos.y === undefined) { - // console.warn( - // "COF render: first vertex missing required properties", - // fpos - // ); - return; - } - ctx.moveTo(fpos.x, fpos.y); - - while (vertices.length > 0) { - const pos = vertices.shift(); - if (pos && pos.x !== undefined && pos.y !== undefined) { - ctx.lineTo(pos.x, pos.y); - } - } - ctx.lineTo(fpos.x, fpos.y); - ctx.closePath(); - ctx.fill(); - } -} - -class FIS { - name = "FIS"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { fillStyle: "rgba(255, 0, 255, 0.7)", ...options }; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length <= 0) { - // console.warn( - // "FIS render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - // console.log("Rendering FIS with fillStyle:", this.options.fillStyle); - ctx.fillStyle = this.options.fillStyle; - ctx.beginPath(); - - const vertices = [...this.vertices]; - const fpos = vertices.shift(); - if (!fpos || fpos.x === undefined || fpos.y === undefined) { - // console.warn( - // "FIS render: first vertex missing required properties", - // fpos - // ); - return; - } - ctx.moveTo(fpos.x, fpos.y); - - while (vertices.length > 0) { - const pos = vertices.shift(); - if (pos && pos.x !== undefined && pos.y !== undefined) { - ctx.lineTo(pos.x, pos.y); - } - } - ctx.lineTo(fpos.x, fpos.y); - ctx.closePath(); - ctx.fill(); - } -} - -class NVT { - name = "NVT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#303030ff", height: 25, ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - // console.warn( - // "NVT render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // console.warn( - // "NVT render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) + 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const size = x2 - x1; - const height = parseFloat(this.options.height.toString()); - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 4; - ctx.beginPath(); - ctx.moveTo(x1 + size / 4, y2); - ctx.lineTo(x1 + size / 2, y2 + height); - ctx.lineTo(x2 - size / 4, y2); - ctx.closePath(); - ctx.stroke(); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - // console.warn( - // "NVT render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // console.warn( - // "NVT render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) - 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const size = x2 - x1; - const height = parseFloat(this.options.height.toString()); - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 4; - ctx.beginPath(); - ctx.moveTo(x1 + size / 4, y1); - ctx.lineTo(x1 + size / 2, y1 - height); - ctx.lineTo(x2 - size / 4, y1); - ctx.closePath(); - ctx.stroke(); - } - } -} - -// RCT class (Perawatan Saluran Akar) -class RCT { - name = "RCT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { - strokeStyle: "#303030ff", - fillStyle: "#303030ff", - height: 25, - ...options - }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - // // console.warn( - // "RCT render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // // console.warn( - // "RCT render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) + 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const size = x2 - x1; - const height = parseFloat(this.options.height); - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.beginPath(); - ctx.moveTo(x1 + size / 4, y2); - ctx.lineTo(x1 + size / 2, y2 + height); - ctx.lineTo(x2 - size / 4, y2); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - // // console.warn( - // "RCT render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // // console.warn( - // "RCT render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) - 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const size = x2 - x1; - const height = parseFloat(this.options.height); - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.beginPath(); - ctx.moveTo(x1 + size / 4, y1); - ctx.lineTo(x1 + size / 2, y1 - height); - ctx.lineTo(x2 - size / 4, y1); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } - } -} - -class NON { - name = "NON"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { - fillStyle: "#303030ff", - fontsize: 14, - offsetX: 25, - offsetY: 5, - ...options - }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - // Log tooth position information - // console.log("IPX Tooth Information:", { - // position: this.pos || "unknown", - // row: this.row || "unknown", - // surface: this.surface || "unknown" - // }); - // Different validation logic based on row - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - // For rows 3 and 4, check first and second vertices - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[1].y.toString()) + (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "top"; - ctx.textAlign = "left"; - ctx.fillText("NON", x, y); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length <= 0) { - return; - } - if ( - this.vertices[0] == null || - this.vertices[0].x === undefined || - this.vertices[0].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[0].y.toString()) - (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize.toString()); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "bottom"; - ctx.textAlign = "left"; - ctx.fillText("NON", x, y); - } - } -} - -class UNE { - name = "UNE"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { - fillStyle: "#303030ff", - fontsize: 14, - offsetX: 25, - offsetY: 5, - ...options - }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - // Log tooth position information - // console.log("IPX Tooth Information:", { - // position: this.pos || "unknown", - // row: this.row || "unknown", - // surface: this.surface || "unknown" - // }); - // Different validation logic based on row - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - // For rows 3 and 4, check first and second vertices - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[1].y.toString()) + (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "top"; - ctx.textAlign = "left"; - ctx.fillText("UNE", x, y); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length <= 0) { - return; - } - if ( - this.vertices[0] == null || - this.vertices[0].x === undefined || - this.vertices[0].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[0].y.toString()) - (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize.toString()); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "bottom"; - ctx.textAlign = "left"; - ctx.fillText("UNE", x, y); - } - } -} - -class PRE { - name = "PRE"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { - fillStyle: "#303030ff", - fontsize: 14, - offsetX: 25, - offsetY: 5, - ...options - }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - // Log tooth position information - // console.log("IPX Tooth Information:", { - // position: this.pos || "unknown", - // row: this.row || "unknown", - // surface: this.surface || "unknown" - // }); - // Different validation logic based on row - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - // For rows 3 and 4, check first and second vertices - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[1].y.toString()) + (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "top"; - ctx.textAlign = "left"; - ctx.fillText("PRE", x, y); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length <= 0) { - return; - } - if ( - this.vertices[0] == null || - this.vertices[0].x === undefined || - this.vertices[0].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[0].y.toString()) - (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize.toString()); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "bottom"; - ctx.textAlign = "left"; - ctx.fillText("PRE", x, y); - } - } -} - -class ANO { - name = "ANO"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { - fillStyle: "#303030ff", - fontsize: 14, - offsetX: 25, - offsetY: 5, - ...options - }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - // Log tooth position information - // console.log("IPX Tooth Information:", { - // position: this.pos || "unknown", - // row: this.row || "unknown", - // surface: this.surface || "unknown" - // }); - // Different validation logic based on row - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - // For rows 3 and 4, check first and second vertices - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[1].y.toString()) + (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "top"; - ctx.textAlign = "left"; - ctx.fillText("ANO", x, y); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length <= 0) { - return; - } - if ( - this.vertices[0] == null || - this.vertices[0].x === undefined || - this.vertices[0].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[0].y.toString()) - (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize.toString()); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "bottom"; - ctx.textAlign = "left"; - ctx.fillText("ANO", x, y); - } - } -} - -class CARIES { - name = "CARIES"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#303030ff", ...options }; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length <= 0) { - // console.warn( - // "CARIES render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 4; - ctx.fillStyle = "rgba(0, 0, 0, 0.1)"; // Subtle fill to soften appearance - ctx.beginPath(); - - // const vertices = [...this.vertices]; - const inset = 0; - const vertices = this.vertices.map((v) => { - if (v == null || v.x === undefined || v.y === undefined) { - // console.warn("CARIES render: vertex missing required properties", v); - return { x: 0, y: 0 }; - } - return { - x: v.x + inset, - y: v.y + inset - }; - }); - const fpos = vertices.shift(); - if (!fpos) { - // console.warn( - // "CARIES render: first vertex missing required properties", - // fpos - // ); - return; - } - ctx.moveTo(fpos.x, fpos.y - 2); - - while (vertices.length > 0) { - const pos = vertices.shift(); - - if (pos && pos.x !== undefined && pos.y !== undefined) { - ctx.lineTo(pos.x, pos.y - 2); - } - } - ctx.lineTo(fpos.x, fpos.y - 2); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } -} - -class CFR { - name = "CFR"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { fillStyle: "#303030ff", ...options }; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length < 2) { - // console.warn( - // "CFR render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // console.warn( - // "CFR render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - const boxSize = x2 - x1; - const fontsize = parseInt(boxSize.toString()); - - const x = x1 + boxSize / 2; - const y = y1 + boxSize / 2; - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; - ctx.fillText("#", x, y); - } -} - -class FMC { - name = "FMC"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#303030ff", ...options }; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length < 2) { - // console.warn( - // "FMC render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // console.warn( - // "FMC render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const vertices = [ - { x: x1, y: y1 }, - { x: x2, y: y1 }, - { x: x2, y: y2 }, - { x: x1, y: y2 } - ]; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - - const fpos = vertices.shift(); - if (!fpos) { - // console.warn( - // "FMC render: first vertex missing required properties", - // fpos - // ); - return; - } - ctx.moveTo(fpos.x, fpos.y); - - while (vertices.length > 0) { - const pos = vertices.shift(); - if (pos && pos.x !== undefined && pos.y !== undefined) { - ctx.lineTo(pos.x, pos.y); - } - } - ctx.lineTo(fpos.x, fpos.y); - ctx.closePath(); - ctx.stroke(); - } -} - -class POC { - name = "POC"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#303030ff", ...options }; - } - - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length < 2) { - // console.warn( - // "POC render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // console.warn( - // "POC render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const vertices = [ - { x: x1, y: y1 }, - { x: x2, y: y1 }, - { x: x2, y: y2 }, - { x: x1, y: y2 } - ]; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - - const fpos = vertices.shift(); - if (!fpos) { - // console.warn( - // "POC render: first vertex missing required properties", - // fpos - // ); - return; - } - ctx.moveTo(fpos.x, fpos.y); - - while (vertices.length > 0) { - const pos = vertices.shift(); - if (pos && pos.x !== undefined && pos.y !== undefined) { - ctx.lineTo(pos.x, pos.y); - } - } - ctx.lineTo(fpos.x, fpos.y); - ctx.closePath(); - ctx.stroke(); - - // Draw Lines - ctx.lineWidth = 1; - for (let xpos = x1; xpos < x2; xpos += (x2 - x1) / 15) { - xpos = Math.min(xpos, x2); - - ctx.beginPath(); - ctx.moveTo(xpos, y1); - ctx.lineTo(xpos, y2); - ctx.stroke(); - } - } -} -class RRX { - name = "RRX"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#303030ff", ...options }; - } - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length < 2) { - // console.warn( - // "RRX render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // console.warn( - // "RRX render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const bigBoxSize = x2 - x1; - const smallBoxSize = bigBoxSize / 2; - const lines = [ - { - x1: x1 + smallBoxSize / 3, - y1: y1 - smallBoxSize / 2, - x2: x1 + smallBoxSize, - y2: y2 + smallBoxSize / 2 - }, - { - x1: x1 + smallBoxSize, - y1: y2 + smallBoxSize / 2, - x2: x1 + smallBoxSize * 2, - y2: y1 - smallBoxSize - } - ]; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 4; - for (const line of lines) { - ctx.beginPath(); - ctx.moveTo(line.x1, line.y1); - ctx.lineTo(line.x2, line.y2); - ctx.stroke(); - } - } -} - -class MIS { - name = "MIS"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#303030ff", ...options }; - } - render(ctx: CanvasRenderingContext2D) { - if (!this.vertices || this.vertices.length < 2) { - // console.warn( - // "MIS render: vertices array too short or undefined", - // this.vertices - // ); - return; - } - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].x === undefined || - this.vertices[0].y === undefined || - this.vertices[1].y === undefined - ) { - // console.warn( - // "MIS render: vertices missing required properties", - // this.vertices - // ); - return; - } - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const bigBoxSize = x2 - x1; - const smallBoxSize = bigBoxSize / 2; - const lines = [ - { - x1: x1 + smallBoxSize * 0.5, - y1: y1 - smallBoxSize / 2, - x2: x1 + smallBoxSize * 1.5, - y2: y2 + smallBoxSize / 2 - }, - { - x1: x1 + smallBoxSize * 1.5, - y1: y1 - smallBoxSize / 2, - x2: x1 + smallBoxSize * 0.5, - y2: y2 + smallBoxSize / 2 - } - ]; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 4; - for (const line of lines) { - ctx.beginPath(); - ctx.moveTo(line.x1, line.y1); - ctx.lineTo(line.x2, line.y2); - ctx.stroke(); - } - } -} - -// IPX class (Implant + Porcelain crown) -class IPX { - name = "IPX"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { - fillStyle: "#303030ff", - fontsize: 14, - offsetX: 25, - offsetY: 5, - ...options - }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - // Log tooth position information - // console.log("IPX Tooth Information:", { - // position: this.pos || "unknown", - // row: this.row || "unknown", - // surface: this.surface || "unknown" - // }); - // Different validation logic based on row - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - // For rows 3 and 4, check first and second vertices - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[1].y.toString()) + (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "top"; - ctx.textAlign = "left"; - ctx.fillText("IPX", x, y); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length <= 0) { - return; - } - if ( - this.vertices[0] == null || - this.vertices[0].x === undefined || - this.vertices[0].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[0].y.toString()) - (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize.toString()); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "bottom"; - ctx.textAlign = "left"; - ctx.fillText("IPX", x, y); - } - } -} - -// FRM_ACR class (Partial Denture/ Full Denture) -class FRM_ACR { - name = "FRM_ACR"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { - fillStyle: "#303030ff", - fontsize: 14, - offsetX: 10, - offsetY: 5, - ...options - }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - // Log tooth position information - // console.log("IPX Tooth Information:", { - // position: this.pos || "unknown", - // row: this.row || "unknown", - // surface: this.surface || "unknown" - // }); - // Different validation logic based on row - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - // For rows 3 and 4, check first and second vertices - if ( - this.vertices[0] == null || - this.vertices[1] == null || - this.vertices[0].x === undefined || - this.vertices[1].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[1].y.toString()) + (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "top"; - ctx.textAlign = "left"; - ctx.fillText("PFD/FLD", x, y); - } else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length <= 0) { - return; - } - if ( - this.vertices[0] == null || - this.vertices[0].x === undefined || - this.vertices[0].y === undefined - ) { - return; - } - const x = - parseFloat(this.vertices[0].x.toString()) + (this.options.offsetX || 0); - const y = - parseFloat(this.vertices[0].y.toString()) - (this.options.offsetY || 0); - const fontsize = parseInt(this.options.fontsize.toString()); - - ctx.fillStyle = "#000"; - ctx.font = "bold " + fontsize + "px Algerian"; - ctx.textBaseline = "bottom"; - ctx.textAlign = "left"; - ctx.fillText("PFD/FLD", x, y); - } - } -} - -class BRIDGE { - name = "BRIDGE"; - mode?: number; - startVert: { x: number; y: number }[]; - endVert: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor( - startVert: { x: number; y: number }[], - endVert: { x: number; y: number }[], - options: any = {} - ) { - this.startVert = startVert; - this.endVert = endVert; - this.options = { strokeStyle: "#303030ff", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - let vert0, vert1; - if (this.row === 1 || this.row === 2) { - if ( - this.startVert && - this.startVert.length >= 2 && - this.startVert[0] != null && - this.startVert[1] != null && - this.startVert[0].x !== undefined && - this.startVert[1].x !== undefined && - this.startVert[0].y !== undefined && - this.startVert[1].y !== undefined - ) { - vert0 = { - x1: parseFloat(this.startVert[0].x.toString()) + 1, - y1: parseFloat(this.startVert[0].y.toString()) + 1, - x2: parseFloat(this.startVert[1].x.toString()) - 1, - y2: parseFloat(this.startVert[1].y.toString()) + 1, - size: 0, - cx: 0, - cy: 0 - }; - vert0.size = vert0.x2 - vert0.x1; - vert0.cx = vert0.x1 + vert0.size / 2; - vert0.cy = vert0.y1 + vert0.size / 2; - } - - if ( - this.endVert && - this.endVert.length >= 2 && - this.endVert[0] != null && - this.endVert[1] != null && - this.endVert[0].x !== undefined && - this.endVert[1].x !== undefined && - this.endVert[0].y !== undefined && - this.endVert[1].y !== undefined - ) { - vert1 = { - x1: parseFloat(this.endVert[0].x.toString()) + 1, - y1: parseFloat(this.endVert[0].y.toString()) + 1, - x2: parseFloat(this.endVert[1].x.toString()) - 1, - y2: parseFloat(this.endVert[1].y.toString()) + 1, - size: 0, - cx: 0, - cy: 0 - }; - vert1.size = vert1.x2 - vert1.x1; - vert1.cx = vert1.x1 + vert1.size / 2; - vert1.cy = vert1.y1 + vert1.size / 2; - } - - // Draw Bridge in vert0 as U shape with vertical line in middle bottom - if (vert0) { - const x1 = vert0.x1; - const y1 = vert0.y1; - const x2 = vert0.x2; - const y2 = vert0.y2; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - - // Draw left line - ctx.moveTo(x1, y1); - ctx.lineTo(x1, y2); - // Draw bottom line - ctx.lineTo(x2, y2); - // Draw right line - ctx.lineTo(x2, y1); - // Draw vertical line in middle bottom - ctx.moveTo((x1 + x2) / 2, y2); - ctx.lineTo((x1 + x2) / 2, y2 + vert0.size / 4); - - ctx.stroke(); - } - - // Draw Bridge in vert1 as U shape with vertical line in middle bottom - if (vert1) { - const x1 = vert1.x1; - const y1 = vert1.y1; - const x2 = vert1.x2; - const y2 = vert1.y2; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - - // Draw left line - ctx.moveTo(x1, y1); - ctx.lineTo(x1, y2); - // Draw bottom line - ctx.lineTo(x2, y2); - // Draw right line - ctx.lineTo(x2, y1); - // Draw vertical line in middle bottom - ctx.moveTo((x1 + x2) / 2, y2); - ctx.lineTo((x1 + x2) / 2, y2 + vert1.size / 4); - - ctx.stroke(); - } - - // JOIN BRIDGE - if (vert0 && vert1) { - const lor = vert1.cx - vert0.cx > 0 ? 1 : -1; - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - ctx.moveTo(vert0.cx - 3 * lor, vert0.y2 + vert0.size / 4); - ctx.lineTo(vert1.cx + 3 * lor, vert1.y2 + vert1.size / 4); - ctx.stroke(); - } - } else if (this.row === 3 || this.row === 4) { - if ( - this.startVert && - this.startVert.length >= 2 && - this.startVert[0] != null && - this.startVert[1] != null && - this.startVert[0].x !== undefined && - this.startVert[1].x !== undefined && - this.startVert[0].y !== undefined && - this.startVert[1].y !== undefined - ) { - vert0 = { - x1: parseFloat(this.startVert[0].x.toString()) + 1, - y1: parseFloat(this.startVert[0].y.toString()) - 1, - x2: parseFloat(this.startVert[1].x.toString()) - 1, - y2: parseFloat(this.startVert[1].y.toString()) - 1, - size: 0, - cx: 0, - cy: 0 - }; - vert0.size = vert0.x2 - vert0.x1; - vert0.cx = vert0.x2 - vert0.size / 2; - vert0.cy = vert0.y2 - vert0.size / 2; - } else { - // // console.warn( - // "BRIDGE render: startVert missing or invalid", - // this.startVert - // ); - } - - if ( - this.endVert && - this.endVert.length >= 2 && - this.endVert[0] != null && - this.endVert[1] != null && - this.endVert[0].x !== undefined && - this.endVert[1].x !== undefined && - this.endVert[0].y !== undefined && - this.endVert[1].y !== undefined - ) { - vert1 = { - x1: parseFloat(this.endVert[0].x.toString()) + 1, - y1: parseFloat(this.endVert[0].y.toString()) - 1, - x2: parseFloat(this.endVert[1].x.toString()) - 1, - y2: parseFloat(this.endVert[1].y.toString()) - 1, - size: 0, - cx: 0, - cy: 0 - }; - vert1.size = vert1.x2 - vert1.x1; - vert1.cx = vert1.x2 - vert1.size / 2; - vert1.cy = vert1.y2 - vert1.size / 2; - } else { - // // console.warn("BRIDGE render: endVert missing or invalid", this.endVert); - } - - // Draw Bridge in vert0 as U shape with vertical line in middle top - if (vert0) { - const x1 = vert0.x1; - const y1 = vert0.y1; - const x2 = vert0.x2; - const y2 = vert0.y2; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - - // Draw left line - ctx.moveTo(x1, y2); - ctx.lineTo(x1, y1); - // Draw top line - ctx.lineTo(x2, y1); - // Draw right line - ctx.lineTo(x2, y2); - // Draw vertical line in middle top - ctx.moveTo((x1 + x2) / 2, y1); - ctx.lineTo((x1 + x2) / 2, y1 - vert0.size / 4); - - ctx.stroke(); - } - - // Draw Bridge in vert1 as U shape with vertical line in middle top - if (vert1) { - const x1 = vert1.x1; - const y1 = vert1.y1; - const x2 = vert1.x2; - const y2 = vert1.y2; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - - // Draw left line - ctx.moveTo(x1, y2); - ctx.lineTo(x1, y1); - // Draw top line - ctx.lineTo(x2, y1); - // Draw right line - ctx.lineTo(x2, y2); - // Draw vertical line in middle top - ctx.moveTo((x1 + x2) / 2, y1); - ctx.lineTo((x1 + x2) / 2, y1 - vert1.size / 4); - - ctx.stroke(); - } - - // JOIN BRIDGE - if (vert0 && vert1) { - const lor = vert1.cx - vert0.cx > 0 ? 1 : -1; - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = 6; - ctx.beginPath(); - ctx.moveTo(vert0.cx - 3 * lor, vert0.y1 - vert0.size / 4); - ctx.lineTo(vert1.cx + 3 * lor, vert1.y1 - vert1.size / 4); - ctx.stroke(); - } - } - } -} - -class HAPUS { - name = "HAPUS"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { fillStyle: "rgba(200, 200, 200, 0.8)", ...options }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - const x1 = parseFloat(this.vertices[0].x.toString()) + 1; - const y1 = parseFloat(this.vertices[0].y.toString()) + 1; - const x2 = parseFloat(this.vertices[1].x.toString()) + 1; - const y2 = parseFloat(this.vertices[1].y.toString()) + 1; - const x = x1; - const y = y1; - const size = x2 - x1; - - ctx.beginPath(); - ctx.fillStyle = this.options.fillStyle; - ctx.rect(x, y, size, size); - ctx.fill(); - } -} - -// Arrow classes for tooth condition visualization - -class ARROW_TOP_LEFT { - name = "ARROW_TOP_LEFT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - // Extract surface from options if provided - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x2 - (x2 - x1) / 3; - const fromy = y1 - 15; - const tox = fromx - 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x2 - (x2 - x1) / 3; - const fromy = y2 + 15; - const tox = fromx - 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - } - - } -} - -class ARROW_TOP_RIGHT { - name = "ARROW_TOP_RIGHT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x1 + (x2 - x1) / 3; - const fromy = y1 - 15; - const tox = fromx + 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x1 + (x2 - x1) / 3; - const fromy = y2 + 15; - const tox = fromx + 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - } - - } -} - -class ARROW_TOP_TURN_LEFT { - name = "ARROW_TOP_TURN_LEFT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x1 + (x2 - x1) / 2) - 50; - const fromy = y1 - 15; - const tox = fromx + 20; - const toy = fromy ; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx + 55, fromy + 5, 5, 1.5 * Math.PI, 0.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox + 20, toy); - ctx.lineTo(fromx + 55, toy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox); - - const newTox = tox +10; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.stroke(); - ctx.fill(); - - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x2 - (x2 - x1) / 2)+10; - const fromy = y2 + 15; - const tox = fromx - 20; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx, fromy - 5, 5, 1.5 * Math.PI, 0.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo(fromx, toy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox); - - const newTox = tox - 5; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.stroke(); - ctx.fill(); - } - - } -} - -class ARROW_TOP_TURN_RIGHT { - name = "ARROW_TOP_TURN_RIGHT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x2 - (x2 - x1) /2) - 10; - const fromy = y1 - 15; - const tox = fromx + 20; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx, fromy + 5, 5, 0.38 * Math.PI, 1.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, fromy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox - fromx); - const newTox = tox + 5; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - newTox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - - ctx.stroke(); - ctx.fill(); - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x2 - (x2 - x1) / 2) + 10; - const fromy = y2 + 15; - const tox = fromx - 20; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx, fromy - 5 , 5, 1.5 * Math.PI, 0.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo(fromx, toy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox); - - const newTox = tox - 5; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.stroke(); - ctx.fill(); - } - - } -} - -class ARROW_BOTTOM_LEFT { - name = "ARROW_BOTTOM_LEFT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x2 - (x2 - x1) / 3; - const fromy = y2 + 15; - const tox = fromx - 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x2 - (x2 - x1) / 3; - const fromy = y1 - 15; - const tox = fromx - 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - } - - } -} - -class ARROW_BOTTOM_RIGHT { - name = "ARROW_BOTTOM_RIGHT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x1 + (x2 - x1) / 3; - const fromy = y2 + 15; - const tox = fromx + 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = x1 + (x2 - x1) / 3; - const fromy = y1 - 15; - const tox = fromx + 25; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - const angle = Math.atan2(toy - fromy, tox - fromx); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, toy); - ctx.strokeStyle = this.options.strokeStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(tox, toy); - ctx.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.fill(); - } - - } -} - -class ARROW_BOTTOM_TURN_LEFT { - name = "ARROW_BOTTOM_TURN_LEFT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x2 - (x2 - x1) / 2) + 10; - const fromy = y2 + 15; - const tox = fromx - 20; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx, fromy - 5 , 5, 1.5 * Math.PI, 0.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox, toy); - ctx.lineTo(fromx, toy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox); - - const newTox = tox - 5; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.stroke(); - ctx.fill(); - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x1 + (x2 - x1) / 2) - 45; - const fromy = y1 - 15; - const tox = fromx + 20; - const toy = fromy ; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx + 55, fromy + 5, 5, 1.5 * Math.PI, 0.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(tox + 20, toy); - ctx.lineTo(fromx + 55, toy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox); - - const newTox = tox +10; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox + headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.stroke(); - ctx.fill(); - } - - } -} - -class ARROW_BOTTOM_TURN_RIGHT { - name = "ARROW_BOTTOM_TURN_RIGHT"; - mode?: number; - vertices: { x: number; y: number }[]; - options: any; - surface?: string; // Tooth surface (T, R, B, L, M) - row?: number; // Tooth row (1, 2, 3, or 4) - pos?: string; // Tooth position/number - constructor(vertices: { x: number; y: number }[], options: any = {}) { - this.vertices = vertices; - this.options = { strokeStyle: "#000000", fillStyle: "#000000", ...options }; - this.surface = options.surface; - // Extract row from options if provided - this.row = options.row; - // Extract pos from options if provided - this.pos = options.pos; - } - - render(ctx: CanvasRenderingContext2D) { - if (this.row === 1 || this.row === 2) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x2 - (x2 - x1) / 2)-10 ; - const fromy = y2 + 10; - const tox = fromx + 20; - const toy = fromy + 5; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx, fromy, 5, 0.38 * Math.PI, 1.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(fromx, toy); - ctx.lineTo(tox, toy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox); - - const newTox = tox + 5; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - newTox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.stroke(); - ctx.fill(); - }else if (this.row === 3 || this.row === 4) { - if (!this.vertices || this.vertices.length < 2) { - return; - } - - const x1 = parseFloat(this.vertices[0].x.toString()); - const y1 = parseFloat(this.vertices[0].y.toString()); - const x2 = parseFloat(this.vertices[1].x.toString()); - const y2 = parseFloat(this.vertices[1].y.toString()); - - const fromx = (x2 - (x2 - x1) /2) - 10; - const fromy = y1 - 15; - const tox = fromx + 20; - const toy = fromy; - - const headlen = 12; - const lineWidth = 4; - - ctx.strokeStyle = this.options.strokeStyle; - ctx.fillStyle = this.options.fillStyle; - ctx.lineWidth = lineWidth; - - ctx.beginPath(); - ctx.arc(fromx, fromy + 5, 5, 0.38 * Math.PI, 1.5 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(fromx, fromy); - ctx.lineTo(tox, fromy); - ctx.stroke(); - - const angle = Math.atan2(toy - fromy, tox - fromx); - - - const newTox = tox + 5; - ctx.beginPath(); - ctx.moveTo(newTox, toy); - ctx.lineTo( - newTox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.lineTo( - newTox - headlen * Math.cos(angle + Math.PI / 7), - toy - headlen * Math.sin(angle + Math.PI / 7) - ); - ctx.lineTo(newTox, toy); - ctx.lineTo( - newTox - headlen * Math.cos(angle - Math.PI / 7), - toy - headlen * Math.sin(angle - Math.PI / 7) - ); - ctx.stroke(); - ctx.fill(); - } - - } -} - -export { - Polygon, - AMF, - COF, - FIS, - NVT, - UNE, - PRE, - NON, - ANO, - MIS, - IPX, - FRM_ACR, - BRIDGE, - HAPUS, - RCT, - CARIES, - CFR, - FMC, - POC, - RRX, - ARROW_TOP_TURN_LEFT, - ARROW_TOP_TURN_RIGHT, - ARROW_BOTTOM_TURN_LEFT, - ARROW_BOTTOM_TURN_RIGHT, - ARROW_TOP_LEFT, - ARROW_TOP_RIGHT, - ARROW_BOTTOM_LEFT, - ARROW_BOTTOM_RIGHT, - useToothRenderer -}; - -function useToothRenderer() { - function renderCondition( - ctx: CanvasRenderingContext2D, - conditionName: string, - vertices: { x: number; y: number }[], - options: any = {} - ) { - const conditionClasses: Record = { - Polygon, - AMF, - COF, - FIS, - NVT, - UNE, - PRE, - NON, - ANO, - MIS, - IPX, - FRM_ACR, - BRIDGE, - HAPUS, - RCT, - CARIES, - CFR, - FMC, - POC, - RRX, - ARROW_TOP_TURN_LEFT, - ARROW_TOP_TURN_RIGHT, - ARROW_BOTTOM_TURN_LEFT, - ARROW_BOTTOM_TURN_RIGHT, - ARROW_TOP_LEFT, - ARROW_TOP_RIGHT, - ARROW_BOTTOM_LEFT, - ARROW_BOTTOM_RIGHT - }; - - const ConditionClass = conditionClasses[conditionName]; - if (!ConditionClass) { - console.warn(`Unknown condition class: ${conditionName}`); - return; - } - const conditionInstance = new ConditionClass(vertices, options); - conditionInstance.render(ctx); - } - - return { - renderCondition - }; -} diff --git a/composables/usePatientForm.ts b/composables/usePatientForm.ts deleted file mode 100644 index 9a018a8..0000000 --- a/composables/usePatientForm.ts +++ /dev/null @@ -1,131 +0,0 @@ -// composables/usePatientForm.ts -import type { FhirPatient, FhirHumanName } from "~/types/fhir/humanName"; - -interface PatientFormData { - dataDiri: { - namaLengkap: string; - jenisKelamin: string; - tanggalLahir: string; - nomorIdentitas: string; - jenisIdentitas: string; - fhirName?: FhirHumanName | null; - }; - // ... other interfaces -} - -export const usePatientForm = () => { - const convertToFhirPatient = (formData: PatientFormData): FhirPatient => { - const fhirPatient: FhirPatient = { - resourceType: "Patient", - identifier: [], - active: true, - name: [], - telecom: [], - gender: undefined, - birthDate: undefined, - address: [] - }; - - // Add parsed FHIR name - if (formData.dataDiri.fhirName) { - fhirPatient.name!.push(formData.dataDiri.fhirName); - } - - // Gender mapping - if (formData.dataDiri.jenisKelamin) { - fhirPatient.gender = - formData.dataDiri.jenisKelamin === "L" ? "male" : "female"; - } - - // Birth date - if (formData.dataDiri.tanggalLahir) { - fhirPatient.birthDate = formData.dataDiri.tanggalLahir; - } - - // Identifiers - if (formData.dataDiri.nomorIdentitas) { - fhirPatient.identifier!.push({ - use: "official", - type: { - coding: [ - { - system: "http://terminology.hl7.org/CodeSystem/v2-0203", - code: - formData.dataDiri.jenisIdentitas === "KTP" ? "NNESP" : "PPN", - display: formData.dataDiri.jenisIdentitas - } - ] - }, - value: formData.dataDiri.nomorIdentitas - }); - } - - return fhirPatient; - }; - - return { - convertToFhirPatient - }; -}; -interface PersonalInfo { - nik?: string; - fullName?: string; - birthPlace?: string; - birthDate?: string; - gender?: string; -} - -interface ContactInfo { - phone?: string; - address?: string; - province?: string; - city?: string; -} - -// export const usePatientForm = () => { -// const validatePersonalInfo = (data: PersonalInfo) => { -// const errors: Record = {}; - -// if (!data.nik) errors.nik = "NIK wajib diisi"; -// if (!data.fullName) errors.fullName = "Nama lengkap wajib diisi"; -// if (!data.birthPlace) errors.birthPlace = "Tempat lahir wajib diisi"; -// if (!data.birthDate) errors.birthDate = "Tanggal lahir wajib diisi"; -// if (!data.gender) errors.gender = "Jenis kelamin wajib diisi"; - -// return { -// valid: Object.keys(errors).length === 0, -// errors -// }; -// }; - -// const validateContactInfo = (data: ContactInfo) => { -// const errors: Record = {}; - -// if (!data.phone) errors.phone = "Nomor telepon wajib diisi"; -// if (!data.address) errors.address = "Alamat wajib diisi"; -// if (!data.province) errors.province = "Provinsi wajib diisi"; -// if (!data.city) errors.city = "Kota wajib diisi"; - -// return { -// valid: Object.keys(errors).length === 0, -// errors -// }; -// }; - -// const generateMRNumber = () => { -// const date = new Date(); -// const year = date.getFullYear().toString().substr(-2); -// const month = (date.getMonth() + 1).toString().padStart(2, "0"); -// const random = Math.floor(Math.random() * 10000) -// .toString() -// .padStart(4, "0"); - -// return `MR${year}${month}${random}`; -// }; - -// return { -// validatePersonalInfo, -// validateContactInfo, -// generateMRNumber -// }; -// }; diff --git a/data/apps/menus/menus.json b/data/apps/menus/menus.json deleted file mode 100644 index a1035bc..0000000 --- a/data/apps/menus/menus.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "menus": [ - { - "id": "1", - "title": "Medis", - "url": "", - "icon": "mdi-medical-bag", - "parentId": null, - "order": 1, - "isActive": true, - "reference": "referensi", - "children": [ - { - "id": "1-1", - "title": "Diagnosa", - "url": "bridging/referensi/medis", - "icon": "", - "parentId": "1", - "order": 1, - "isActive": true, - "reference": "referensi" - }, - { - "id": "1-2", - "title": "Poliklinik", - "url": "bridging/referensi/medis", - "icon": "", - "parentId": "1", - "order": 2, - "isActive": true, - "reference": "referensi" - } - ] - } - ], - "references": ["Referensi", "Main", "Admin"], - "menuOptions": ["Select a menu", "Main Menu", "Side Menu", "Footer Menu"] -} diff --git a/data/apps/menus/pages.json b/data/apps/menus/pages.json deleted file mode 100644 index 1f3d15b..0000000 --- a/data/apps/menus/pages.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "pages": [ - { - "id": "1", - "name": "Dashboard", - "path": "/dashboard", - "templateType": "dashboard", - "metadata": { - "title": "Dashboard", - "description": "Main dashboard page", - "keywords": ["dashboard", "admin"] - }, - "content": {}, - "createdBy": "admin", - "createdAt": "2025-06-30T05:32:00Z", - "updatedAt": "2025-06-30T05:32:00Z", - "status": "published" - } - ] -} diff --git a/data/apps/roles/roles.json b/data/apps/roles/roles.json deleted file mode 100644 index 997619f..0000000 --- a/data/apps/roles/roles.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "roles": [ - { - "id": "1", - "name": "Admin", - "description": "Full system access", - "permissions": ["create", "read", "update", "delete"], - "directories": ["/pages/admin/*", "/pages/content/*", "/pages/roles/*"], - "createdAt": "2025-06-30T05:32:00Z" - }, - { - "id": "2", - "name": "Editor", - "description": "Content management access", - "permissions": ["create", "read", "update"], - "directories": ["/pages/content/*", "/pages/blog/*"], - "createdAt": "2025-06-30T05:32:00Z" - } - ] -} diff --git a/pages/shared-components/AppBaseCard.vue b/pages/shared-components/AppBaseCard.vue new file mode 100644 index 0000000..eb7b4c0 --- /dev/null +++ b/pages/shared-components/AppBaseCard.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/pages/shared-components/BaseBreadcrumb.vue b/pages/shared-components/BaseBreadcrumb.vue new file mode 100644 index 0000000..d309b63 --- /dev/null +++ b/pages/shared-components/BaseBreadcrumb.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/pages/shared-components/CardComponents.vue b/pages/shared-components/CardComponents.vue new file mode 100644 index 0000000..5397611 --- /dev/null +++ b/pages/shared-components/CardComponents.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/pages/shared-components/UiParentCard.vue b/pages/shared-components/UiParentCard.vue new file mode 100644 index 0000000..5cce025 --- /dev/null +++ b/pages/shared-components/UiParentCard.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/pages/shared-components/UiTextfieldPrimary.vue b/pages/shared-components/UiTextfieldPrimary.vue new file mode 100644 index 0000000..73632fa --- /dev/null +++ b/pages/shared-components/UiTextfieldPrimary.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/pages/shared-components/WidgetCards.vue b/pages/shared-components/WidgetCards.vue new file mode 100644 index 0000000..22b9e7d --- /dev/null +++ b/pages/shared-components/WidgetCards.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/pages/shared-components/index.vue b/pages/shared-components/index.vue new file mode 100644 index 0000000..6ff0b1d --- /dev/null +++ b/pages/shared-components/index.vue @@ -0,0 +1,282 @@ + + + + + diff --git a/pages/style-components/Shadow.vue b/pages/style-components/Shadow.vue new file mode 100644 index 0000000..5e5a91d --- /dev/null +++ b/pages/style-components/Shadow.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/pages/style-components/Typography.vue b/pages/style-components/Typography.vue new file mode 100644 index 0000000..b2f82df --- /dev/null +++ b/pages/style-components/Typography.vue @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/pages/widgets/Banners.vue b/pages/widgets/Banners.vue new file mode 100644 index 0000000..6c67303 --- /dev/null +++ b/pages/widgets/Banners.vue @@ -0,0 +1,40 @@ + diff --git a/pages/widgets/Cards.vue b/pages/widgets/Cards.vue new file mode 100644 index 0000000..f9a49cd --- /dev/null +++ b/pages/widgets/Cards.vue @@ -0,0 +1,49 @@ + diff --git a/pages/widgets/Charts.vue b/pages/widgets/Charts.vue new file mode 100644 index 0000000..8f2dd93 --- /dev/null +++ b/pages/widgets/Charts.vue @@ -0,0 +1,76 @@ + diff --git a/plugins/vuetify.ts b/plugins/vuetify.ts index 3d5bd9c..8521629 100644 --- a/plugins/vuetify.ts +++ b/plugins/vuetify.ts @@ -2,7 +2,8 @@ import { createVuetify } from "vuetify"; import * as components from "vuetify/components"; import * as directives from "vuetify/directives"; import PerfectScrollbar from "vue3-perfect-scrollbar"; -// import VueApexCharts from "vue3-apexcharts"; +// @ts-ignore: module has incompatible/undiscoverable typings in package exports +import VueApexCharts from "vue3-apexcharts"; import VueTablerIcons from "vue-tabler-icons"; import "@mdi/font/css/materialdesignicons.css"; import "~/scss/style.scss"; @@ -74,6 +75,6 @@ export default defineNuxtPlugin((nuxtApp) => { }); nuxtApp.vueApp.use(vuetify); nuxtApp.vueApp.use(PerfectScrollbar); - // nuxtApp.vueApp.use(VueApexCharts); + nuxtApp.vueApp.use(VueApexCharts); nuxtApp.vueApp.use(VueTablerIcons); }); diff --git a/server/api/menus/delete.delete.ts b/server/api/menus/delete.delete.ts deleted file mode 100644 index ad6e81f..0000000 --- a/server/api/menus/delete.delete.ts +++ /dev/null @@ -1,49 +0,0 @@ -import fs from 'fs/promises' -import path from 'path' - -export default defineEventHandler(async (event) => { - try { - const { menuId, reference } = await readBody<{ - menuId: string - reference: string - }>(event) - - if (!menuId || !reference) { - throw createError({ - statusCode: 400, - statusMessage: 'MenuId and reference are required' - }) - } - - const filePath = path.join(process.cwd(), 'matdash', 'data', 'menus.json') - const fileContent = await fs.readFile(filePath, 'utf-8') - const data = JSON.parse(fileContent) - - // Remove menu item and its children recursively - const removeMenuItem = (items: any[], id: string): any[] => { - return items.filter(item => { - if (item.id === id) return false - if (item.children) { - item.children = removeMenuItem(item.children, id) - } - return true - }) - } - - data.menus = removeMenuItem(data.menus, menuId) - - // Save updated data - await fs.writeFile(filePath, JSON.stringify(data, null, 2)) - - return { - success: true, - message: 'Menu deleted successfully' - } - } catch (error) { - console.error('Error deleting menu:', error) - throw createError({ - statusCode: 500, - statusMessage: 'Failed to delete menu' - }) - } -}) diff --git a/server/api/menus/list.get.ts b/server/api/menus/list.get.ts deleted file mode 100644 index d716aa3..0000000 --- a/server/api/menus/list.get.ts +++ /dev/null @@ -1,34 +0,0 @@ -import fs from 'fs/promises' -import path from 'path' - -export default defineEventHandler(async (event) => { - try { - const filePath = path.join(process.cwd(), 'matdash', 'data', 'menus.json') - - // Check if file exists, create if not - try { - await fs.access(filePath) - } catch { - const defaultData = { - menus: [], - references: ["Referensi", "Main", "Admin"], - menuOptions: ["Select a menu", "Main Menu", "Side Menu", "Footer Menu"] - } - await fs.writeFile(filePath, JSON.stringify(defaultData, null, 2)) - } - - const fileContent = await fs.readFile(filePath, 'utf-8') - const data = JSON.parse(fileContent) - - return { - success: true, - data - } - } catch (error) { - console.error('Error reading menus:', error) - throw createError({ - statusCode: 500, - statusMessage: 'Failed to load menus' - }) - } -}) diff --git a/server/api/menus/save.post.ts b/server/api/menus/save.post.ts deleted file mode 100644 index 44c0816..0000000 --- a/server/api/menus/save.post.ts +++ /dev/null @@ -1,57 +0,0 @@ -import fs from 'fs/promises' -import path from 'path' -import type { MenuItem } from '../../../types/menu' - -export default defineEventHandler(async (event) => { - try { - const { menuItem, reference } = await readBody<{ - menuItem: MenuItem - reference: string - }>(event) - - if (!menuItem || !reference) { - throw createError({ - statusCode: 400, - statusMessage: 'MenuItem and reference are required' - }) - } - - const filePath = path.join(process.cwd(), 'matdash', 'data', 'menus.json') - - // Read current data - let data - try { - const fileContent = await fs.readFile(filePath, 'utf-8') - data = JSON.parse(fileContent) - } catch { - data = { menus: [], references: [], menuOptions: [] } - } - - // Update or add menu item - const existingIndex = data.menus.findIndex((item: MenuItem) => item.id === menuItem.id) - - if (existingIndex >= 0) { - data.menus[existingIndex] = { ...menuItem, updatedAt: new Date().toISOString() } - } else { - data.menus.push({ - ...menuItem, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }) - } - - // Save to file - await fs.writeFile(filePath, JSON.stringify(data, null, 2)) - - return { - success: true, - data: menuItem - } - } catch (error) { - console.error('Error saving menu:', error) - throw createError({ - statusCode: 500, - statusMessage: 'Failed to save menu' - }) - } -}) diff --git a/server/api/pages/generate.post.ts b/server/api/pages/generate.post.ts deleted file mode 100644 index c728982..0000000 --- a/server/api/pages/generate.post.ts +++ /dev/null @@ -1,111 +0,0 @@ -import fs from 'fs/promises' -import path from 'path' - -export default defineEventHandler(async (event) => { - try { - const pageData = await readBody(event) - - if (!pageData.name || !pageData.path) { - throw createError({ - statusCode: 400, - statusMessage: 'Page name and path are required' - }) - } - - // Generate Vue file content - const vueTemplate = ` - - - - -` - - // Determine file path - const fileName = `${pageData.name.toLowerCase().replace(/\s+/g, '-')}.vue` - const pagesDir = path.join(process.cwd(), 'matdash', 'pages') - const filePath = path.join(pagesDir, fileName) - - // Ensure pages directory exists - await fs.mkdir(pagesDir, { recursive: true }) - - // Write Vue file - await fs.writeFile(filePath, vueTemplate) - - // Save page data to JSON - const dataPath = path.join(process.cwd(), 'matdash', 'data', 'pages.json') - let pagesData - - try { - const fileContent = await fs.readFile(dataPath, 'utf-8') - pagesData = JSON.parse(fileContent) - } catch { - pagesData = { pages: [] } - } - - // Add or update page data - const existingIndex = pagesData.pages.findIndex((page: any) => page.path === pageData.path) - const pageRecord = { - ...pageData, - id: pageData.id || `page-${Date.now()}`, - fileName, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - } - - if (existingIndex >= 0) { - pagesData.pages[existingIndex] = pageRecord - } else { - pagesData.pages.push(pageRecord) - } - - await fs.writeFile(dataPath, JSON.stringify(pagesData, null, 2)) - - return { - success: true, - data: { - fileName, - filePath: filePath.replace(process.cwd(), ''), - pageData: pageRecord - } - } - } catch (error) { - console.error('Error generating page:', error) - throw createError({ - statusCode: 500, - statusMessage: 'Failed to generate page' - }) - } -}) diff --git a/store/apps/medical/odontogram.ts b/store/apps/medical/odontogram.ts deleted file mode 100644 index 3e38206..0000000 --- a/store/apps/medical/odontogram.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { defineStore } from "pinia"; -import { ref, reactive, readonly } from "vue"; -import type { - OdontogramData, - ToothCondition -} from "~/types/apps/medical/odontogram"; -import { OdontogramMode } from "~/types/apps/medical/odontogram"; -import { useDataStorage } from "~/composables/apps/medical/useDataStorage"; - -export const useOdontogramStore = defineStore("odontogram", () => { - const conditions = ref([]); - const currentMode = ref(OdontogramMode.DEFAULT); - const metadata = ref<{ patientId: string; date: string; dentist: string }>({ - patientId: "", - date: new Date().toISOString().split("T")[0], - dentist: "" - }); - - const { saveData, loadData } = useDataStorage(); - - const addCondition = (condition: ToothCondition) => { - console.log("addCondition called with:", condition); - // Remove existing condition for same tooth and surface - const index = conditions.value.findIndex( - (c) => - c.toothNumber === condition.toothNumber && - c.surface === condition.surface - ); - - if (index >= 0) { - conditions.value.splice(index, 1); - } - - conditions.value.push(condition); - saveCurrentData(); - }; - - const removeCondition = (toothNumber: string, surface?: string) => { - const index = conditions.value.findIndex( - (c) => - c.toothNumber === toothNumber && (!surface || c.surface === surface) - ); - - if (index >= 0) { - conditions.value.splice(index, 1); - saveCurrentData(); - } - }; - - const clearAllConditions = () => { - conditions.value = []; - saveCurrentData(); - }; - - const setMode = (mode: OdontogramMode) => { - currentMode.value = mode; - saveCurrentData(); - }; - - let isLoading = false; - - const saveCurrentData = () => { - if (isLoading) { - console.log("Skipping saveCurrentData during loading"); - return; - } - console.log("saveCurrentData called with conditions:", conditions.value); - const data: OdontogramData = { - conditions: conditions.value, - metadata: metadata.value, - currentMode: currentMode.value - }; - saveData(data); - }; - - const loadStoredData = () => { - isLoading = true; - const data = loadData(); - console.log("Loading stored data:", data); - if (data) { - conditions.value = data.conditions || []; - metadata.value = { - patientId: data.metadata?.patientId ?? "", - date: data.metadata?.date ?? new Date().toISOString().split("T")[0], - dentist: data.metadata?.dentist ?? "" - }; - if (data.currentMode !== undefined) { - currentMode.value = data.currentMode; - } - } - isLoading = false; - }; - - const exportCurrentData = () => { - const data: OdontogramData = { - conditions: conditions.value, - metadata: { - patientId: metadata.value.patientId ?? "", - date: metadata.value.date, - dentist: metadata.value.dentist ?? "" - }, - currentMode: currentMode.value - }; - return data; - }; - - const importData = (data: OdontogramData) => { - conditions.value = data.conditions || []; - metadata.value = { - patientId: data.metadata?.patientId ?? "", - date: data.metadata?.date ?? new Date().toISOString().split("T")[0], - dentist: data.metadata?.dentist ?? "" - }; - if (data.currentMode !== undefined) { - currentMode.value = data.currentMode; - } - saveCurrentData(); - }; - - function setConditions(newConditions: ToothCondition[]) { - conditions.value = newConditions; - saveCurrentData(); - } - - return { - conditions: readonly(conditions), - currentMode: readonly(currentMode), - metadata, - addCondition, - removeCondition, - clearAllConditions, - setMode, - saveCurrentData, - loadStoredData, - exportCurrentData, - importData, - setConditions, - }; -}); diff --git a/types/apps/medical/odontogram.ts b/types/apps/medical/odontogram.ts deleted file mode 100644 index 30f7013..0000000 --- a/types/apps/medical/odontogram.ts +++ /dev/null @@ -1,83 +0,0 @@ -export type Point = { - x: number; - y: number; -}; - -export type Geometry = { - name: string; - vertices: Point[]; - options?: Record; - pos?: string; -}; - -export interface ToothGeometry { - top: { tl: Point; tr: Point; br: Point; bl: Point }; - right: { tl: Point; tr: Point; br: Point; bl: Point }; - bottom: { tl: Point; tr: Point; br: Point; bl: Point }; - left: { tl: Point; tr: Point; br: Point; bl: Point }; - middle: { tl: Point; tr: Point; br: Point; bl: Point }; -} - -export interface Tooth { - num: string; - bigBoxSize: number; - smallBoxSize: number; - x1: number; - y1: number; - x2: number; - y2: number; - cx: number; - cy: number; - geometry: ToothGeometry; -} - -export enum OdontogramMode { - DEFAULT = 0, - AMF = 1, - COF = 2, - FIS = 3, - NVT = 4, - RCT = 5, - NON = 6, - UNE = 7, - PRE = 8, - ANO = 9, - CARIES = 10, - CFR = 11, - FMC = 12, - POC = 13, - RRX = 14, - MIS = 15, - IPX = 16, - FRM_ACR = 17, - BRIDGE = 18, - ARROW_TOP_LEFT = 19, // TOP-LEFT ARROW - ARROW_TOP_RIGHT = 20, // TOP-RIGHT ARROW - ARROW_TOP_TURN_LEFT = 21, // TOP-TURN-LEFT ARROW - ARROW_TOP_TURN_RIGHT = 22, // TOP-TURN-RIGHT ARROW - ARROW_BOTTOM_LEFT = 23, // BOTTOM-LEFT ARROW - ARROW_BOTTOM_RIGHT = 24, // BOTTOM-RIGHT ARROW - ARROW_BOTTOM_TURN_LEFT = 25, // BOTTOM-TURN-LEFT ARROW - ARROW_BOTTOM_TURN_RIGHT = 26, // BOTTOM-TURN-RIGHT ARROW - HAPUS = 100 - // Arrow modes can be added here -} - -export interface ToothCondition { - mode: OdontogramMode; - position: string; - toothNumber: string; - surface?: "T" | "R" | "B" | "L" | "M"; // Top, Right, Bottom, Left, Middle - group?: number; // Group number for bridge conditions - timestamp?: string; // Optional tracking -} - -export interface OdontogramData { - conditions: ToothCondition[]; - metadata: { - patientId?: string; - date: string; - dentist?: string; - }; - currentMode?: OdontogramMode; -} diff --git a/types/fhir/humanName.ts b/types/fhir/humanName.ts deleted file mode 100644 index 7cdf126..0000000 --- a/types/fhir/humanName.ts +++ /dev/null @@ -1,64 +0,0 @@ -// types/fhir.ts - -export interface FhirHumanName { - use?: - | "usual" - | "official" - | "temp" - | "nickname" - | "anonymous" - | "old" - | "maiden"; - text: string; - family?: string; - given?: string[]; - prefix?: string[]; - suffix?: string[]; - period?: { - start?: string; - end?: string; - }; -} - -export interface FhirIdentifier { - use?: "usual" | "official" | "temp" | "secondary" | "old"; - type?: { - coding?: Array<{ - system?: string; - code?: string; - display?: string; - }>; - }; - system?: string; - value?: string; - period?: { - start?: string; - end?: string; - }; -} - -export interface FhirPatient { - resourceType: "Patient"; - id?: string; - identifier?: FhirIdentifier[]; - active?: boolean; - name?: FhirHumanName[]; - telecom?: Array<{ - system?: "phone" | "fax" | "email" | "pager" | "url" | "sms" | "other"; - value?: string; - use?: "home" | "work" | "temp" | "old" | "mobile"; - }>; - gender?: "male" | "female" | "other" | "unknown"; - birthDate?: string; - address?: Array<{ - use?: "home" | "work" | "temp" | "old" | "billing"; - type?: "postal" | "physical" | "both"; - text?: string; - line?: string[]; - city?: string; - district?: string; - state?: string; - postalCode?: string; - country?: string; - }>; -} diff --git a/types/menu.ts b/types/menu.ts deleted file mode 100644 index 41a0140..0000000 --- a/types/menu.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface MenuItem { - id: string - title: string - url: string - icon: string - parentId?: string - children?: MenuItem[] - order: number - isActive: boolean - reference?: string - createdAt?: string - updatedAt?: string -} - -export interface MenuForm { - titleMenu: string - sideMenu: string - nameMenu: string - linkUrlMenu: string - iconMenu: string -} - -export interface MenuData { - menus: MenuItem[] - references: string[] - menuOptions: string[] -} diff --git a/types/roles.ts b/types/roles.ts deleted file mode 100644 index 6f6fc39..0000000 --- a/types/roles.ts +++ /dev/null @@ -1,264 +0,0 @@ -// types/simrs-roles.ts - -interface Roles { - nama_role: string; - deskripsi: string; - tanggung_jawab: string[]; - } - - interface KonfigurasiKeamanan { - autentikasi: string; - enkripsi: string; - audit_trail: boolean; - session_timeout: number; - } - - interface KonfigurasiSistem { - level_akses: string[]; - modul_utama: string[]; - keamanan: KonfigurasiKeamanan; - } - - interface SIMRSRoles { - role_administratif: Roles[]; - role_klinis: Roles[]; - role_penunjang_medis: Roles[]; - role_manajemen: Roles[]; - role_teknis: Roles[]; - konfigurasi_sistem: KonfigurasiSistem; - } - - // Enum untuk level akses - enum LevelAkses { - SUPER_ADMIN = "Super Admin", - ADMIN_DEPARTEMEN = "Admin Departemen", - USER_KLINIS = "User Klinis", - USER_ADMINISTRATIF = "User Administratif", - GUEST_READ_ONLY = "Guest/Read Only" - } - - // Enum untuk modul utama - enum ModulUtama { - PENDAFTARAN_PASIEN = "Pendaftaran Pasien", - REKAM_MEDIS_ELEKTRONIK = "Rekam Medis Elektronik", - FARMASI = "Farmasi", - LABORATORIUM = "Laboratorium", - RADIOLOGI = "Radiologi", - KEUANGAN = "Keuangan", - INVENTORY = "Inventory", - REPORTING = "Reporting" - } - - // Data konstanta untuk SIMRS roles - const simrsRoles: SIMRSRoles = { - role_administratif: [ - { - nama_role: "Administrator Pendaftaran", - deskripsi: "Mengelola proses pendaftaran pasien dan penjadwalan dalam sistem SIMRS", - tanggung_jawab: [ - "Mengelola pendaftaran pasien dan penjadwalan dokter", - "Mengatur penjadwalan operasi atau pemeriksaan", - "Mengelola antrian dan mengurangi waktu tunggu pasien", - "Memastikan data pasien terinput dengan benar" - ] - }, - { - nama_role: "Staff Administrasi Keuangan", - deskripsi: "Mengelola aspek keuangan dan penagihan dalam sistem rumah sakit", - tanggung_jawab: [ - "Mengelola penagihan dan klaim asuransi", - "Mengintegrasikan sistem dengan BPJS Kesehatan", - "Mengelola administrasi keuangan rumah sakit", - "Memproses pembayaran dan verifikasi biaya" - ] - }, - { - nama_role: "Petugas Rekam Medis", - deskripsi: "Mengelola dan memelihara rekam medis elektronik pasien", - tanggung_jawab: [ - "Mengelola Electronic Medical Records (EMR)", - "Memastikan dokumentasi pasien yang akurat", - "Menjaga keamanan dan kerahasiaan data pasien", - "Melakukan backup dan archiving data medis" - ] - } - ], - role_klinis: [ - { - nama_role: "Dokter/Tenaga Medis", - deskripsi: "Profesional medis yang menggunakan sistem untuk diagnosis dan pengobatan", - tanggung_jawab: [ - "Mengakses riwayat medis, alergi, dan catatan pengobatan pasien", - "Menggunakan modul pendukung pengambilan keputusan klinis", - "Melakukan pemantauan pasien secara berkelanjutan", - "Menginput diagnosis dan resep obat ke sistem" - ] - }, - { - nama_role: "Perawat", - deskripsi: "Tenaga keperawatan yang melakukan perawatan langsung kepada pasien", - tanggung_jawab: [ - "Mencatat parameter vital dan perkembangan pasien", - "Mengakses informasi terintegrasi dari berbagai departemen", - "Melakukan pemantauan pasien rawat inap", - "Mengelola jadwal pemberian obat dan tindakan keperawatan" - ] - }, - { - nama_role: "Apoteker/Staff Farmasi", - deskripsi: "Mengelola obat-obatan dan memastikan keamanan farmasi", - tanggung_jawab: [ - "Mengelola persediaan obat dan bahan medis", - "Memastikan ketersediaan obat yang sesuai", - "Mencegah risiko kekurangan atau kelebihan stok", - "Melakukan verifikasi resep dan dispensing obat" - ] - } - ], - role_penunjang_medis: [ - { - nama_role: "Petugas Laboratorium", - deskripsi: "Mengelola pemeriksaan laboratorium dan hasilnya", - tanggung_jawab: [ - "Menginput hasil tes laboratorium ke sistem", - "Mengintegrasikan data laboratorium dengan rekam medis", - "Melakukan quality control hasil laboratorium", - "Mengelola sampel dan alat laboratorium" - ] - }, - { - nama_role: "Petugas Radiologi", - deskripsi: "Mengelola pemeriksaan radiologi dan imaging", - tanggung_jawab: [ - "Mengelola hasil pemeriksaan radiologi", - "Mengintegrasikan data radiologi dengan sistem", - "Melakukan penjadwalan pemeriksaan radiologi", - "Memastikan kualitas gambar dan interpretasi" - ] - } - ], - role_manajemen: [ - { - nama_role: "Manajer Rumah Sakit", - deskripsi: "Mengelola operasional rumah sakit secara keseluruhan", - tanggung_jawab: [ - "Mengakses laporan dan analisis kinerja", - "Melakukan monitoring operasional rumah sakit", - "Menggunakan modul prediktif untuk perencanaan sumber daya", - "Membuat keputusan strategis berdasarkan data sistem" - ] - }, - { - nama_role: "Supervisor Departemen", - deskripsi: "Mengawasi dan mengoordinasikan departemen spesifik", - tanggung_jawab: [ - "Mengelola koordinasi antar-departemen", - "Memastikan integrasi berbagai aspek operasional", - "Monitoring kinerja staff departemen", - "Melakukan evaluasi dan pelaporan departemen" - ] - } - ], - role_teknis: [ - { - nama_role: "Administrator Sistem SIMRS", - deskripsi: "Mengelola infrastruktur dan keamanan sistem SIMRS", - tanggung_jawab: [ - "Mengelola keamanan sistem dan data terenkripsi", - "Melakukan maintenance dan troubleshooting sistem", - "Menangani masalah infrastruktur teknologi", - "Mengatur hak akses user dan permission" - ] - }, - { - nama_role: "Staff IT Support", - deskripsi: "Memberikan dukungan teknis kepada pengguna sistem", - tanggung_jawab: [ - "Memberikan pelatihan teknologi kepada pengguna", - "Menangani masalah teknis harian", - "Melakukan monitoring sistem dan evaluasi", - "Membantu troubleshooting masalah user" - ] - } - ], - konfigurasi_sistem: { - level_akses: [ - LevelAkses.SUPER_ADMIN, - LevelAkses.ADMIN_DEPARTEMEN, - LevelAkses.USER_KLINIS, - LevelAkses.USER_ADMINISTRATIF, - LevelAkses.GUEST_READ_ONLY - ], - modul_utama: [ - ModulUtama.PENDAFTARAN_PASIEN, - ModulUtama.REKAM_MEDIS_ELEKTRONIK, - ModulUtama.FARMASI, - ModulUtama.LABORATORIUM, - ModulUtama.RADIOLOGI, - ModulUtama.KEUANGAN, - ModulUtama.INVENTORY, - ModulUtama.REPORTING - ], - keamanan: { - autentikasi: "Multi-factor Authentication", - enkripsi: "AES-256", - audit_trail: true, - session_timeout: 30 - } - } - }; - - // Utility functions - class SIMRSRoleManager { - private roles: SIMRSRoles; - - constructor(roles: SIMRSRoles) { - this.roles = roles; - } - - // Mendapatkan semua role berdasarkan kategori - getRolesByCategory(category: keyof Omit): Roles[] { - return this.roles[category]; - } - - // Mencari role berdasarkan nama - findRoleByName(name: string): Roles | undefined { - const allCategories = [ - ...this.roles.role_administratif, - ...this.roles.role_klinis, - ...this.roles.role_penunjang_medis, - ...this.roles.role_manajemen, - ...this.roles.role_teknis - ]; - - return allCategories.find(role => role.nama_role === name); - } - - // Mendapatkan konfigurasi keamanan - getSecurityConfig(): KonfigurasiKeamanan { - return this.roles.konfigurasi_sistem.keamanan; - } - - // Mendapatkan semua level akses - getAccessLevels(): string[] { - return this.roles.konfigurasi_sistem.level_akses; - } - } - -export type { - Roles, - KonfigurasiKeamanan, - KonfigurasiSistem, - SIMRSRoles, -}; - -export { - LevelAkses, - ModulUtama, - SIMRSRoleManager, - simrsRoles as default, -}; - - // Contoh penggunaan - export const roleManager = new SIMRSRoleManager(simrsRoles); - \ No newline at end of file diff --git a/utils/UpdateColors.ts b/utils/UpdateColors.ts index 586f374..10d1fba 100644 --- a/utils/UpdateColors.ts +++ b/utils/UpdateColors.ts @@ -1,7 +1,7 @@ import { computed } from 'vue'; import * as themeColors from '@/theme/LightTheme'; import * as DarkThemeColors from '@/theme/DarkTheme'; -import { useCustomizerStore } from '@/stores/customizer'; +import { useCustomizerStore } from '~/store/customizer'; const custmizer = useCustomizerStore();