import { ComponentNature, sceneComponent } from '@hatiolab/things-scene' import { themesColorRange } from '@fmsim/api' import MCSVehicle from './mcs-vehicle' import { getValueOnRanges } from './utils/get-value-on-ranges' import { BATTERY_RATE_LEGEND } from './features/mcs-status-default' const NATURE: ComponentNature = { mutable: false, resizable: true, rotatable: true, properties: [ ...MCSVehicle.properties, { type: 'select', label: 'battery-rate-legend-name', name: 'batteryRateLegendName', property: { options: themesColorRange } } ] } const GAP = 2 @sceneComponent('AGV') export default class AGV extends MCSVehicle { static get nature() { return NATURE } get auxColor() { return 'black' } getLegendFallback() { return (this.root as any).agvLegendTheme || super.getLegendFallback() } getBatteryLegendFallback() { return (this.root as any).batteryRateLegendTheme || BATTERY_RATE_LEGEND } getBatteryLegendTheme() { const { batteryRateLegendName } = this.state if (batteryRateLegendName) { return (this.root as any)?.style?.[batteryRateLegendName] } } getBatteryColor(rate: number) { return getValueOnRanges(rate, this.getBatteryLegendTheme() || this.getBatteryLegendFallback()) } render(ctx: CanvasRenderingContext2D) { var { left, top, width, height } = this.bounds ctx.translate(left, top) ctx.beginPath() // 작은 크기에서도 보이도록 최소 선 두께 설정 const minLineWidth = 0.5 const scaleFactor = Math.min(width, height) / 100 const lineWidth = Math.max(minLineWidth, scaleFactor) ctx.strokeStyle = this.auxColor ctx.fillStyle = this.statusColor! ctx.lineWidth = lineWidth ctx.lineCap = 'round' ctx.setLineDash([]) // 기본 사각형 그리기 ctx.rect(1, 0, width - 2, height) ctx.rect(GAP + 1, GAP, width - (GAP + 1) * 2, height - GAP * 2) ctx.fill() ctx.stroke() ctx.beginPath() // 바퀴 그리기 ctx.strokeStyle = 'black' ctx.moveTo(0, (height * 1) / 3) ctx.lineTo(0, (height * 2) / 3) ctx.moveTo(width, (height * 1) / 3) ctx.lineTo(width, (height * 2) / 3) ctx.stroke() ctx.translate(-left, -top) } // 사선 패턴을 그리는 함수 drawHatchPattern(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { // 6개의 빗금을 그리기 위한 간격 계산 const spacing = (width + height) / 6 // 선 두께 계산 (크기에 비례) const minLineWidth = 0.5 const scaleFactor = Math.min(width, height) / 20 // 130에서 50으로 변경하여 더 두껍게 const lineWidth = Math.max(minLineWidth, scaleFactor) ctx.beginPath() ctx.strokeStyle = 'black' ctx.lineWidth = lineWidth // 배터리 박스 내부로 클리핑 설정 ctx.save() ctx.rect(x, y, width, height) ctx.clip() // 6개의 사선 패턴 그리기 for (let i = 0; i < 6; i++) { const startX = x + i * spacing ctx.moveTo(startX, y) ctx.lineTo(startX - height, y + height) } ctx.stroke() // 클리핑 해제 ctx.restore() } postrender(ctx: CanvasRenderingContext2D): void { super.postrender(ctx) const BATTERYRATE = this.data?.BATTERYRATE if (typeof BATTERYRATE === 'number') { const { left, top, width, height } = this.bounds ctx.translate(left, top) // 배터리 크기 및 위치 설정 (최소 크기 보장) const minBatteryHeight = 2 const minBatteryWidth = 4 const preferredHeight = height / 5 const preferredWidth = (width * 3) / 4 const batteryHeight = Math.max(minBatteryHeight, preferredHeight) const batteryWidth = Math.max(minBatteryWidth, preferredWidth) const batteryX = (width - batteryWidth) / 2 const batteryY = (height - batteryHeight) * 1.1 // 배터리 테두리 그리기 (크기에 비례한 선 두께) const minLineWidth = 0.5 const scaleFactor = Math.min(width, height) / 100 const lineWidth = Math.max(minLineWidth, scaleFactor) // 배터리 배경을 흰색으로 채우기 ctx.fillStyle = 'white' ctx.fillRect(batteryX, batteryY, batteryWidth, batteryHeight) // 배터리 전체 영역에 사선 패턴 그리기 this.drawHatchPattern(ctx, batteryX, batteryY, batteryWidth, batteryHeight) // 배터리 잔량 표시 const fillWidth = (batteryWidth * BATTERYRATE) / 100 // 배터리 잔량에 따른 색상 설정 - 테마 적용 ctx.fillStyle = this.getBatteryColor(BATTERYRATE) // 배터리 잔량 영역 채우기 ctx.fillRect(batteryX, batteryY, fillWidth, batteryHeight) // 배터리 테두리 다시 그리기 ctx.beginPath() ctx.strokeStyle = 'black' ctx.lineWidth = lineWidth ctx.strokeRect(batteryX, batteryY, batteryWidth, batteryHeight) // 배터리 잔량 텍스트 표시 const fontSize = batteryHeight * 0.8 // 실제 렌더링될 텍스트 크기 계산 const transform = ctx.getTransform() const scaleX = Math.sqrt(transform.m11 * transform.m11 + transform.m21 * transform.m21) const actualFontSize = (fontSize * (scaleX || 1)) / window.devicePixelRatio // 실제 텍스트 크기가 10px 이상일 때만 텍스트 표시 if (actualFontSize >= 10) { ctx.font = `${fontSize}px Roboto, Arial, sans-serif` ctx.fillStyle = 'black' ctx.textAlign = 'center' ctx.textBaseline = 'middle' // 배터리 잔량 텍스트 (정수 + % 포함) const { BATTERYRATETEXT } = this.data || {} const text = BATTERYRATETEXT || `${Math.round(BATTERYRATE)}%` // 텍스트 크기 측정 const textMetrics = ctx.measureText(text) const textWidth = textMetrics.width * 1.1 // 여백을 위해 10% 추가 const textHeight = fontSize * 1.1 // 여백을 위해 10% 추가 // 배터리 중심점 계산 const batteryCenterX = batteryX + batteryWidth / 2 const batteryCenterY = batteryY + batteryHeight / 2 // 현재 변환 상태 저장 ctx.save() // 배터리 중심점으로 이동 ctx.translate(batteryCenterX, batteryCenterY) // 순수한 회전 각도 계산 (라디안) const normalizedM11 = transform.m11 / scaleX const normalizedM21 = transform.m21 / scaleX let angle = Math.atan2(normalizedM21, normalizedM11) // 회전 보정 (같은 방향으로 회전) ctx.rotate(angle) // 텍스트 배경 위치 계산 (중심점 기준) const textBgX = -textWidth / 2 const textBgY = -textHeight / 2 // 텍스트 배경 그리기 ctx.fillStyle = 'white' ctx.fillRect(textBgX, textBgY, textWidth, textHeight) // 텍스트 그리기 ctx.fillStyle = 'black' ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillText(text, 0, textHeight * 0.1) // 텍스트를 약간 아래로 이동 // 변환 상태 복원 ctx.restore() } ctx.translate(-left, -top) } } }