23 changed files with 1144 additions and 430 deletions
@ -1,26 +1,285 @@ |
|||
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 { |
|||
protected itemJson!: ItemJson |
|||
protected objects!: THREE.Object3D[] |
|||
export default abstract class BaseEntity implements EntityIf, Loadable, Carry, Walk, ForkArm, LiftingArm { |
|||
protected readonly viewport: Viewport |
|||
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 { |
|||
this.itemJson = itemJson |
|||
pickupItem(id: string): THREE.Object3D { |
|||
// 找到物品所在的地方, 删除他
|
|||
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 { |
|||
this.objects = objects |
|||
dropItem(item: ItemJson, storeItemId: string, bay?: number, level?: number, cell?: number): void { |
|||
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