first commit
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div class="odontogram-container">
|
||||
<AppsMedicalOdontogramToolbar
|
||||
:currentMode="mode"
|
||||
@modeChange="setMode"
|
||||
@clearAll="clearAll"
|
||||
/>
|
||||
|
||||
<canvas
|
||||
ref="canvas"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="odontogram-canvas"
|
||||
@mousemove="onMouseMove"
|
||||
@click="onMouseClick"
|
||||
>
|
||||
Browser anda tidak support canvas, silahkan update browser anda.
|
||||
</canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, watch, defineEmits } from "vue";
|
||||
import {
|
||||
useOdontogram,
|
||||
convertGeom
|
||||
} from "~/composables/apps/medical/useOdontogram";
|
||||
import { useToothRenderer } from "~/composables/apps/medical/useToothRenderer";
|
||||
import { useOdontogramStore } from "~/store/apps/medical/odontogram";
|
||||
|
||||
const emit = defineEmits(["update:geometry"]);
|
||||
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
const width = 1500;
|
||||
const height = 675;
|
||||
|
||||
const store = useOdontogramStore();
|
||||
|
||||
const {
|
||||
odontogramInstance,
|
||||
mode,
|
||||
initialize,
|
||||
setMode,
|
||||
onMouseMove,
|
||||
onMouseClick,
|
||||
geometry,
|
||||
clearAll
|
||||
} = useOdontogram();
|
||||
|
||||
const { renderCondition } = useToothRenderer();
|
||||
|
||||
function initializeGeometryFromConditions() {
|
||||
if (odontogramInstance.value && store.conditions.length > 0) {
|
||||
const newGeometry: Record<string, any[]> = {};
|
||||
const teeth = odontogramInstance.value.teeth;
|
||||
|
||||
// Helper to get polygons for a given surface on a tooth
|
||||
function getPolygonsForSurface(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;
|
||||
}
|
||||
|
||||
// Build a map from tooth.num to toothKey for quick lookup
|
||||
const toothNumToKeyMap = new Map<string, string>();
|
||||
Object.keys(teeth).forEach((key) => {
|
||||
const tooth = teeth[key];
|
||||
if (tooth && tooth.num) {
|
||||
toothNumToKeyMap.set(tooth.num, key);
|
||||
}
|
||||
});
|
||||
|
||||
for (const condition of store.conditions) {
|
||||
if (!teeth) continue;
|
||||
const toothKey = toothNumToKeyMap.get(String(condition.toothNumber));
|
||||
if (!toothKey) {
|
||||
console.warn(
|
||||
`No tooth key found for toothNumber: ${condition.toothNumber}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Map short surface codes to full property names in teeth object
|
||||
const surfaceMap: Record<string, string> = {
|
||||
T: "top",
|
||||
R: "right",
|
||||
B: "bottom",
|
||||
L: "left",
|
||||
M: "middle"
|
||||
};
|
||||
let surfaceKey = "middle";
|
||||
if (
|
||||
condition.surface &&
|
||||
["T", "R", "B", "L", "M"].includes(condition.surface)
|
||||
) {
|
||||
surfaceKey = surfaceMap[condition.surface];
|
||||
} else if (condition.position && typeof condition.position === "string") {
|
||||
// Try to infer surface from position string (pos)
|
||||
const parts = condition.position.split("-");
|
||||
if (parts.length > 1) {
|
||||
const surf = parts[1].toUpperCase();
|
||||
if (["T", "R", "B", "L", "M"].includes(surf)) {
|
||||
surfaceKey = surfaceMap[surf];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For AMF, COF, FIS, CARIES modes, use polygons for surface
|
||||
if (
|
||||
condition.mode === 1 || // AMF
|
||||
condition.mode === 2 || // COF
|
||||
condition.mode === 3 || // FIS
|
||||
condition.mode === 10 // CARIES
|
||||
) {
|
||||
const polygons = getPolygonsForSurface(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;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use tooth rectangle vertices and pos as tooth number
|
||||
const coord = odontogramInstance.value.teeth[toothKey];
|
||||
if (!newGeometry[toothKey]) {
|
||||
newGeometry[toothKey] = [];
|
||||
}
|
||||
if (coord) {
|
||||
// Append start/finish suffix to pos for bridge mode
|
||||
let posValue = condition.toothNumber;
|
||||
if (
|
||||
condition.mode === 18 &&
|
||||
condition.position &&
|
||||
typeof condition.position === "string"
|
||||
) {
|
||||
if (condition.position.toLowerCase().includes("start")) {
|
||||
posValue = condition.toothNumber + "-start";
|
||||
} else if (condition.position.toLowerCase().includes("finish")) {
|
||||
posValue = condition.toothNumber + "-finish";
|
||||
}
|
||||
}
|
||||
const geom = convertGeom(
|
||||
{
|
||||
vertices: [
|
||||
{ x: coord.x1, y: coord.y1 },
|
||||
{ x: coord.x2, y: coord.y2 }
|
||||
],
|
||||
pos: posValue
|
||||
},
|
||||
condition.mode
|
||||
);
|
||||
newGeometry[toothKey].push(geom);
|
||||
}
|
||||
}
|
||||
odontogramInstance.value.geometry = newGeometry;
|
||||
odontogramInstance.value.redraw();
|
||||
console.log("Geometry initialized and canvas redrawn.");
|
||||
} else {
|
||||
console.log("No conditions found or odontogram instance missing.");
|
||||
}
|
||||
console.log("Odontogram instance initialized:", odontogramInstance.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canvas.value) {
|
||||
console.log(
|
||||
"Initializing odontogram canvas with width:",
|
||||
width,
|
||||
"height:",
|
||||
height
|
||||
);
|
||||
initialize(canvas.value, width, height);
|
||||
// Load data synchronously
|
||||
store.loadStoredData();
|
||||
console.log("Data loaded from store:", store.conditions);
|
||||
|
||||
// Set mode from store or default
|
||||
// if (store.currentMode) {
|
||||
// setMode(store.currentMode);
|
||||
// } else {
|
||||
// setMode(mode.value);
|
||||
// }
|
||||
|
||||
// Initialize geometry from conditions
|
||||
// initializeGeometryFromConditions();
|
||||
} else {
|
||||
console.warn("Canvas ref is null on mounted");
|
||||
}
|
||||
});
|
||||
|
||||
// watch(
|
||||
// () => store.conditions,
|
||||
// () => {
|
||||
// initializeGeometryFromConditions();
|
||||
// }
|
||||
// );
|
||||
|
||||
// Watch store currentMode and update local mode and odontogram instance
|
||||
watch(
|
||||
() => store.currentMode,
|
||||
(newMode) => {
|
||||
if (newMode !== mode.value) {
|
||||
mode.value = newMode;
|
||||
odontogramInstance.value?.setMode(newMode);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Keep existing watch on local mode to update odontogram instance and store mode
|
||||
watch(mode, (newMode) => {
|
||||
console.log("Mode changed to:", newMode);
|
||||
odontogramInstance.value?.setMode(newMode);
|
||||
store.setMode(newMode);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.odontogram-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.odontogram-canvas {
|
||||
border: 1px solid #a9a9a9;
|
||||
margin-top: 15px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.odontogram-canvas {
|
||||
max-width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<SharedWidgetCard title="Alat Kondisi Gigi" :icon="mdi - tooth">
|
||||
<v-row>
|
||||
<v-col cols="12" md="8">
|
||||
<div class="mode-grid">
|
||||
<v-btn
|
||||
v-for="mode in odontogramModes"
|
||||
:key="mode.value"
|
||||
:color="props.currentMode === mode.value ? 'primary' : 'default'"
|
||||
:variant="
|
||||
props.currentMode === mode.value ? 'elevated' : 'outlined'
|
||||
"
|
||||
size="small"
|
||||
flat
|
||||
class="mode-btn"
|
||||
@click="setMode(mode.value)"
|
||||
>
|
||||
<v-icon size="small" :color="mode.color">{{ mode.icon }}</v-icon>
|
||||
{{ mode.label }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-btn color="error" variant="elevated" block @click="clearAll">
|
||||
<v-icon left>mdi-delete</v-icon>
|
||||
Hapus Semua
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</SharedWidgetCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SharedWidgetCard } from "#components";
|
||||
import { defineEmits, defineProps } from "vue";
|
||||
import { OdontogramMode } from "~/types/apps/medical/odontogram";
|
||||
|
||||
const props = defineProps({
|
||||
currentMode: {
|
||||
type: Number as () => OdontogramMode,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["modeChange", "clearAll"]);
|
||||
|
||||
const odontogramModes = [
|
||||
{
|
||||
value: OdontogramMode.DEFAULT,
|
||||
label: "Normal",
|
||||
icon: "mdi-cursor-default"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.AMF,
|
||||
label: "Amalgam",
|
||||
icon: "mdi-circle",
|
||||
color: "#222"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.COF,
|
||||
label: "Composite",
|
||||
icon: "mdi-circle",
|
||||
color: "#29b522"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.FIS,
|
||||
label: "Sealant",
|
||||
icon: "mdi-circle",
|
||||
color: "#ed3bed"
|
||||
},
|
||||
{ value: OdontogramMode.NVT, label: "Non-vital", icon: "mdi-triangle" },
|
||||
{ value: OdontogramMode.RCT, label: "Saluran Akar", icon: "mdi-triangle" },
|
||||
{ value: OdontogramMode.NON, label: "Tidak Ada", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.UNE, label: "Un-Erupted", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.PRE, label: "Partial-Erupt", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.ANO, label: "Anomali", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.CARIES, label: "Caries", icon: "mdi-border-color" },
|
||||
{ value: OdontogramMode.CFR, label: "Fraktur", icon: "mdi-pound" },
|
||||
{
|
||||
value: OdontogramMode.FMC,
|
||||
label: "Metal Crown",
|
||||
icon: "mdi-square-outline"
|
||||
},
|
||||
{ value: OdontogramMode.POC, label: "Porcelain Crown", icon: "mdi-square" },
|
||||
{ value: OdontogramMode.RRX, label: "Sisa Akar", icon: "mdi-tilde" },
|
||||
{ value: OdontogramMode.MIS, label: "Hilang", icon: "mdi-close" },
|
||||
{ value: OdontogramMode.IPX, label: "Implant", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.FRM_ACR, label: "Denture", icon: "mdi-text" },
|
||||
{ value: OdontogramMode.BRIDGE, label: "Bridge", icon: "mdi-bridge" },
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_LEFT,
|
||||
label: "Top Left Arrow",
|
||||
icon: "mdi-arrow-top-left"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_RIGHT,
|
||||
label: "Top Right Arrow",
|
||||
icon: "mdi-arrow-top-right"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_LEFT,
|
||||
label: "Bottom Left Arrow",
|
||||
icon: "mdi-arrow-bottom-left"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_RIGHT,
|
||||
label: "Bottom Right Arrow",
|
||||
icon: "mdi-arrow-bottom-right"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_TURN_LEFT,
|
||||
label: "Top Turn Left Arrow",
|
||||
icon: "mdi-arrow-top-left-bold"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_TOP_TURN_RIGHT,
|
||||
label: "Top Turn Right Arrow",
|
||||
icon: "mdi-arrow-top-right-bold"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_TURN_LEFT,
|
||||
label: "Bottom Turn Left Arrow",
|
||||
icon: "mdi-arrow-bottom-left-bold"
|
||||
},
|
||||
{
|
||||
value: OdontogramMode.ARROW_BOTTOM_TURN_RIGHT,
|
||||
label: "Bottom Turn Right Arrow",
|
||||
icon: "mdi-arrow-bottom-right-bold"
|
||||
},
|
||||
{ value: OdontogramMode.HAPUS, label: "Hapus", icon: "mdi-eraser" }
|
||||
];
|
||||
|
||||
const setMode = (mode: OdontogramMode) => {
|
||||
emit("modeChange", mode);
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
if (confirm("Apakah Anda yakin ingin menghapus semua kondisi gigi?")) {
|
||||
emit("clearAll", true);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mode-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
text-transform: none;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mode-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="tooth-element">
|
||||
<!-- Placeholder for individual tooth element -->
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// This component can be extended to handle individual tooth rendering or interaction
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tooth-element {
|
||||
/* Basic styling */
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user