import { Component, ComponentNature, POSITION, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene' import { LEGEND_CARRIER } from './features/mcs-status-default' import { safeRound } from './utils/safe-round' import { ParentObjectMixin, ParentObjectMixinProperties } from './features/parent-object-mixin' /** * 캐리어 상태별 비율 게이지 (Full / Empty / EmptyEmpty) * * 스토커에 보관된 캐리어들의 상태 비율을 세그먼트 게이지로 표시한다. * Portrait(세로) 모드에서는 아래→위, Landscape(가로) 모드에서는 왼→오 방향으로 채운다. */ interface Segment { label: string count: number ratio: number color: string } const NATURE: ComponentNature = { mutable: false, resizable: true, rotatable: true, properties: [ ...ParentObjectMixinProperties, { type: 'number', name: 'carrierFull', label: 'carrier-full' }, { type: 'number', name: 'carrierEmpty', label: 'carrier-empty' }, { type: 'number', name: 'carrierEmptyEmpty', label: 'carrier-empty-empty' }, { type: 'number', label: 'round', name: 'round', property: { min: 0 } } ] } const DEFAULT_COLORS = { full: LEGEND_CARRIER['FULL'] || '#4AA7FE', empty: LEGEND_CARRIER['EMPTY'] || '#8BDA5B', emptyEmpty: LEGEND_CARRIER['EMPTYEMPTY'] || '#019D59' } var controlHandler = { ondragmove: function (point: POSITION, index: number, component: Component) { var { left, top, width, height } = component.model var transcoorded = component.transcoordP2S(point.x, point.y) var round = ((transcoorded.x - left) / (width / 2)) * 100 round = safeRound(round, width, height) component.set({ round }) } } @sceneComponent('StockerCarrierBar') export default class StockerCarrierBar extends ParentObjectMixin(RectPath(Shape)) { static get nature() { return NATURE } get controls() { var { left, top, width, round, height } = this.state round = round == undefined ? 0 : safeRound(round, width, height) return [ { x: left + (width / 2) * (round / 100), y: top, handler: controlHandler } ] } isIdentifiable() { return false } get segments(): Segment[] { const { carrierFull = 0, carrierEmpty = 0, carrierEmptyEmpty = 0 } = this.state const total = carrierFull + carrierEmpty + carrierEmptyEmpty if (total === 0) { return [ { label: 'Full', count: 0, ratio: 0, color: DEFAULT_COLORS.full }, { label: 'Empty', count: 0, ratio: 0, color: DEFAULT_COLORS.empty }, { label: 'Empty\nEmpty', count: 0, ratio: 0, color: DEFAULT_COLORS.emptyEmpty } ] } return [ { label: 'Full', count: carrierFull, ratio: carrierFull / total, color: DEFAULT_COLORS.full }, { label: 'Empty', count: carrierEmpty, ratio: carrierEmpty / total, color: DEFAULT_COLORS.empty }, { label: 'Empty\nEmpty', count: carrierEmptyEmpty, ratio: carrierEmptyEmpty / total, color: DEFAULT_COLORS.emptyEmpty } ] } render(context: CanvasRenderingContext2D) { const { width, height } = this.bounds if (width >= height) { this.renderLandscape(context) } else { this.renderPortrait(context) } } renderPortrait(context: CanvasRenderingContext2D) { const { left, top, width, height } = this.bounds const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state const segments = this.segments context.save() context.translate(left, top) // background context.beginPath() context.fillStyle = fillStyle context.roundRect(0, 0, width, height, round) context.fill() context.clip() // segments (아래→위) let y = height for (const seg of segments) { const segHeight = height * seg.ratio if (segHeight <= 0) continue y -= segHeight context.beginPath() context.fillStyle = seg.color context.rect(0, y, width, segHeight) context.fill() // label this._drawSegmentLabel(context, seg, 0, y, width, segHeight) } // outline context.beginPath() context.setLineDash([]) context.strokeStyle = strokeStyle context.lineWidth = lineWidth context.roundRect(0, 0, width, height, round) context.stroke() context.translate(-left, -top) context.restore() } renderLandscape(context: CanvasRenderingContext2D) { const { left, top, width, height } = this.bounds const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state const segments = this.segments context.save() context.translate(left, top) // background context.beginPath() context.fillStyle = fillStyle context.roundRect(0, 0, width, height, round) context.fill() context.clip() // segments (왼→오) let x = 0 for (const seg of segments) { const segWidth = width * seg.ratio if (segWidth <= 0) continue context.beginPath() context.fillStyle = seg.color context.rect(x, 0, segWidth, height) context.fill() this._drawSegmentLabel(context, seg, x, 0, segWidth, height) x += segWidth } // outline context.beginPath() context.setLineDash([]) context.strokeStyle = strokeStyle context.lineWidth = lineWidth context.roundRect(0, 0, width, height, round) context.stroke() context.translate(-left, -top) context.restore() } private _drawSegmentLabel( context: CanvasRenderingContext2D, seg: Segment, x: number, y: number, w: number, h: number ) { const percent = Math.round(seg.ratio * 100) const lines = seg.label.split('\n') lines.push(`${percent}%`) const fontSize = Math.max(9, Math.min(14, Math.min(w, h) * 0.22)) context.font = `bold ${fontSize}px sans-serif` context.fillStyle = '#333' context.textAlign = 'center' context.textBaseline = 'middle' const lineHeight = fontSize * 1.2 const totalHeight = lines.length * lineHeight const startY = y + (h - totalHeight) / 2 + lineHeight / 2 for (let i = 0; i < lines.length; i++) { context.fillText(lines[i], x + w / 2, startY + i * lineHeight) } } onchangeData(after: Record, before: Record): void { const { CARRIER_FULL = 0, CARRIER_EMPTY = 0, CARRIER_EMPTY_EMPTY = 0 } = this.data && typeof this.data == 'object' ? this.data : {} this.setState({ carrierFull: Number(CARRIER_FULL) || 0, carrierEmpty: Number(CARRIER_EMPTY) || 0, carrierEmptyEmpty: Number(CARRIER_EMPTY_EMPTY) || 0 }) } }