-
Left aligned on all viewport sizes.
-
Center aligned on all viewport sizes.
-
Right aligned on all viewport sizes.
-
Left aligned on viewports SM (small) or wider.
-
Left aligned on viewports MD (medium) or wider.
-
Left aligned on viewports LG (large) or wider.
-
Left aligned on viewports XL (extra-large) or wider.
+
+
+
+
Left aligned on all viewport sizes.
+
Center aligned on all viewport sizes.
+
Right aligned on all viewport sizes.
+
Left aligned on viewports SM (small) or wider.
+
Left aligned on viewports MD (medium) or wider.
+
Left aligned on viewports LG (large) or wider.
+
Left aligned on viewports XL (extra-large) or wider.
+
+
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 @@
-
-
Non-underlined link
-
Line-through text
-
Overline text
-
Underline text
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ AppBaseCard adalah komponen layout yang membagi tampilan menjadi
+ dua panel: panel kiri (sidebar) dan panel kanan (konten utama).
+ Panel kiri hanya ditampilkan pada layar besar (lgAndUp).
+ Pada layar kecil, panel kiri dapat diakses melalui drawer navigasi yang dipicu tombol Menu.
+
+
+
+ Komponen ini digunakan pada aplikasi seperti Chat dan
+ Kanban yang membutuhkan layout dua panel responsif.
+
+
+
+
+ | Slot | Keterangan |
+
+
+
+ leftpart |
+ Konten panel kiri (lebar tetap 320px), tampil di layar besar saja |
+
+
+ rightpart |
+ Konten panel kanan (mengisi sisa lebar) |
+
+
+ mobileLeftContent |
+ Konten dalam drawer navigasi untuk tampilan mobile |
+
+
+
+
+
+ {{ appBaseCardCode }}
+
+
+
+
+
+
+
Panel Kiri (Sidebar)
+
+
+
+
+
+
+
+
Panel Kanan (Konten Utama)
+
+ Ini adalah area konten utama. Pada layar kecil, panel kiri akan
+ disembunyikan dan dapat dibuka melalui tombol "Menu" yang muncul di atas.
+
+
+ Coba perkecil browser untuk melihat perilaku responsif!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ BaseBreadcrumb adalah komponen header halaman yang menampilkan
+ judul halaman di sebelah kiri dan navigasi breadcrumb di sebelah kanan.
+ Setiap item breadcrumb ditampilkan sebagai chip berwarna primary. Ikon rumah (home)
+ otomatis ditambahkan di awal breadcrumb.
+
+
+
+
+ | Prop | Tipe | Keterangan |
+
+
+
+ title |
+ String |
+ Judul halaman yang ditampilkan di sebelah kiri |
+
+
+ breadcrumbs |
+ Array |
+
+ Array objek breadcrumb. Setiap objek: { text, disabled, href }
+ |
+
+
+
+
+
+ {{ breadcrumbDataStructure }}
+
+
+
+ {{ breadcrumbCode }}
+
+
+
+
+
+ ↑ BaseBreadcrumb di atas merupakan contoh dengan 2 level navigasi.
+
+
+
+
+
+
+ ↑ BaseBreadcrumb dengan 4 level navigasi (Dashboard → Apps → Pengguna → Profil).
+
+
+
+
+
+ Posisi terbaik: Tempatkan BaseBreadcrumb sebagai
+ elemen pertama di dalam <template> halaman, sebelum konten utama.
+ Biasanya diletakkan di luar <v-row> konten.
+
+
+ Item breadcrumb terakhir sebaiknya diberi disabled: true untuk
+ menandakan halaman saat ini (tidak dapat diklik).
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ CardHeaderFooter adalah card outlined yang menyediakan area
+ header (judul), konten (slot default), dan footer (slot footer)
+ yang dipisahkan oleh garis pembatas.
+
+
+
+
+ | Prop | Tipe | Keterangan |
+
+
+ title | String | Judul card di bagian header |
+
+
+
+
+
+ | Slot | Keterangan |
+
+
+ default | Konten utama antara header dan footer |
+ footer | Konten footer (tombol aksi, info, dsb.) |
+
+
+
+
+ {{ cardHeaderFooterCode }}
+
+
+
+
+
+
+
+
+
+ Simpan
+ Batal
+
+
+
+
+
+
+ Apakah Anda yakin ingin menghapus data ini?
+
+
+
+ Hapus
+ Batal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UiTableCard adalah card khusus sebagai wrapper tabel data. Slot
+ default ditempatkan langsung setelah divider (tanpa padding ekstra)
+ agar tabel bisa full-width sesuai card.
+
+
+
+
+ | Prop | Tipe | Keterangan |
+
+
+ title | String | Judul di atas tabel |
+
+
+
+
+ {{ uiTableCardCode }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UiParentCardLogo sama seperti UiParentCard tetapi menampilkan
+ Logo aplikasi (komponen Logo.vue) di header alih-alih teks judul.
+ Cocok digunakan pada halaman auth (login, register, dsb.).
+
+
+
+
+ | Slot | Keterangan |
+
+
+ default | Konten utama di bawah logo |
+ action | Aksi di sebelah kanan logo |
+
+
+
+
+ {{ uiParentCardLogoCode }}
+
+
+
+
+
+
+
+ v1.0
+
+
+ Silakan masuk untuk melanjutkan. Slot default berisi
+ form login atau konten lainnya.
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ UiParentCard adalah card dengan elevation="10" yang biasanya digunakan sebagai
+ wrapper utama pada halaman. Mendukung slot default (konten) dan slot
+ action (tombol/aksi di kanan atas header).
+
+
+
+
+
+
+ | Prop |
+ Tipe |
+ Keterangan |
+
+
+
+ title | String | Judul yang ditampilkan di header card |
+
+
+
+
+
+
+
+ | Slot |
+ Keterangan |
+
+
+
+ default | Konten utama di dalam card |
+ action | Konten aksi di kanan atas (misal: tombol) |
+
+
+
+
+
+ {{ uiParentCardCode }}
+
+
+
+
+
+
+ Aksi
+
+ Ini adalah konten di dalam UiParentCard. Gunakan slot default untuk mengisi konten.
+
+
+
+
+
+
+
+
+
+
+
+ UiChildCard adalah card ber-outline (tanpa elevation) yang biasanya digunakan
+ di dalam UiParentCard untuk memisahkan bagian-bagian konten.
+
+
+
+
+
+
+ | Prop |
+ Tipe |
+ Keterangan |
+
+
+
+ title | String | Judul section di dalam card |
+
+
+
+
+
+ {{ uiChildCardCode }}
+
+
+
+
+
+
+
+ Konten section A.
+
+
+
+
+ Konten section B.
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ UiTextfieldPrimary adalah wrapper tipis di atas v-text-field
+ Vuetify yang secara otomatis menerapkan color="primary".
+ Gunakan v-model dan atribut lainnya langsung seperti pada
+ v-text-field biasa, dan manfaatkan slot default untuk label/ikon khusus.
+
+
+
+ Komponen ini menggunakan slot default (bukan prop label)
+ untuk menyisipkan konten. Untuk label teks biasa, cukup tulis teks di dalam tag.
+ Untuk atribut seperti v-model, placeholder,
+ variant, dan lainnya, gunakan langsung pada tag
+ <UiTextfieldPrimary>.
+
+
+
+
+ | Prop/Atribut | Keterangan |
+
+
+
+ v-model |
+ Binding dua arah untuk nilai input |
+
+
+ label, placeholder |
+ Diteruskan langsung ke v-text-field |
+
+
+ variant |
+ Gaya tampilan: outlined, filled, underlined, dll. |
+
+
+ density |
+ default, comfortable, compact |
+
+
+ prepend-inner-icon, append-inner-icon |
+ Ikon di dalam field |
+
+
+ Slot default |
+ Konten/label tambahan yang disisipkan ke dalam field |
+
+
+
+
+
+ {{ textfieldCode }}
+
+
+
+
+
+ Outlined (default Vuetify)
+
+
+
+ Filled
+
+
+
+ Underlined
+
+
+
+ Dengan Ikon & Density Compact
+
+
+
+
+
+
+
+
+ UiTextfieldPrimary (warna primary otomatis)
+
+
+
+ v-text-field biasa (tanpa warna)
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ WidgetCard adalah card outlined tanpa elevation yang dirancang untuk widget/statistik.
+ Memiliki margin bawah otomatis (mb-6) dan slot default untuk kontennya.
+
+
+
+
+ | Prop | Tipe | Keterangan |
+
+
+ title | String | Judul widget card |
+
+
+
+
+
+ | Slot | Keterangan |
+
+
+ default | Konten utama widget |
+
+
+
+
+ {{ widgetCardCode }}
+
+
+
+
+
+
+
+ mdi-account-group
+ 1,250
+
+ +12% dari bulan lalu
+
+
+
+
+
+ mdi-cash
+ Rp 45jt
+
+ +8% dari bulan lalu
+
+
+
+
+
+ mdi-cart
+ 320
+
+ +5% dari bulan lalu
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WidgetCardv2 adalah versi lanjutan dari WidgetCard dengan dukungan slot
+ footer untuk menambahkan aksi/info di bawah konten. Footer dapat
+ disembunyikan menggunakan prop hideaction.
+
+
+
+
+ | Prop | Tipe | Keterangan |
+
+
+ title | String | Judul widget |
+ hideaction | Boolean | Jika true, footer disembunyikan |
+
+
+
+
+
+ | Slot | Keterangan |
+
+
+ default | Konten utama |
+ footer | Konten footer (aksi/info tambahan) |
+
+
+
+
+ {{ widgetCardv2Code }}
+
+
+
+
+
+
+
+ mdi-chart-bar
+ 75%
+ target tercapai
+
+
+
+ Lihat Detail
+
+ Update: hari ini
+
+
+
+
+
+
+ Prop hideaction="true" menyembunyikan area footer
+ meskipun slot footer telah diisi.
+
+
+ Footer ini tidak akan terlihat
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ Direktori components/shared/ berisi komponen-komponen yang dapat digunakan
+ kembali di seluruh aplikasi. Gunakan komponen ini secara konsisten untuk menjaga
+ tampilan yang seragam.
+
+
+
+
+
+
+
+ {{ components.filter(c => c.category === category).length }}
+
+ {{ category }}
+
+
+
+
+ {{ components.length }}
+ Total
+
+
+
+
+
+
+
+
+
+
+
+ {{ comp.category }}
+
+
+
+ {{ comp.name }}
+
+
+ {{ comp.file }}
+
+
+
+
+ {{ comp.description }}
+
+
+
+ PROPS:
+
+ {{ prop }}
+
+
+
+
+
+ SLOTS:
+
+ #{{ slot }}
+
+
+
+
+
+
+ Lihat Tutorial
+
+
+
+
+
+
+
+
+ {{ quickStartCode }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ Default Text
+
+
+
+ Heading
+
+
+
+ Opacity
+
+
+
+ Text Alignment
+
+
+
+ Text Decoration
+
+
+
+
\ 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 @@
+
+
+
+
+
+ Blog Cards
+
+
+
+ Follower Cards
+
+
+
+ Gift Cards
+
+
+
+ Music Cards
+
+
+
+ Payment Gateways
+
+
+
+ Product Cards
+
+
+
+ Profile Cards
+
+
+
+ Settings
+
+
+
+ Upcoming Activity Card
+
+
+
+ User Cards
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ Current Value
+
+
+
+ Customer
+
+
+
+ Earned
+
+
+
+
+ Followers
+
+
+
+
+ Monthly Earnings
+
+
+
+
+ Most Visited
+
+
+
+
+ Page Impression
+
+
+
+
+ Projects
+
+
+
+
+ Revenue Updates
+
+
+
+
+ Sales Overview
+
+
+
+
+ Total Earning
+
+
+
+
+ Views
+
+
+
+
+ Yearly Breakup
+
+
+
+
+ Yearly Sales
+
+
+
+
+
+
+
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 = `
-
-
-
-
-
- {{ pageData.content.icon }}
- {{ pageData.metadata?.title || 'Page Title' }}
-
-
-
- {{ pageData.metadata?.description || 'Page description goes here.' }}
-
-
-
- This page was automatically generated from the menu system.
-
-
-
-
-
-
-
-
-
-
-
-`
-
- // 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();