You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
24 KiB
24 KiB
物流世界
一个物流仓库, 就是一个世界
他有自己的项目定义, 楼层, 围墙, 柱子, 其他数据, 个性化脚本等等
每次对建模文件打开的时候, 因为性能问题, 一次只会读取一个楼层, 或者一个水平横截面.
因此, 一个 floor 就是对应一个 THREE.Scene
物流世界数据示例
export default {
project_uuid: 'example1',
Tool: {
Group: [], // 分组
GlobalVariables: [], // 全局变量
UserCommand: [], // 全局脚本
Dashboard: [], // 监控面板
DataTable: [], // 地图自带的数据
Trigger: [ // 触发器
{ name: 'OnOpen', fn: '' }, // 打开
{ name: 'OnReset', fn: '' }, // 仿真重置
{ name: 'OnStart', fn: '' }, // 开始仿真
{ name: 'OnStop', fn: '' } // 停止仿真
],
gridHelper: { // 网格辅助线
axesEnabled: true, // 是否显示中心轴
axesSize: 50, // 中心轴长度
axesDivisions: 2, // 中心轴分割数
axesColor: 0x000000, // 中心轴颜色
axesOpacity: 1, // 中心轴透明度
gridEnabled: true, // 是否显示网格
gridSize: 1000, // 网格大小
gridDivisions: 1000, // 网格分割数
gridColor: 0x999999, // 网格颜色
gridOpacity: 0.8, // 网格透明度
snapEnabled: true, // 是否启用吸附
snapDistance: 0.25 // 吸附距离
}
},
items: [
{
catalogCode: 'f1', t: 'floor', // 楼层
items: [
{
id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid
t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理
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 中
label: '测量1', // 标签名称, 显示用
color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色
center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid)
in: [], // 物流入方向关联的对象(id)
out: [] // 物流出方向关联的对象(id)
}
},
{
id: 'p2',
t: 'measure',
tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]],
dt: {
color: '#ff0000',
label: '测量2',
center: ['p1', 'p3', 'p4']
}
},
{
id: 'p3', t: 'measure',
tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]],
dt: {
label: '测量3',
center: ['p2']
}
},
{
id: 'p4',
t: 'measure',
tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]],
dt: {
label: '测量4',
center: ['p2']
}
}
]
}
],
elevator: [], // 电梯
wall: [], // 墙体
pillar: [], // 柱子
catalog: [ // 目录
{
label: '仓库楼层', // 目录分组名
items: [
{ catalogCode: '-f1', label: '地下室 (-f1)' }, // 目录项
{ catalogCode: 'f1', label: '一楼 (f1)' },
{ catalogCode: 'f2', label: '二楼 (f2)' },
{ catalogCode: 'OUT', label: '外场 (OUT)' },
{ catalogCode: 'fe', label: '楼层电梯 (fe)' }
]
},
{
label: '密集库区域',
items: [
{ catalogCode: 'm1', label: 'M1 (m1)' },
{ catalogCode: 'm2', label: 'M2 (m2)' },
{ catalogCode: 'm3', label: 'M3 (m3)' },
{ catalogCode: 'm4', label: 'M4 (m4)' },
{ catalogCode: 'me', label: '提升机 (me)' }
]
},
{
label: '多穿库A',
items: [
{ catalogCode: 'd1', label: 'D1 (d1)' },
{ catalogCode: 'd2', label: 'D2 (d2)' },
{ catalogCode: 'd3', label: 'D3 (d3)' },
{ catalogCode: 'd4', label: 'D4 (d4)' },
{ catalogCode: 'de1', label: '提升机 (de1)' }
]
},
{
label: '多穿库B',
items: [
{ catalogCode: 'e1', label: 'E1 (e1)' },
{ catalogCode: 'e2', label: 'E2 (e2)' },
{ catalogCode: 'e3', label: 'E3 (e3)' },
{ catalogCode: 'e4', label: 'E4 (e4)' },
{ catalogCode: 'ee1', label: '提升机 (ee1)' }
]
}
]
}
物流世界模型
/**
* 物流世界模型
*/
export default class WorldModel {
/**
* 世界模型双向绑定的状态数据
*/
state: WorldModelState = reactive({
isOpened: false, // 是否已打开世界模型
catalogCode: '', // 当前楼层的目录代码
isDraft: false, // 是否是草稿数据, 如果是草稿数据, 则不需要再从服务器加载数据
stateManagerId: '', // 当前楼层的状态管理器id, 一般是 项目ID+目录项ID
catalog: [] as Catalog // 世界模型目录
})
/**
* 初始化世界模型
*/
init()
/**
* 读取世界地图数据目录
*/
loadCatalog(data: any)
/**
* 当楼层发生改变时调用此方法, 将事件派发出去
*/
onCatalogCodeChanged(catalogCode: string)
/**
* 从服务器获取当前目录楼层的所有数据
*/
async getCatalogData(catalogCode: string): Promise<Partial<VData>>
}
状态管理器 StateManager
/**
数据状态管理器
他能够对数据进行载入, 保存草稿, 载入草稿, 数据增删改查,撤销、重做等操作
最终他会将数据的变化同步到 EntityManager 中
主要功能包括:
1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能
2. 管理撤销、重做功能
3. 交互控制组件通过 update() 方法修改数据
this.viewport.stateManager.update(({ getEntity, putEntity, deleteEntity, addEntity }) => {
const entity = getEntity(id) // 获取实体
entity.abc = 123
putEntity(entity) // 提交修改
deleteEntity(id) // 删除实体
addEntity(newEntity) // 添加新实体
})
4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 EntityManager 进行同步
5. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作
主要难点:
- 单张地图数据量可能超过 10000 个对象, 需要高效的管理数据状态
*/
export default class StateManager {
/**
* 唯一场景标识符, 用于做临时存储的 key
*/
readonly id: string
/**
* 实体对象, 用于同步当前场景的状态
*/
readonly entityManager: EntityManager
/**
* 是否发生了变化,通知外部是否需要保存数据
*/
readonly isChanged = ref(false)
/**
* 是否正在加载数据,通知外部是否需要等待加载完成
*/
readonly isLoading = ref(false)
readonly storeKey: string
/**
* 当前场景数据
*/
vdata: VData
/**
* 使用循环缓冲区存储历史记录
*/
private historySteps: HistoryStep[] = []
private historyIndex = -1
private maxHistorySteps = 20
// 变化追踪器
private readonly changeTracker: DataDiff = {
added: [],
removed: [],
updated: []
}
/**
* 数据快照(用于差异计算)
*/
private lastStateDict = new Map<string, ItemJson>()
/**
* @param id 唯一场景标识符, 用于做临时存储的 key
* @param viewport 视口对象
* @param maxHistorySteps 最大回撤步数 默认为 20
*/
constructor(id: string, viewport: Viewport, maxHistorySteps = 20)
/**
* 将当前数据 与 entityManager 进行同步, 对比出不同的部分,分别进行更新
* - 调用 entityManager.beginEntityUpdate() 开始更新
* - 调用 entityManager.createOrUpdateEntity(vdataItem) 添加场景中新的实体
* - 调用 entityManager.deleteEntity(id) 删除场景中的实体
* - 调用 entityManager.endEntityUpdate() 结束更新场景
*/
syncDataState(diff: DataDiff): void
/**
* 从外部加载数据
*/
async load(items: VDataItem[])
/**
* 保存数据到外部
*/
async save(): Promise<Object>
/**
* 撤销
*/
undo()
/**
* 重做
*/
redo()
/**
* 使用 updater 函数更新状态, 并同步转换为实体和渲染
*/
update(updaterFn: (updater: StateUpdater) => void): void
/**
* 保存到本地存储 浏览器indexDb(防止数据丢失)
*/
async saveToLocalstore()
/**
* 从本地存储还原数据
*/
async loadFromLocalstore()
/**
* 删除本地存储
*/
async removeLocalstore()
}
// 状态更新器接口
interface StateUpdater {
getEntity(id: string): ItemJson
putEntity(entity: ItemJson): void // 修改已有实体
deleteEntity(id: string): boolean // 删除实体
addEntity(entity: ItemJson): void // 添加新实体
}
单元点数据 ItemJson
/**
* 物体单元(点)
*/
export interface ItemJson {
/**
* 对应 three.js 中的 uuid, 物体ID, 唯一标识, 需保证唯一, 有方法可以进行快速的 O(1) 查找
*/
id?: string
/**
* 物体名称, 显示用, 最后初始化到 three.js 的 name 中, 可以不设置, 可以不唯一, 但他的查找速度是 O(N)
*/
name?: string
/**
* "点"的物体单元类型, 最终对应到 measure / conveyor / task 等不同的单元处理逻辑中
*/
t: 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: {
/**
* 标签名称, 显示用, 最后初始化到 three.js 的 userData.label 中, 最终应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑
*/
label?: string
/**
* 颜色, 最后初始化到 three.js 的 userData.color 中, 最终颜色应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑
*/
color?: string
/**
* S连线(又称逻辑连线), 与其他点之间的无方向性关联, 关系的起点需要在他的 dt.center[] 数组中添加目标点的id, 关系的终点需要在他的 dt.center[] 数组中添加起点的 id
*/
center?: string[]
/**
* A连线(又称物体流动线)的输入, 关系的终点需要在 dt.in[] 数组中添加起点的 id
*/
in?: string[]
/**
* A连线(又称物体流动线)的输出, 关系的起点需要在 dt.out[] 数组中添加目标点的 id
*/
out?: string[]
/**
* 是否可以被选中, 默认 true
*/
selectable?: boolean
/**
* 是否受保护, 不可在图形编辑器中拖拽, 默认 false
*/
protected?: boolean
/**
* 在 ThreeJs 中, 这个点应当属于哪个父元素
*/
parentId?: string
/**
* 其他自定义数据, 可以存储任何数据
*/
[key: string]: any
},
}
场景和视窗
场景 SceneHelp
/**
* 场景对象
* 通常是某个楼层的所有物品, 对应一个 Three.Scene, 每个场景可能会有多个不同的 Viewport 对他进行观察
*/
export default class SceneHelp {
scene: THREE.Scene
}
视窗 Viewport
/**
* 视窗对象
* 所有状态管理器,场景,控制器,摄像机,实体管理器, 都在这里可以取到
*/
export default class Viewport {
sceneHelp: SceneHelp
viewerDom: HTMLElement
camera: THREE.OrthographicCamera
renderer: THREE.WebGLRenderer
statsControls: Stats
controls: OrbitControls
raycaster: THREE.Raycaster
dragControl: EsDragControls
animationFrameId: any = null
constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement)
}
实体管理器 EntityManager
/**
实体管理器
缓存所有 数据(ItemJson)和他们的关系, 以及渲染对象 THREE.Object3D
数据更新的流程是:
1. 状态管理器 StateManager 发出数据被修改的请求, 这些请求通常只有 ItemJson 数据
2. 实体管理器 (EntityManager) 需要计算所有相关的点和线的变化. 在 endUpdate 时需要根据 center / in / out 关系, 计算出: 点的创建 / 点的更新 / 点的删除 / 线的创建 / 线的更新 / 线的删除
3. 通知对应的渲染器 (Renderer) 进行点和线的创建 / 更新 / 删除
*/
export class EntityManager {
viewport: Viewport
// 所有数据点的实体
private readonly entities = new Map<string, ItemJson>()
// 关系索引
private readonly relationIndex = new Map<string, Relation>()
// 所有 THREEJS "点"对象, 检索值是"点实体"的 id, 值是 THREE.Object3D 数组
private readonly objects = new Map<string, THREE.Object3D[]>()
// 所有 THREEJS "线"对象, 检索值是"线实体"的 id, 取值方式是 {type}${startId}${endId}, 值是 THREE.Object3D 数组
private readonly lines = new Map<string, THREE.Object3D[]>()
// 差量渲染器
private readonly diffRenderer = new Map<string, BaseRenderer>()
// 线差量记录
private readonly lineDiffs = {
create: new Map<string, LineDiffItem>(),
update: new Map<string, LineDiffItem>(),
delete: new Map<string, LineDiffItem>()
}
constructor(viewport: Viewport)
/**
* 批量更新开始
*/
beginEntityUpdate(): void
/**
* 创建或更新一个实体, 这个点的 center[] / in[] / out[] 关联的点, 可能都要对应进行关联
*/
createOrUpdateEntity(entity: ItemJson, option: EntityCudOption = {}): void
/**
* 删除实体, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系
*/
deleteEntity(id: string, option: EntityCudOption = {}): void
/**
* 批量更新结束, 结束后会触发视窗的渲染
* 这个方法最重要的是进行连线逻辑的处理
* - 如果进行了添加, 那么这个点的 center[] / in[] / out[] 关联的点, 都要对应进行关联
* - 如果进行了删除, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系
* - 如果进行了更新, 如果改了颜色/位置, 则需要在UI上进行对应修改,如果改了关系,需要与关联的节点批量调整
* 将影响到的所有数据, 都变成一个修改集合, 统一调用对应单元类型渲染器(BaseRenderer)的 createPoint / deletePoint / updatePoint / createLine / updateLine / deleteLine 方法
*/
endEntityUpdate(): void
/**
* 更新关系关系网, 计算出差值, 可以临时放在 diffRenderer 中, 等待 commitUpdate 时统一处理
*/
private updateRelations(entity: ItemJson): void
private updateReverseRelations(id: string, relatedIds: string[] | undefined, relationType: 'center' | 'in' | 'out'): void
/**
* 删除关系关系网, 计算出差值, 可以临时放在 diffRenderer 中, 等待 commitUpdate 时统一处理
*/
private removeRelations(id: string): void
}
/**
* 关系详情
*/
export class Relation {
center = new Set<string>()
input = new Set<string>()
output = new Set<string>()
add(type: LinkType, id: string): boolean
delete(type: LinkType, id: string): boolean
}
交互管理器 InteractionManager
export default class InteractionManager {
private viewport: Viewport;
/**
* 当前激活的交互工具
*/
private currentTool: BaseInteraction | null = null
constructor(viewport: Viewport) {
this.viewport = viewport;
}
}
物流单元封装
物流单元组件封装, 每个组件类别都会有
- 渲染器 renderer
- 交互控制器 interaction
- 实体定义 entity
- 属性元数据 meta
比如测量组件 Measure, 他属于最基础的线类型物流单元.
基础渲染器 base.renderer
/**
基本渲染器基类
定义了点 / 线如何渲染到 Three.js 场景中.
后期考虑调用 InstancePool 渲染
*/
public export default BaseRenderer {
/**
* 每次 beginUpdate 时记录临时 viewport, endUpdate 之后要马上删除, 因为 BaseRenderer 全局只有一个实例, 而 viewport 有多个
*/
tempViewport?: Viewport = undefined
/**
* 开始更新
* @param viewport 当前视口
*/
beginRendererUpdate(viewport: Viewport): void {
this.tempViewport = viewport
}
/**
* 创建一个最基本的点对象, 不用管 item 的 name / id / 位置 / 转换 / 大小 和 userData, 除非有明确定义
*/
abstract createPointBasic(item: ItemJson, option?: RendererCudOption): THREE.Object3D[]
/**
* 创建测量线
*/
abstract createLineBasic(start: ItemJson, end: ItemJson, type: LinkType): THREE.Object3D[]
/**
* 将对象添加到当前视口的场景中
*/
appendToScene(...objects: THREE.Object3D[]): void
/**
* 创建一个点
* @param item 点的定义
* @param option 渲染选项
*/
createPoint(item: ItemJson, option?: RendererCudOption): void
/**
* 删除一个点
* @param id 点的唯一标识
* @param option 渲染选项
*/
deletePoint(id: string, option?: RendererCudOption): void
/**
* 更新一个点
* @param item 点的定义
* @param option 渲染选项
*/
updatePoint(item: ItemJson, option?: RendererCudOption): void
/**
* 创建一根线
* @param start 起点
* @param end 终点
* @param type 线的类型
* @param option 渲染选项
*/
createLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption): void
/**
* 更新一根线
* @param start 起点
* @param end 终点
* @param type 线的类型
* @param option 渲染选项
*/
updateLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption): void
/**
* 删除一根线
* @param start 起点
* @param end 终点
* @param option 渲染选项
*/
deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption): void
/**
* 结束更新
*/
endRendererUpdate(): void
}
测量尺渲染器 measure.renderer
/**
* 辅助测量工具渲染器
*/
export default class MeasureRenderer extends BaseRenderer {
/**
* 当前测绘内容组, 所有测量点、线、标签都在这个组中. 但不包括临时点、线
*/
group: THREE.Group
static GROUP_NAME = 'measure_group'
static LABEL_NAME = 'measure_label'
static POINT_NAME = 'measure_point'
static LINE_NAME = 'measure_line'
pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 })
lineMaterial = new LineMaterial({
color: 0xE63C17, // 主颜色
linewidth: 2, // 实际可用的线宽
vertexColors: true, // 启用顶点颜色
dashed: false,
alphaToCoverage: true
})
createLineBasic(start: ItemJson, end: ItemJson, type: LinkType): THREE.Object3D[] {
const geom = new LineGeometry()
const obj = new Line2(geom, this.lineMaterial)
obj.frustumCulled = false
obj.name = MeasureRenderer.LINE_NAME
obj.uuid = getLineId(start.id, end.id, type)
return [obj]
}
createPointBasic(item: ItemJson, option?: RendererCudOption): THREE.Object3D[] {
const tt = new THREE.BoxGeometry(1, 1, 1)
const obj = new THREE.Mesh(tt, this.pointMaterial)
obj.name = MeasureRenderer.POINT_NAME
obj.uuid = item.id
return [obj]
}
appendToScene(...objects: THREE.Object3D[]) {
if (!this.group) {
this.group = new THREE.Group()
this.group.name = MeasureRenderer.GROUP_NAME
this.tempViewport?.scene.add(this.group)
}
this.group.add(...objects)
}
}
实例池 InstancePool
{
不知道怎么做
}
基础交互控制器 base.interaction
/**
* 交互控制器基类
* 定义了在建模编辑器里面物流单元, 如何响应鼠标, 键盘的操作.
* 每个物流单元类型, 全局只有一个实例
*/
public default class BaseInteraction {
// 初始化
init(viewport: Viewport)
// 测量工具开始, 监听 Three.Renderer.domElement 的鼠标事件, 有可能用户会指定以某个点为起点, 也有可能第一个点由用户点击而来
start(startPoint?: THREE.Object3D)
// 用户鼠标移动时, 判断是否存在起点, 有起点就要实时构建临时线, 临时线的中间创建一个 Label 显示线的长度. 每次鼠标移动都要进行重新调整
onMousemove(e: MouseEvent)
// 用户如果点左键, 就调用 MeasureRenderer.createPoint 创建点, 如果点击右键就调用 Stop 退出工具
onMouseup(e: MouseEvent)
// 退出这个工具的点击, 停止对 Three.Renderer.domElement 的监听
stop()
// 用户在设计器拽动某个点时触发. 这时调整的都是 "虚点" 和 "虚线"
dragPointStart(point: THREE.Object3D)
// 用户在设计器拖拽完毕后触发, 他会触发 MeasureRenderer.updatePoint 事件
dragPointComplete()
// 在用户开始测量工具 start / 或拖拽 dragPointStart 过程中,会创建临时点和临时线, 辅助用户
// 临时点在一次交互中只会有一个
createOrUpdateTempPoint(e: MouseEvent)
// 临时线在一次交互中, 可能会有多个, 取决于调整的点 有多少关联点, 都要画出虚线和label
createOrUpdateTempLine(label: string, pointStart: THREE.Object3D, pointEnd: THREE.Object3D)
// 清空所有临时点和线
clearTemps()
}
测量尺 交互控制器 measure.interaction
public default class MeasureInteraction extends BaseInteraction {
// 用户在设计器拽动某个点时触发. 这时调整的都是 "虚点" 和 "虚线"
dragPointStart(point: THREE.Object3D)
// 用户在设计器拖拽完毕后触发, 他会触发 MeasureRenderer.updatePoint 事件
dragPointComplete()
}
属性元数据 meta
/**
* 他定义了数据如何呈现在属性面板, 编辑器如果修改, 配合 Entity 该如何进行
*/
export default {
// "点"属性面板
point: {
// 基础面板
basic: [
{ field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true },
{ field: 'name', editor: 'TextInput', label: '名称' },
{ field: 'dt.label', editor: 'TextInput', label: '标签' },
{ editor: 'TransformEditor' },
{ field: 'dt.color', editor: 'Color', label: '颜色' },
{ editor: '-' },
{ field: 'tf', editor: 'InOutCenterEditor' },
{ field: 'dt.selectable', editor: 'Switch', label: '可选中' },
{ field: 'dt.protected', editor: 'Switch', label: '受保护' },
{ field: 'visible', editor: 'Switch', label: '可见' }
]
},
// "线"属性面板
line: {
...xxx
}
}
实体定义 entity
/**
* 基本"点"属性操作代理实体
* 这个对象不易大量存在, 只有在绑定控制面板, 高级属性对话框, 或操作 Modeltree, 或脚本控制的时候才被实例化
* 他提供操作的抽象代理, 最终可能调用 DataStateManager 进行数据的保存
*/
export default class BaseItemEntity {
setItem(itemJson: ItemJson)
setObject(point: THREE.Object3D)
}
/**
* 基本"线"属性操作代理实体
* 这个对象不易大量存在, 只有在绑定控制面板, 高级属性对话框, 或操作 Modeltree, 或脚本控制的时候提供Proxy操作代理
*/
export default class BaseLineEntity {
setItem(start: ItemJson, end: ItemJson)
setObjects(line: THREE.Object3D[])
}
/**
* "线"属性 操作代理实体
*/
export class MeasurePoint extends BaseItemEntity {
... 各种 get / set / method
}
/**
* "线"属性 操作代理实体
*/
export class MeasureLine extends BaseLineEntity {
... 各种 get / set / method
}
所有的模型操作都遵循如流程
StateManager -> EntityManager -> xxx.renderer -> InstancePool
辅助类的操作则是通过交互控制器
interaction 完成临时辅助对象的创建, 辅助线 / 临时单元 是不会被持久化的
点之间的关系不会非常复杂, 通常是比较稀疏的, 可能一个点最多有6个连线, 绝大部分点 只有1~2个关系连线.