feat(web-platform): release V0.8.1 常规地图优化
This commit is contained in:
@@ -5,10 +5,16 @@ import type {
|
||||
EditableMapObject,
|
||||
MapEditorTransformMode,
|
||||
} from '../map/editor/types';
|
||||
import type { SpawnPoint } from '../map/types';
|
||||
import type { MapEditorDraftPreviewInstance } from '../map/mapSceneDraft';
|
||||
import {
|
||||
DEFAULT_MAP_INSTANCE_TRANSFORM,
|
||||
type MapInstanceTransform,
|
||||
type SpawnPoint,
|
||||
} from '../map/types';
|
||||
|
||||
export interface MapEditorLayerCallbacks {
|
||||
onSelect(id: string | null): void;
|
||||
onPreviewSelect(mapAssetId: string, objectId: string): void;
|
||||
onTransform(
|
||||
id: string,
|
||||
position: [number, number, number],
|
||||
@@ -32,6 +38,7 @@ function mesh(geometry: THREE.BufferGeometry, object: EditableMapObject): THREE.
|
||||
const result = new THREE.Mesh(geometry, material(object));
|
||||
result.castShadow = true;
|
||||
result.receiveShadow = true;
|
||||
result.renderOrder = 9;
|
||||
return result;
|
||||
}
|
||||
function spawnPreview(spawn: SpawnPoint): THREE.Group {
|
||||
@@ -47,6 +54,8 @@ function spawnPreview(spawn: SpawnPoint): THREE.Group {
|
||||
});
|
||||
const ring = new THREE.Mesh(new THREE.TorusGeometry(0.22, 0.025, 8, 24), markerMaterial);
|
||||
const arrow = new THREE.Mesh(new THREE.ConeGeometry(0.08, 0.32, 12), markerMaterial);
|
||||
ring.renderOrder = 9;
|
||||
arrow.renderOrder = 9;
|
||||
arrow.rotation.z = -Math.PI / 2;
|
||||
arrow.position.x = 0.25;
|
||||
group.add(ring, arrow);
|
||||
@@ -99,13 +108,30 @@ function objectPreview(object: EditableMapObject): THREE.Group {
|
||||
return group;
|
||||
}
|
||||
|
||||
function disposeGroupChildren(group: THREE.Group): void {
|
||||
const geometries = new Set<THREE.BufferGeometry>();
|
||||
const materials = new Set<THREE.Material>();
|
||||
for (const child of group.children)
|
||||
child.traverse((object) => {
|
||||
if (!(object instanceof THREE.Mesh)) return;
|
||||
geometries.add(object.geometry);
|
||||
for (const item of Array.isArray(object.material) ? object.material : [object.material])
|
||||
materials.add(item);
|
||||
});
|
||||
group.clear();
|
||||
for (const geometry of geometries) geometry.dispose();
|
||||
for (const item of materials) item.dispose();
|
||||
}
|
||||
|
||||
export class MapEditorLayer {
|
||||
readonly group = new THREE.Group();
|
||||
readonly previewGroup = new THREE.Group();
|
||||
readonly transform: TransformControls;
|
||||
private readonly helper: THREE.Object3D;
|
||||
private readonly raycaster = new THREE.Raycaster();
|
||||
private readonly pointer = new THREE.Vector2();
|
||||
private readonly objects = new Map<string, THREE.Group>();
|
||||
private readonly previewInstances = new Map<string, THREE.Group>();
|
||||
private selectedId: string | null = null;
|
||||
private documentLoaded = false;
|
||||
|
||||
@@ -115,8 +141,9 @@ export class MapEditorLayer {
|
||||
domElement: HTMLElement,
|
||||
private readonly callbacks: MapEditorLayerCallbacks,
|
||||
) {
|
||||
this.previewGroup.name = '__platform_map_editor_scene_previews__';
|
||||
this.group.name = '__platform_map_editor__';
|
||||
scene.add(this.group);
|
||||
scene.add(this.previewGroup, this.group);
|
||||
this.transform = new TransformControls(camera, domElement);
|
||||
this.transform.setSpace('world');
|
||||
this.transform.setSize(0.8);
|
||||
@@ -134,12 +161,18 @@ export class MapEditorLayer {
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.documentLoaded;
|
||||
return this.documentLoaded || this.previewInstances.size > 0;
|
||||
}
|
||||
|
||||
setDocument(document: EditableMapDocument | null): void {
|
||||
setDocument(
|
||||
document: EditableMapDocument | null,
|
||||
instanceTransform: MapInstanceTransform = DEFAULT_MAP_INSTANCE_TRANSFORM,
|
||||
): void {
|
||||
const selected = this.selectedId;
|
||||
this.clearObjects();
|
||||
this.group.position.set(instanceTransform.positionX, instanceTransform.positionY, 0);
|
||||
this.group.rotation.set(0, 0, THREE.MathUtils.degToRad(instanceTransform.yawDeg));
|
||||
this.group.updateMatrixWorld(true);
|
||||
this.documentLoaded = Boolean(document);
|
||||
this.transform.enabled = this.documentLoaded;
|
||||
if (!document) {
|
||||
@@ -156,6 +189,27 @@ export class MapEditorLayer {
|
||||
this.selectObject(selected && this.objects.has(selected) ? selected : null, false);
|
||||
}
|
||||
|
||||
/** 绘制非激活、但仍处于统一场景草稿中的认证资产实例。 */
|
||||
setPreviewInstances(instances: readonly MapEditorDraftPreviewInstance[]): void {
|
||||
this.clearPreviewInstances();
|
||||
for (const instance of instances) {
|
||||
const root = new THREE.Group();
|
||||
root.name = `__platform_map_editor_scene_preview_${instance.id}__`;
|
||||
root.userData.mapAssetId = instance.id;
|
||||
root.position.set(instance.transform.positionX, instance.transform.positionY, 0);
|
||||
root.rotation.z = THREE.MathUtils.degToRad(instance.transform.yawDeg);
|
||||
for (const object of instance.document.objects) {
|
||||
if (object.enabled) root.add(objectPreview(object));
|
||||
}
|
||||
for (const spawn of instance.document.spawnPoints) root.add(spawnPreview(spawn));
|
||||
root.traverse((object) => {
|
||||
object.userData.mapEditorMapAssetId = instance.id;
|
||||
});
|
||||
this.previewInstances.set(instance.id, root);
|
||||
this.previewGroup.add(root);
|
||||
}
|
||||
}
|
||||
|
||||
setTransformMode(mode: MapEditorTransformMode): void {
|
||||
this.transform.setMode(mode);
|
||||
const translate = mode === 'translate';
|
||||
@@ -197,18 +251,38 @@ export class MapEditorLayer {
|
||||
if (notify) this.callbacks.onSelect(this.selectedId);
|
||||
}
|
||||
|
||||
/** 命中编辑对象或操纵器时消费左键;空白区域留给 OrbitControls 旋转视角。 */
|
||||
handlePointerDown(event: PointerEvent, bounds: DOMRect): boolean {
|
||||
/** 命中编辑对象或操纵器时消费左键;点击空白区域取消对象选中。 */
|
||||
handlePointerDown(
|
||||
event: PointerEvent,
|
||||
bounds: DOMRect,
|
||||
maximumDistance = Number.POSITIVE_INFINITY,
|
||||
): boolean {
|
||||
if (!this.enabled || event.button !== 0) return false;
|
||||
if (this.transform.axis || this.transform.dragging) return true;
|
||||
this.pointer.x = ((event.clientX - bounds.left) / bounds.width) * 2 - 1;
|
||||
this.pointer.y = -((event.clientY - bounds.top) / bounds.height) * 2 + 1;
|
||||
this.raycaster.setFromCamera(this.pointer, this.camera);
|
||||
const hit = this.raycaster.intersectObjects([...this.objects.values()], true)[0];
|
||||
const id = hit?.object.userData.mapEditorObjectId;
|
||||
if (typeof id !== 'string') return false;
|
||||
this.selectObject(id, true);
|
||||
return true;
|
||||
this.group.updateMatrixWorld(true);
|
||||
this.previewGroup.updateMatrixWorld(true);
|
||||
const hit = this.raycaster
|
||||
.intersectObjects([...this.objects.values(), ...this.previewInstances.values()], true)
|
||||
.find(
|
||||
(candidate) =>
|
||||
typeof candidate.object.userData.mapEditorObjectId === 'string' &&
|
||||
candidate.distance <= maximumDistance + 1e-3,
|
||||
);
|
||||
const objectId = hit?.object.userData.mapEditorObjectId;
|
||||
const mapAssetId = hit?.object.userData.mapEditorMapAssetId;
|
||||
if (typeof objectId === 'string' && typeof mapAssetId === 'string') {
|
||||
this.callbacks.onPreviewSelect(mapAssetId, objectId);
|
||||
return true;
|
||||
}
|
||||
if (typeof objectId === 'string') {
|
||||
this.selectObject(objectId, true);
|
||||
return true;
|
||||
}
|
||||
if (this.selectedId) this.selectObject(null, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
private commitTransform(): void {
|
||||
@@ -233,26 +307,22 @@ export class MapEditorLayer {
|
||||
private clearObjects(): void {
|
||||
this.transform.detach();
|
||||
this.helper.visible = false;
|
||||
const geometries = new Set<THREE.BufferGeometry>(),
|
||||
materials = new Set<THREE.Material>();
|
||||
for (const child of this.group.children)
|
||||
child.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
geometries.add(object.geometry);
|
||||
for (const item of Array.isArray(object.material) ? object.material : [object.material])
|
||||
materials.add(item);
|
||||
}
|
||||
});
|
||||
this.group.clear();
|
||||
disposeGroupChildren(this.group);
|
||||
this.objects.clear();
|
||||
for (const geometry of geometries) geometry.dispose();
|
||||
for (const item of materials) item.dispose();
|
||||
}
|
||||
|
||||
private clearPreviewInstances(): void {
|
||||
disposeGroupChildren(this.previewGroup);
|
||||
this.previewInstances.clear();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.documentLoaded = false;
|
||||
this.selectedId = null;
|
||||
this.clearObjects();
|
||||
this.clearPreviewInstances();
|
||||
this.group.position.set(0, 0, 0);
|
||||
this.group.rotation.set(0, 0, 0);
|
||||
this.transform.enabled = false;
|
||||
this.callbacks.onDragging(false);
|
||||
}
|
||||
@@ -262,6 +332,7 @@ export class MapEditorLayer {
|
||||
this.callbacks.onDragging(false);
|
||||
this.transform.dispose();
|
||||
this.helper.removeFromParent();
|
||||
this.previewGroup.removeFromParent();
|
||||
this.group.removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user