23 changed files with 1144 additions and 430 deletions
@ -1,26 +1,285 @@ |
|||||
import * as THREE from 'three' |
import * as THREE from 'three' |
||||
|
import gsap from 'gsap' |
||||
|
import type { Object3DLike } from '@/types/ModelTypes.ts' |
||||
|
import Viewport from '@/core/engine/Viewport.ts' |
||||
|
import TaskQueue, { type Task } from '@/core/base/TaskQueue.ts' |
||||
|
import type { MeshWrap } from '@/core/manager/InstanceMeshManager.ts' |
||||
|
import { getRenderer } from '@/core/manager/ModuleManager.ts' |
||||
|
|
||||
/** |
/** |
||||
* BaseEntity class |
* 物流实体基类 |
||||
* Provides a base for managing logistics unit entities. |
|
||||
*/ |
*/ |
||||
export default abstract class BaseEntity { |
export default abstract class BaseEntity implements EntityIf, Loadable, Carry, Walk, ForkArm, LiftingArm { |
||||
protected itemJson!: ItemJson |
protected readonly viewport: Viewport |
||||
protected objects!: THREE.Object3D[] |
protected readonly taskQueue: TaskQueue = new TaskQueue() |
||||
|
readonly id: string |
||||
|
readonly item: ItemJson |
||||
|
readonly object: THREE.Object3D |
||||
|
readonly t: string |
||||
|
|
||||
|
isArmExtended: boolean = false |
||||
|
isArmRaised: boolean = false |
||||
|
isCarrying: boolean = false |
||||
|
isLoading: boolean = false |
||||
|
isUnloading: boolean = false |
||||
|
isWalking: boolean = false |
||||
|
isPaused: boolean = false |
||||
|
|
||||
|
constructor(viewport: Viewport, id: string) { |
||||
|
this.viewport = viewport |
||||
|
this.id = id |
||||
|
this.item = viewport.find(id) |
||||
|
this.object = viewport.entityManager.findObjectById(id) as THREE.Object3D |
||||
|
this.t = this.item.t |
||||
|
} |
||||
|
|
||||
|
// 帮助方法:获取机械臂对象
|
||||
|
abstract getArmObject(): THREE.Object3D | undefined |
||||
|
|
||||
|
// 帮助方法:获取机械臂立柱
|
||||
|
abstract getArmPillar(): THREE.Object3D | undefined |
||||
|
|
||||
|
// 待执行任务总数
|
||||
|
get taskCount(): number { |
||||
|
return this.taskQueue.length |
||||
|
} |
||||
|
|
||||
|
// 是否正在执行任务
|
||||
|
get taskIsRunning(): boolean { |
||||
|
return this.taskQueue.isExecuting |
||||
|
} |
||||
|
|
||||
|
// 取消所有任务
|
||||
|
cancelTask(): void { |
||||
|
this.taskQueue.cancelAll() |
||||
|
} |
||||
|
|
||||
|
// 暂停任务
|
||||
|
taskPause(): void { |
||||
|
this.isPaused = true |
||||
|
this.taskQueue.pause() |
||||
|
} |
||||
|
|
||||
|
// 恢复任务
|
||||
|
taskResume(): void { |
||||
|
this.isPaused = false |
||||
|
this.taskQueue.resume() |
||||
|
} |
||||
|
|
||||
|
// 开始执行任务
|
||||
|
taskStartRun(): void { |
||||
|
this.taskQueue.start() |
||||
|
} |
||||
|
|
||||
|
// 创建基础任务
|
||||
|
protected createTask<T extends Task>(type: string, execute: () => Promise<void>, cancel?: () => void): T { |
||||
|
return { type, execute, cancel, isCancelable: true } as T |
||||
|
} |
||||
|
|
||||
|
// 上
|
||||
|
addArmRaise(height: number): void { |
||||
|
this.taskQueue.add(this.createTask('ARM_RAISE', async () => { |
||||
|
return new Promise(resolve => { |
||||
|
const arm = this.getArmObject() |
||||
|
if (!arm) return resolve() |
||||
|
|
||||
|
gsap.to(arm.position, { |
||||
|
y: height, |
||||
|
duration: 3, |
||||
|
ease: 'sine.inOut', |
||||
|
onComplete: resolve |
||||
|
}) |
||||
|
}) |
||||
|
}, () => { |
||||
|
// 取消动画
|
||||
|
gsap.killTweensOf(this.getArmObject()?.position) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 下
|
||||
|
addArmLower(): void { |
||||
|
this.taskQueue.add(this.createTask('ARM_LOWER', async () => { |
||||
|
return new Promise(resolve => { |
||||
|
const arm = this.getArmObject() |
||||
|
if (!arm) return resolve() |
||||
|
|
||||
|
gsap.to(arm.position, { |
||||
|
y: 0, |
||||
|
duration: 3, |
||||
|
ease: 'sine.inOut', |
||||
|
onComplete: resolve |
||||
|
}) |
||||
|
}) |
||||
|
}, () => { |
||||
|
gsap.killTweensOf(this.getArmObject()?.position) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 伸
|
||||
|
addArmExtender(): void { |
||||
|
this.taskQueue.add(this.createTask('ARM_EXTEND', async () => { |
||||
|
return new Promise(resolve => { |
||||
|
const pillar = this.getArmPillar() |
||||
|
if (!pillar) return resolve() |
||||
|
|
||||
|
gsap.to(pillar.position, { |
||||
|
z: -1.2, |
||||
|
duration: 3, |
||||
|
ease: 'sine.inOut', |
||||
|
onComplete: () => { |
||||
|
this.isArmExtended = true |
||||
|
resolve() |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}, () => { |
||||
|
gsap.killTweensOf(this.getArmPillar()?.position) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 缩
|
||||
|
addArmRetractor(): void { |
||||
|
this.taskQueue.add(this.createTask('ARM_RETRACT', async () => { |
||||
|
return new Promise(resolve => { |
||||
|
const pillar = this.getArmPillar() |
||||
|
if (!pillar) return resolve() |
||||
|
|
||||
|
gsap.to(pillar.position, { |
||||
|
z: 0, |
||||
|
duration: 3, |
||||
|
ease: 'sine.inOut', |
||||
|
onComplete: () => { |
||||
|
this.isArmExtended = false |
||||
|
resolve() |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}, () => { |
||||
|
gsap.killTweensOf(this.getArmPillar()?.position) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 装
|
||||
|
addLoad(item: string): void { |
||||
|
this.taskQueue.add(this.createTask('LOAD', async () => { |
||||
|
// 实际业务中应包含装载逻辑
|
||||
|
this.isCarrying = true |
||||
|
throw new Error(`装载物品 ${item} 的逻辑未实现`) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 卸
|
||||
|
addUnload(item: string, rack: string, bay?: number, level?: number): void { |
||||
|
this.taskQueue.add(this.createTask('UNLOAD', async () => { |
||||
|
// 实际业务中应包含卸载逻辑
|
||||
|
this.isCarrying = false |
||||
|
throw new Error(`装载物品 ${item} 的逻辑未实现`) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 转
|
||||
|
addRotation(angle: number): void { |
||||
|
this.taskQueue.add(this.createTask('ROTATION', async () => { |
||||
|
return new Promise(resolve => { |
||||
|
const time = Math.abs(angle - THREE.MathUtils.radToDeg(this.object.rotation.y)) / 45 |
||||
|
const duration = Math.max(0.5, time) |
||||
|
|
||||
|
gsap.to(this.object.rotation, { |
||||
|
y: THREE.MathUtils.degToRad(angle), |
||||
|
duration, |
||||
|
ease: 'none', |
||||
|
onComplete: resolve |
||||
|
}) |
||||
|
}) |
||||
|
}, () => { |
||||
|
gsap.killTweensOf(this.object.rotation) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 走
|
||||
|
addTravel(pos: Vector3IF | string): void { |
||||
|
this.taskQueue.add(this.createTask('TRAVEL', async () => { |
||||
|
return new Promise(resolve => { |
||||
|
const fromPos = this.object.position |
||||
|
|
||||
|
let position: Vector3IF |
||||
|
if (pos instanceof String || typeof pos === 'string') { |
||||
|
pos = Model.getPositionByEntityId(pos as string) |
||||
|
} |
||||
|
|
||||
|
const toPos = new THREE.Vector3(pos.x, pos.y, pos.z) |
||||
|
const distance = fromPos.distanceTo(toPos) |
||||
|
const speed = 2 // 2m/s
|
||||
|
const duration = Math.max(1.5, distance / speed) |
||||
|
|
||||
|
this.isWalking = true |
||||
|
|
||||
|
gsap.to(this.object.position, { |
||||
|
x: toPos.x, |
||||
|
y: toPos.y, |
||||
|
z: toPos.z, |
||||
|
duration, |
||||
|
ease: 'sine.inOut', |
||||
|
onComplete: () => { |
||||
|
this.isWalking = false |
||||
|
resolve() |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}, () => { |
||||
|
this.isWalking = false |
||||
|
gsap.killTweensOf(this.object.position) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
/** |
/** |
||||
* Sets the `ItemJson` data for the entity. |
* 拾取物品 |
||||
* @param itemJson - The `ItemJson` data to set. |
|
||||
*/ |
*/ |
||||
setItem(itemJson: ItemJson): void { |
pickupItem(id: string): THREE.Object3D { |
||||
this.itemJson = itemJson |
// 找到物品所在的地方, 删除他
|
||||
|
const item = this.viewport.entityManager.findItemById(id) |
||||
|
const wrap = this.viewport.entityManager.findObjectById(id) as MeshWrap |
||||
|
|
||||
|
if (wrap.type !== 'MeshWrap') { |
||||
|
throw new Error(`无法拾取物品 ${id},它不是一个有效的 MeshWrap`) |
||||
|
} |
||||
|
item.dt.storeAt = { |
||||
|
item: this.id |
||||
|
} |
||||
|
const mesh = wrap.manager.wrapToObject3D(wrap) |
||||
|
this.viewport.entityManager.replaceObject(id, mesh) |
||||
|
return mesh |
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Sets the `THREE.Object3D` object for the entity. |
* 卸下物品 |
||||
* @param object3D - The `THREE.Object3D` object to set. |
|
||||
*/ |
*/ |
||||
setObjects(objects: THREE.Object3D[]): void { |
dropItem(item: ItemJson, storeItemId: string, bay?: number, level?: number, cell?: number): void { |
||||
this.objects = objects |
item.dt.storeAt = { |
||||
|
item: storeItemId, |
||||
|
bay: bay, |
||||
|
level: level, |
||||
|
cell: cell |
||||
|
} |
||||
|
const rack = Model.find(storeItemId) |
||||
|
const rackRenderer = getRenderer(rack.t) |
||||
|
const itemRenderer = getRenderer(item.t) |
||||
|
if (!rackRenderer) { |
||||
|
throw new Error(`Renderer for type ${item.t} not found`) |
||||
|
} |
||||
|
if (!itemRenderer) { |
||||
|
throw new Error(`Renderer for type ${item.t} not found`) |
||||
|
} |
||||
|
|
||||
|
const { position, rotation } = rackRenderer.getStorePlacement(this.viewport, item) |
||||
|
if (!position || !rotation) { |
||||
|
throw new Error(`无法获取物品 ${item.id} 的存储位置`) |
||||
|
} |
||||
|
|
||||
|
this.getArmObject().clear() |
||||
|
|
||||
|
itemRenderer.tempViewport = this.viewport |
||||
|
itemRenderer.createOrUpdatePointForRuntime(item, position, rotation) |
||||
|
itemRenderer.tempViewport = null |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1 @@ |
|||||
|
|
||||
@ -0,0 +1,98 @@ |
|||||
|
/** |
||||
|
* 任务类型定义 |
||||
|
*/ |
||||
|
export interface Task { |
||||
|
type: string |
||||
|
|
||||
|
execute(): Promise<void> |
||||
|
|
||||
|
cancel?(): void |
||||
|
|
||||
|
isCancelable?: boolean |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 任务队列管理类 |
||||
|
*/ |
||||
|
export default class TaskQueue { |
||||
|
private queue: Task[] = [] |
||||
|
private currentTask: Task | null = null |
||||
|
private isRunning = false |
||||
|
private isPaused = false |
||||
|
private onTaskComplete: ((task: Task) => void) | null = null |
||||
|
|
||||
|
constructor() { |
||||
|
} |
||||
|
|
||||
|
get length(): number { |
||||
|
return this.queue.length + (this.currentTask ? 1 : 0) |
||||
|
} |
||||
|
|
||||
|
get isExecuting(): boolean { |
||||
|
return this.isRunning && !this.isPaused |
||||
|
} |
||||
|
|
||||
|
add(task: Task): void { |
||||
|
this.queue.push(task) |
||||
|
} |
||||
|
|
||||
|
clear(): void { |
||||
|
this.queue = [] |
||||
|
} |
||||
|
|
||||
|
pause(): void { |
||||
|
this.isPaused = true |
||||
|
if (this.currentTask && this.currentTask.cancel) { |
||||
|
this.currentTask.cancel() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
resume(): void { |
||||
|
this.isPaused = false |
||||
|
if (this.currentTask) { |
||||
|
this.executeNextTask() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancelAll(): void { |
||||
|
this.isRunning = false |
||||
|
this.isPaused = false |
||||
|
this.queue = [] |
||||
|
if (this.currentTask && this.currentTask.cancel) { |
||||
|
this.currentTask.cancel() |
||||
|
} |
||||
|
this.currentTask = null |
||||
|
if (this.onTaskComplete) { |
||||
|
this.onTaskComplete(this.currentTask || { type: 'CANCELLED' } as Task) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
start(onTaskComplete?: (task: Task) => void): void { |
||||
|
this.onTaskComplete = onTaskComplete |
||||
|
if (!this.isRunning) { |
||||
|
this.isRunning = true |
||||
|
this.executeNextTask() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async executeNextTask(): Promise<void> { |
||||
|
if (this.isPaused || this.queue.length === 0) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.currentTask = this.queue.shift()! |
||||
|
|
||||
|
try { |
||||
|
await this.currentTask.execute() |
||||
|
if (this.onTaskComplete) { |
||||
|
this.onTaskComplete(this.currentTask) |
||||
|
} |
||||
|
this.currentTask = null |
||||
|
this.executeNextTask() |
||||
|
} catch (error) { |
||||
|
console.error('Task execution failed:', error) |
||||
|
this.currentTask = null |
||||
|
this.executeNextTask() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
import BaseEntity from '@/core/base/BaseItemEntity.ts' |
||||
|
import type Viewport from '@/core/engine/Viewport.ts' |
||||
|
import gsap from 'gsap' |
||||
|
import { nextTick } from 'vue' |
||||
|
|
||||
|
/** |
||||
|
* CL2 机械臂实体类 |
||||
|
*/ |
||||
|
export default class Cl2Entity extends BaseEntity { |
||||
|
constructor(viewport: Viewport, id: string) { |
||||
|
super(viewport, id) |
||||
|
} |
||||
|
|
||||
|
// 装
|
||||
|
addLoad(item: string): void { |
||||
|
this.taskQueue.add(this.createTask('LOAD', async () => { |
||||
|
// 实际业务中应包含装载逻辑
|
||||
|
this.isCarrying = true |
||||
|
|
||||
|
const arm = this.getArmObject() |
||||
|
|
||||
|
// 抬高20cm
|
||||
|
this.isLoading = true |
||||
|
gsap.to(arm.position, { |
||||
|
y: arm.position.y + 0.2, |
||||
|
duration: 0.5, |
||||
|
ease: 'sine.inOut', |
||||
|
onComplete: () => { |
||||
|
this.isCarrying = true |
||||
|
this.isLoading = false |
||||
|
const mesh = this.pickupItem(item) |
||||
|
const ptrForkMesh = this.getArmObject() |
||||
|
mesh.position.set(0, 0.1, 0) |
||||
|
mesh.rotation.set(0, THREE.MathUtils.degToRad(90), 0) |
||||
|
ptrForkMesh.add(mesh) |
||||
|
} |
||||
|
}) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 卸
|
||||
|
addUnload(itemId: string, rackId: string, bay?: number, level?: number, cell?: number): void { |
||||
|
this.taskQueue.add(this.createTask('UNLOAD', async () => { |
||||
|
const item = this.viewport.entityManager.findItemById(itemId) |
||||
|
this.isCarrying = false |
||||
|
this.isUnloading = false |
||||
|
this.dropItem(item, rackId, bay, level) |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 帮助方法:获取机械臂对象
|
||||
|
getArmObject(): THREE.Object3D | undefined { |
||||
|
const agv = this.object as THREE.Group |
||||
|
if (agv.children.length > 1) { |
||||
|
const pillar = agv.children[1] |
||||
|
if (pillar.children.length > 1) { |
||||
|
return pillar.children[1] |
||||
|
} |
||||
|
} |
||||
|
return undefined |
||||
|
} |
||||
|
|
||||
|
// 帮助方法:获取机械臂立柱
|
||||
|
getArmPillar(): THREE.Object3D | undefined { |
||||
|
const agv = this.object as THREE.Group |
||||
|
if (agv.children.length > 1) { |
||||
|
return agv.children[1] |
||||
|
} |
||||
|
return undefined |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
import { defineModule } from '@/core/manager/ModuleManager.ts' |
||||
|
import Cl2Renderer from './Cl2Renderer.ts' |
||||
|
import Cl2Entity from './Cl2Entity.ts' |
||||
|
import Cl2Interaction from './Cl2Interaction.ts' |
||||
|
import propertySetter from "./Cl2PropertySetter.ts"; |
||||
|
|
||||
|
export const ITEM_TYPE_NAME = 'cl2' |
||||
|
|
||||
|
export default defineModule({ |
||||
|
name: ITEM_TYPE_NAME, |
||||
|
renderer: new Cl2Renderer(ITEM_TYPE_NAME), |
||||
|
interaction: new Cl2Interaction(ITEM_TYPE_NAME), |
||||
|
setter: propertySetter, |
||||
|
entity: Cl2Entity, |
||||
|
}) |
||||
@ -1,5 +0,0 @@ |
|||||
import BaseEntity from '@/core/base/BaseItemEntity.ts' |
|
||||
|
|
||||
export default class PtrEntity extends BaseEntity { |
|
||||
|
|
||||
} |
|
||||
@ -1,15 +0,0 @@ |
|||||
import { defineModule } from '@/core/manager/ModuleManager.ts' |
|
||||
import PtrRenderer from './PtrRenderer.ts' |
|
||||
import PtrEntity from './PtrEntity.ts' |
|
||||
import PtrInteraction from './PtrInteraction.ts' |
|
||||
import propertySetter from "@/modules/ptr/PtrPropertySetter.ts"; |
|
||||
|
|
||||
export const ITEM_TYPE_NAME = 'ptr' |
|
||||
|
|
||||
export default defineModule({ |
|
||||
name: ITEM_TYPE_NAME, |
|
||||
renderer: new PtrRenderer(ITEM_TYPE_NAME), |
|
||||
interaction: new PtrInteraction(ITEM_TYPE_NAME), |
|
||||
setter: propertySetter, |
|
||||
entity: PtrEntity, |
|
||||
}) |
|
||||
Loading…
Reference in new issue