From afd6d11f9abcb469641b78d7839221a74481dbf1 Mon Sep 17 00:00:00 2001 From: luoyifan Date: Mon, 26 May 2025 19:08:29 +0800 Subject: [PATCH] WorldModel.Measure --- src/model/ModelUtils.ts | 49 +++++++++ src/model/WorldModel.ts | 24 ++++- src/model/example1.js | 53 ++++++---- src/model/itemTypeDefine/ItemTypeBase.ts | 29 ++++++ src/model/itemTypeDefine/ItemTypeDefine.ts | 131 ++++++++++++++++++++++++ src/model/itemTypeDefine/ItemTypeLineBase.ts | 128 +++++++++++++++++++++++ src/model/itemTypeDefine/measure/Measure.ts | 71 +++++++++++++ src/model/itemTypeDefine/measure/MeasureMeta.ts | 8 +- src/runtime/DefineItem.ts | 28 ----- src/runtime/DefineItemType.ts | 28 +++++ 10 files changed, 498 insertions(+), 51 deletions(-) create mode 100644 src/model/ModelUtils.ts create mode 100644 src/model/itemTypeDefine/ItemTypeBase.ts create mode 100644 src/model/itemTypeDefine/ItemTypeDefine.ts create mode 100644 src/model/itemTypeDefine/ItemTypeLineBase.ts create mode 100644 src/model/itemTypeDefine/measure/Measure.ts delete mode 100644 src/runtime/DefineItem.ts create mode 100644 src/runtime/DefineItemType.ts diff --git a/src/model/ModelUtils.ts b/src/model/ModelUtils.ts new file mode 100644 index 0000000..c01645c --- /dev/null +++ b/src/model/ModelUtils.ts @@ -0,0 +1,49 @@ +import * as THREE from 'three' +import type { ItemJson } from '@/model/itemTypeDefine/ItemTypeDefine.ts' +import { getAllItemTypes, getItemTypeByName } from '@/runtime/DefineItemType.ts' + +export function loadSceneFromJson(scene: THREE.Scene, items: ItemJson[]) { + const object3ds = loadObject3DFromJson(items) + + // 通知所有加载的对象, 模型加载完成 + getAllItemTypes().forEach(itemType => { + if (typeof itemType.clazz.afterLoadComplete === 'function') { + itemType.clazz.afterLoadComplete(object3ds) + } + }) + + object3ds.forEach(object3D => { + scene.add(object3D) + }) + + // 通知所有加载的对象, 模型加载完成 + getAllItemTypes().forEach(itemType => { + itemType.clazz.afterAddScene(scene, object3ds) + }) +} + +function loadObject3DFromJson(items: ItemJson[]): THREE.Object3D[] { + const result: THREE.Object3D[] = [] + + for (const item of items) { + if (!item || !item.t) { + console.error('unkown item:', item) + continue + } + + const object3D: THREE.Object3D | undefined = getItemTypeByName(item.t)?.clazz.loadFromJson(item) + if (object3D === undefined) { + continue + } + + if (_.isArray(item.items)) { + // 如果有子元素,递归处理 + const children = loadObject3DFromJson(item.items) + children.forEach(child => object3D.add(child)) + } + + result.push(object3D) + } + + return result +} \ No newline at end of file diff --git a/src/model/WorldModel.ts b/src/model/WorldModel.ts index 7eae62b..2df41bd 100644 --- a/src/model/WorldModel.ts +++ b/src/model/WorldModel.ts @@ -1,9 +1,12 @@ import _ from 'lodash' import Example1 from './example1' import { markRaw, reactive } from 'vue' +import * as THREE from 'three' import { Scene } from 'three' import type Viewport from '@/designer/Viewport.ts' -import * as THREE from 'three' +import { loadSceneFromJson } from '@/model/ModelUtils.ts' + +import MeasureMeta from './itemTypeDefine/measure/MeasureMeta' /** * 世界模型 @@ -21,6 +24,23 @@ export default class WorldModel { init() { window['worldModel'] = this + + Promise.all([ + MeasureMeta.clazz.init(this) + + ]).then(() => { + console.log('世界模型初始化完成') + }) + } + + loadFloorToScene(scene: THREE.Scene, levelCode: string) { + const floor = _.find(this.data.items, r => r.name === levelCode && r.t === 'floor') + if (!floor) { + console.warn(`未找到楼层数据: ${levelCode}`) + return [] + } + + loadSceneFromJson(scene, floor.items) } open() { @@ -63,6 +83,8 @@ export default class WorldModel { createScene(floor: string) { const scene = new Scene() scene.background = new THREE.Color(0xeeeeee) + + this.loadFloorToScene(scene, floor) return scene } diff --git a/src/model/example1.js b/src/model/example1.js index 77c3e94..89a9500 100644 --- a/src/model/example1.js +++ b/src/model/example1.js @@ -13,34 +13,49 @@ export default { { name: 'OnStop', fn: '' } ] }, - item: [ + items: [ { - name: 'f1', type: 'floor', uuid: 'f1', + name: 'f1', t: 'floor', // 楼层 items: [ { - uuid: 'measure-group', type: 'group', items: [ + name: 'measure-group', t: 'measure', a: 'gp', // 类型, itemType.name == 'measure' 的组件处理. a:'gp' 代表分组, 渲染时他会是 Three.Group + items: [ { - uuid: 'p1', - type: 'measure', - category: 'line', - pos: [], - rotation: [0, 0, 0], - scale: [1, 1, 1], - link: ['p2'] + id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid + t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 + a: 'ln', // 交互类型, ln表示线点操作, pt 表示点操作 + l: '测量1', // 标签名称, 显示用 + c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 + tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + [-9.0, 0, -1.0], // 平移向量 position + [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + [0.25, 0.1, 0.25] // 缩放向量 scale + ], + dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + link: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) + center: [], // 物流关联对象(uuid) + in: [], // 物流入方向关联的对象(uuid) + out: [] // 物流出方向关联的对象(uuid) + } }, { - uuid: 'p2', - type: 'measure', - category: 'line', - pos: [], - rotation: [0, 0, 0], - scale: [1, 1, 1], - link: ['p3'] + id: 'p2', + t: 'measure', a: 'ln', l: '测量2', c: '#ff0000', + tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + link: ['p3'] + } }, - { uuid: 'p3', type: 'measure', category: 'line', pos: [], rotation: [0, 0, 0], scale: [1, 1, 1], link: [] } + { + id: 'p3', + t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', + tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + link: [] + } + } ] } - ] } ], diff --git a/src/model/itemTypeDefine/ItemTypeBase.ts b/src/model/itemTypeDefine/ItemTypeBase.ts new file mode 100644 index 0000000..a825ca9 --- /dev/null +++ b/src/model/itemTypeDefine/ItemTypeBase.ts @@ -0,0 +1,29 @@ +import { type Object3D } from 'three' +import type WorldModel from '@/model/WorldModel.ts' +import type { ItemJson, ItemTypeDefineOption } from '@/model/itemTypeDefine/ItemTypeDefine.ts' +import * as THREE from 'three' + +export default abstract class ItemTypeBase { + name: string + option: ItemTypeDefineOption + worldModel: WorldModel + + public init(worldModel: WorldModel) { + this.worldModel = worldModel + + // 初始化方法,子类可以重写 + return Promise.resolve() + } + + abstract loadFromJson(item: ItemJson): undefined | THREE.Object3D + + afterLoadComplete(objects: THREE.Object3D[]): THREE.Object3D[] { + return [] + } + + /** + * 添加到 scene 后的回调 + */ + afterAddScene(scene: THREE.Scene, objects: THREE.Object3D[]): void { + } +} \ No newline at end of file diff --git a/src/model/itemTypeDefine/ItemTypeDefine.ts b/src/model/itemTypeDefine/ItemTypeDefine.ts new file mode 100644 index 0000000..8fd5fd3 --- /dev/null +++ b/src/model/itemTypeDefine/ItemTypeDefine.ts @@ -0,0 +1,131 @@ +import type ItemTypeBase from '@/model/itemTypeDefine/ItemTypeBase.ts' + +export type ActionType = +/** + * 线类型 + */ + 'ln' | + /** + * 点类型 + */ + 'pt' | + /** + * 物流运输单元 + */ + 'fl' | + /** + * 分组单元,仅用于分组 + */ + 'gp' + +export interface ItemTypeDefineOption { + name: string + label: string + actionType: ActionType + clazz: ItemTypeBase +} + +/** + * 定义物体类型的元数据 + * 举例: + * { + * id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid + * t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 + * a: 'ln', // 交互类型, ln表示线点操作, pt 表示点操作 + * l: '测量1', // 标签名称, 显示用 + * c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 + * tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + * [-9.0, 0, -1.0], // 平移向量 position + * [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + * [0.25, 0.1, 0.25] // 缩放向量 scale + * ], + * dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + * link: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) + * center: [], // 物流关联对象(uuid) + * in: [], // 物流入方向关联的对象(uuid) + * out: [] // 物流出方向关联的对象(uuid) + * } + * } + */ +export interface ItemJson { + /** + * 物体名称, 显示用, 最后初始化到 three.js 的 name 中, 可以不设置, 可以不唯一 + */ + name?: string + + /** + * 对应 three.js 中的 uuid, 物体ID, 唯一标识, 需保证唯一 + */ + id: string + + /** + * 物体类型, 对应 defineItemType.name + */ + t: string + + /** + * 交互类型 + */ + a: ActionType + + /** + * 标签名称, 显示用, 最后初始化到 three.js 的 userData.label 中 + */ + l?: string + + /** + * 颜色, 最后初始化到 three.js 的 userData.color 中 + */ + c: string + + /** + * 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + */ + tf: [ + /** + * 平移向量 position, 三维坐标 + */ + [number, number, number], + /** + * 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + */ + [number, number, number], + /** + * 缩放向量 scale, 三维缩放比例 + */ + [number, number, number], + ] + + /** + * 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + */ + dt: { + /** + * 测量线段关联的点对象(uuid), 仅在 a='ln' 时有效 + */ + link?: string[] + + /** + * 物流关联对象(uuid) + */ + center?: string[] + /** + * 物流入方向关联的对象(uuid) + */ + in?: string[] + /** + * 物流出方向关联的对象(uuid) + */ + out?: string[] + + /** + * 其他自定义数据, 可以存储任何数据 + */ + [key: string]: any + }, + + /** + * 子元素, 用于分组等, 可以为空数组 + */ + items: ItemJson[] +} \ No newline at end of file diff --git a/src/model/itemTypeDefine/ItemTypeLineBase.ts b/src/model/itemTypeDefine/ItemTypeLineBase.ts new file mode 100644 index 0000000..fa1c074 --- /dev/null +++ b/src/model/itemTypeDefine/ItemTypeLineBase.ts @@ -0,0 +1,128 @@ +import * as THREE from 'three' +import ItemTypeBase from '@/model/itemTypeDefine/ItemTypeBase.ts' +import type { ItemJson } from '@/model/itemTypeDefine/ItemTypeDefine.ts' +import type WorldModel from '@/model/WorldModel.ts' + +/** + * ILineType 接口定义了线类型的基本方法 + * 用于创建点和线 + */ +export default abstract class ItemTypeLineBase extends ItemTypeBase { + + /** + * 所有点的数组 + */ + pointArray: THREE.Object3D[] = [] + + /** + * 所有连接线的数组 + */ + linkArray: THREE.Object3D[] = [] + + public init(worldModel: WorldModel) { + return super.init(worldModel).then(() => { + // 初始化方法,子类可以重写 + this.pointArray.length = 0 + this.linkArray.length = 0 + }) + } + + afterLoadGroup(group: THREE.Group): void { + } + + afterLoadLine(line: THREE.Line): void { + } + + afterLoadPoint(point: THREE.Object3D): void { + } + + + afterAddScene(scene: THREE.Scene, objects: THREE.Object3D[]) { + super.afterAddScene(scene, objects) + + // 为所有的 pointArray 连接线 + for (let i = 0; i < this.pointArray.length; i++) { + const startPoint = this.pointArray[i] + + // 找到这个元素的 userData.link 数组 + const linkArray: string[] = startPoint.userData.link || [] + + for (let j = 0; j < linkArray.length; j++) { + const linkId = linkArray[j] + // 在 pointArray 中查找对应的点 + const endPoint = this.pointArray.find(p => p.uuid === linkId) + if (!endPoint) { + console.warn('not found link point uuid=${}', linkId) + continue + } + + const line = this.createLine() + const geom = line.geometry + geom.setFromPoints([startPoint.position, endPoint.position]) + + this.afterLoadLine(line) + + if (startPoint.parent) { + startPoint.parent.add(line) + } else { + scene.add(line) + } + + this.linkArray.push(line) + } + } + } + + override loadFromJson(item: ItemJson): undefined | THREE.Object3D { + if (item.a === 'gp') { + // gp 是为了分组而存在的 + const group = new THREE.Group() + group.name = item.name + group.uuid = item.id || THREE.MathUtils.generateUUID() + group.userData = _.cloneDeep(item.dt) || {} + group.userData.type = item.t + group.userData.actionType = item.a + group.userData.label = item.l + group.userData.color = item.c + + this.afterLoadGroup(group) + return group + } + + // 其他情况都是 ln + else if (item.a === 'ln') { + const position = new THREE.Vector3( + item.tf[0][0], + item.tf[0][1], + item.tf[0][2] + ) + + const point = this.createPoint(position) + point.name = item.name + point.uuid = item.id || THREE.MathUtils.generateUUID() + point.userData = _.cloneDeep(item.dt) || {} + point.userData.type = item.t + point.userData.actionType = item.a + point.userData.label = item.l + point.userData.color = item.c + + point.rotation.set( + THREE.MathUtils.degToRad(item.tf[1][0]), + THREE.MathUtils.degToRad(item.tf[1][1]), + THREE.MathUtils.degToRad(item.tf[1][2]) + ) + + point.scale.set(item.tf[2][0], item.tf[2][1], item.tf[2][2]) + this.pointArray.push(point) + + this.afterLoadPoint(point) + return point + } + + console.error('ItemTypeLineBase.loadFromJson: Unsupported', item) + } + + abstract createPoint(position: THREE.Vector3): THREE.Object3D + + abstract createLine(): THREE.Line +} \ No newline at end of file diff --git a/src/model/itemTypeDefine/measure/Measure.ts b/src/model/itemTypeDefine/measure/Measure.ts new file mode 100644 index 0000000..7997496 --- /dev/null +++ b/src/model/itemTypeDefine/measure/Measure.ts @@ -0,0 +1,71 @@ +import * as THREE from 'three' +import ItemTypeLineBase from '@/model/itemTypeDefine/ItemTypeLineBase.ts' +import type WorldModel from '@/model/WorldModel.ts' +import { Material } from 'three/src/materials/Material' + +export const TYPE_NAME = 'measure' +export const POINT_NAME = 'measure_point' +export const LABEL_NAME = 'measure_label' +export const LINE_NAME = 'measure_line' + +export default class Measure extends ItemTypeLineBase { + /** + * 当前测绘内容组, 所有测量点、线、标签都在这个组中. 但不包括临时点、线 + */ + group: THREE.Group + + pointMaterial!: Material + + lineMaterial!: Material + + override init(worldModel: WorldModel): Promise { + super.init(worldModel) + + this.lineMaterial = new THREE.LineBasicMaterial({ + color: 0xE63C17, + linewidth: 2, + opacity: 0.9, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + depthTest: false + }) + + this.pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) + return Promise.resolve() + } + + // 创建完线之后,创建 label + afterLoadLine(line: THREE.Line) { + super.afterLoadLine(line) + } + + /** + * 创建测量点 + */ + createPoint(position?: THREE.Vector3): THREE.Object3D { + const p = position + const scale = 0.25 + + const tt = new THREE.BoxGeometry(1, 1, 1) + const obj = new THREE.Mesh(tt, this.pointMaterial) + obj.scale.set(scale, 0.1, scale) + if (p) { + obj.position.set(p.x, p.y, p.z) + } + obj.name = POINT_NAME + return obj + } + + /** + * 创建测量线 + */ + createLine(): THREE.Line { + const geom = new THREE.BufferGeometry() + const obj = new THREE.Line(geom, this.lineMaterial) + obj.frustumCulled = false + obj.name = LINE_NAME + obj.uuid = THREE.MathUtils.generateUUID() + return obj + } +} \ No newline at end of file diff --git a/src/model/itemTypeDefine/measure/MeasureMeta.ts b/src/model/itemTypeDefine/measure/MeasureMeta.ts index 195915c..3b7d409 100644 --- a/src/model/itemTypeDefine/measure/MeasureMeta.ts +++ b/src/model/itemTypeDefine/measure/MeasureMeta.ts @@ -1,7 +1,9 @@ -import { defineItemType } from '@/runtime/DefineItem.ts' +import { defineItemType } from '@/runtime/DefineItemType.ts' +import Measure from '@/model/itemTypeDefine/measure/Measure.ts' export default defineItemType({ name: 'measure', - label: '测量工具', - category: 'line' + label: '测量距离', + actionType: 'ln', + clazz: new Measure() }) \ No newline at end of file diff --git a/src/runtime/DefineItem.ts b/src/runtime/DefineItem.ts deleted file mode 100644 index 56de6bb..0000000 --- a/src/runtime/DefineItem.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 定义一个 物流单元 - */ -export class ItemTypeDefine { - option!: ItemOption - - constructor(option: ItemOption) { - this.option = option - } -} - -export type ItemCategory = 'point' | 'line' | 'store' | 'executer' | 'flow_item' | 'other' - -export interface ItemOption { - name: string - label: string - category: ItemCategory -} - -/** - * 定义一个 物流单元 - */ -export function defineItemType(option: ItemOption): Promise { - return new Promise((resolve, reject) => { - const item = new ItemTypeDefine(option) - resolve(item) - }) -} \ No newline at end of file diff --git a/src/runtime/DefineItemType.ts b/src/runtime/DefineItemType.ts new file mode 100644 index 0000000..adfc34b --- /dev/null +++ b/src/runtime/DefineItemType.ts @@ -0,0 +1,28 @@ +import _ from 'lodash' +import type { ItemTypeDefineOption } from '@/model/itemTypeDefine/ItemTypeDefine.ts' + +const itemTypes: Record = {} +window['itemTypes'] = itemTypes + +/** + * 定义一个 物流单元 + */ +export function defineItemType(option: ItemTypeDefineOption) { + itemTypes[option.name] = option + option.clazz.name = option.name + option.clazz.option = option + return option +} + +export function getItemTypeByName(type: string): ItemTypeDefineOption { + const itemType = _.get(itemTypes, type) + if (!itemType) { + console.warn(`未找到物流单元类型定义: ${type}`) + return + } + return itemType +} + +export function getAllItemTypes(): ItemTypeDefineOption[] { + return Object.values(itemTypes) +} \ No newline at end of file