This commit is contained in:
2026-09-16 13:15:15 +08:00
parent 296300c362
commit eb6e29263c
4 changed files with 368 additions and 154 deletions

View File

@@ -81,7 +81,6 @@
<script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchAssetDetail } from '@/api/ops/asset'
import type { ThreeMachineRoomDevice } from '@/api/ops/three-machine-room'
import {
destroyHikvisionPlayer,
@@ -91,14 +90,19 @@ import {
stopHikvisionPreview,
type HikvisionPreviewConfig,
} from '@/services/hikvisionWebSdk'
import {
findThreeMachineRoomCameraPointByText,
getPlayableThreeMachineRoomCameraPoints,
getThreeMachineRoomCameraPointByIndex,
type ThreeMachineRoomCameraPoint,
} from '../config/camera-points'
/** 摄像头预览浮层属性。 */
interface Props {
visible: boolean
device: ThreeMachineRoomDevice | null
devices?: ThreeMachineRoomDevice[]
cameraIndex?: number
cameraCount?: number
cameraPoint?: ThreeMachineRoomCameraPoint | null
}
/** 摄像头预览浮层事件。 */
@@ -123,7 +127,7 @@ interface CameraConnectionForm {
}
const PLAYER_CONTAINER_ID = 'hikvision-camera-preview-player'
/** 3D 预览使用已验证通过的固定 WebSDK 参数,设备身份信息由资产详情提供。 */
/** 3D 预览使用已验证通过的固定 WebSDK 参数,设备身份信息来自固定点位表。 */
const FIXED_CAMERA_CONFIG: Omit<CameraConnectionForm, 'host' | 'username' | 'password'> = {
protocol: 2,
httpPort: 443,
@@ -161,22 +165,27 @@ let dragPointerId: number | null = null
let dragOffsetX = 0
let dragOffsetY = 0
const selectedCameraSlot = computed(() => {
if (props.cameraIndex) return props.cameraIndex
const selectedAssetId = Number(props.device?.asset_id)
const deviceIndex = (props.devices || []).findIndex((device) => Number(device.asset_id) === selectedAssetId)
return deviceIndex >= 0 ? deviceIndex + 1 : 1
})
const previewCount = computed(() =>
Math.min(16, Math.max(1, Number(props.cameraCount) || 0, props.devices?.length || 0, selectedCameraSlot.value))
const selectedFixedCameraPoint = computed(
() => props.cameraPoint || getThreeMachineRoomCameraPointByIndex(props.cameraIndex) || getCameraPointByDevice(props.device)
)
const previewCameraPoints = computed(() => {
const selectedPoint = selectedFixedCameraPoint.value
if (selectedPoint) return selectedPoint.playable && selectedPoint.brand === 'hikvision' ? [selectedPoint] : []
return getPlayableThreeMachineRoomCameraPoints()
})
const previewCount = computed(() => Math.min(16, Math.max(1, previewCameraPoints.value.length)))
const splitName = computed(() => {
if (previewCount.value <= 1) return '单画面'
if (previewCount.value <= 4) return '四宫格'
if (previewCount.value <= 9) return '九宫格'
return '十六宫格'
})
const floatingTitle = computed(() => `监控预览 · ${splitName.value}(${previewCount.value} 路)· 当前摄像头 ${selectedCameraSlot.value}`)
const currentCameraLabel = computed(() => {
const selectedPoint = selectedFixedCameraPoint.value
if (!selectedPoint) return `海康摄像头(${previewCameraPoints.value.length} 路)`
return `${selectedPoint.name} · ${selectedPoint.host}`
})
const floatingTitle = computed(() => `监控预览 · ${splitName.value} · ${currentCameraLabel.value}`)
const floatingLayerDimensions = computed(() => {
const preferred =
previewCount.value <= 1
@@ -202,22 +211,6 @@ const developmentProxyTip = computed(() => {
return '开发环境尚未配置海康代理;请配置 VITE_HIKVISION_PROXY_TARGET,或切换为直连。'
})
/** 将未知值转换为对象。 */
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null
}
/** 从若干配置对象中读取第一个有效字段。 */
function readConfigValue(records: Record<string, unknown>[], keys: string[]): unknown {
for (const record of records) {
for (const key of keys) {
const value = record[key]
if (value !== undefined && value !== null && value !== '') return value
}
}
return undefined
}
/** 校验浮层设备与固定开发代理是否为同一目标,避免凭据被发往错误设备。 */
function matchesDevelopmentProxyTarget(connection: CameraConnectionForm): boolean {
if (!import.meta.env.DEV || !connection.proxyEnabled || !developmentProxyConfigured) return true
@@ -233,87 +226,25 @@ function matchesDevelopmentProxyTarget(connection: CameraConnectionForm): boolea
}
}
/** 解析资产 source_address 中的设备地址与登录凭据。 */
function parseSourceAddress(sourceAddress: unknown): Record<string, unknown> {
if (typeof sourceAddress !== 'string' || !sourceAddress.trim()) return {}
const source = sourceAddress.trim()
if (source.startsWith('{')) {
try {
return asRecord(JSON.parse(source)) || {}
} catch (_error) {
return { host: source }
}
}
try {
const sourceUrl = new URL(source.includes('://') ? source : `http://${source}`)
return {
host: sourceUrl.hostname,
username: sourceUrl.username ? decodeURIComponent(sourceUrl.username) : undefined,
password: sourceUrl.password ? decodeURIComponent(sourceUrl.password) : undefined,
}
} catch (_error) {
return { host: source }
}
/** 从资产名称或编码匹配固定摄像头点位。 */
function getCameraPointByDevice(device: ThreeMachineRoomDevice | null): ThreeMachineRoomCameraPoint | null {
if (!device) return null
return findThreeMachineRoomCameraPointByText(device.asset_name, device.asset_code, device.category_name, device.category_code)
}
/** 只将资产详情中的 IP、用户名和密码映射到固定 WebSDK 参数。 */
function applyAssetCameraConfig(asset: Record<string, unknown>): CameraConnectionForm {
const sourceConfig = parseSourceAddress(asset.source_address)
const nestedConfig =
asRecord(asset.camera_config) || asRecord(asset.video_config) || asRecord(asset.stream_config) || asRecord(asset.hikvision_config)
const records = [nestedConfig, asset, sourceConfig].filter((item): item is Record<string, unknown> => Boolean(item))
/** 将固定摄像头点位转换为海康 WebSDK 连接参数。 */
function createCameraConnection(point: ThreeMachineRoomCameraPoint): CameraConnectionForm {
return {
host: String(readConfigValue(records, ['camera_host', 'host', 'ip_address', 'ip', 'device_ip', 'device_address']) || '').trim(),
username: String(readConfigValue(records, ['camera_username', 'username', 'user', 'login_username', 'login_name']) || '').trim(),
password: String(readConfigValue(records, ['camera_password', 'password', 'login_password']) || ''),
host: point.host,
username: point.username,
password: point.password,
...FIXED_CAMERA_CONFIG,
}
}
/** 获取单个资产详情中的摄像头连接配置。 */
async function loadAssetCameraConfig(device: ThreeMachineRoomDevice): Promise<CameraConnectionForm> {
if (!device.asset_id) throw new Error('摄像头未关联资产')
const response = (await fetchAssetDetail(device.asset_id)) as unknown
const responseRecord = asRecord(response)
if (!responseRecord || Number(responseRecord.code) !== 0) {
throw new Error(String(responseRecord?.message || '摄像头资产详情获取失败'))
}
const details = asRecord(responseRecord.details)
if (!details) throw new Error('摄像头资产详情格式错误')
const connection = applyAssetCameraConfig(details)
if (!connection.host || !connection.username || !connection.password) throw new Error('摄像头资产详情缺少 IP、用户名或密码')
return connection
}
/** 返回指定宫格位置对应的摄像头资产。 */
function getCameraDevice(cameraIndex: number): ThreeMachineRoomDevice | null {
const listedDevice = props.devices?.[cameraIndex - 1]
if (listedDevice) return listedDevice
return cameraIndex === selectedCameraSlot.value ? props.device : null
}
/** 并行加载全部宫格的摄像头身份信息;固定的 WebSDK 参数不依赖接口返回。 */
/** 加载当前可播放点位的摄像头身份信息;不再依赖接口返回的 IP。 */
async function loadPreviewConnections(): Promise<CameraConnectionForm[]> {
let failureCount = 0
const results = await Promise.all(
Array.from({ length: previewCount.value }, async (_item, index) => {
const cameraIndex = index + 1
const device = getCameraDevice(cameraIndex)
if (!device) return null
try {
return await loadAssetCameraConfig(device)
} catch (error) {
failureCount += 1
console.warn(`摄像头 ${cameraIndex} 资产身份信息获取失败:`, error)
return null
}
})
)
if (failureCount > 0) {
Message.warning(`${failureCount} 路摄像头的 IP、用户名或密码获取失败`)
}
return results.filter((connection): connection is CameraConnectionForm => Boolean(connection))
return previewCameraPoints.value.map((point) => createCameraConnection(point))
}
/** 将视频浮层限制在浏览器可视区域内。 */
@@ -392,14 +323,11 @@ async function openPreview(): Promise<void> {
async function startPreview(): Promise<void> {
const sequence = ++playbackSequence
errorMessage.value = ''
const selectedIndex = Math.min(connections.value.length - 1, Math.max(0, selectedCameraSlot.value - 1))
const orderedConnections = [
connections.value[selectedIndex],
...connections.value.filter((_connection, index) => index !== selectedIndex),
].filter((connection): connection is CameraConnectionForm => Boolean(connection))
const previewConnections = orderedConnections.map((connection) => ({ ...connection }))
const previewConnections = connections.value.map((connection) => ({ ...connection }))
if (!previewConnections.length) {
errorMessage.value = '没有可播放的摄像头'
const selectedPoint = selectedFixedCameraPoint.value
errorMessage.value =
selectedPoint && selectedPoint.brand === 'huawei' ? '华为摄像头暂未接入,当前仅支持海康视频查看' : '没有可播放的海康摄像头'
return
}
if (previewConnections.some((connection) => !connection.host || !connection.username || !connection.password)) {
@@ -505,9 +433,10 @@ watch(
[
() => props.visible,
() => props.device?.asset_id,
() => props.device?.asset_name,
() => props.device?.asset_code,
() => props.cameraIndex,
() => props.cameraCount,
() => (props.devices || []).map((device) => device.asset_id).join(','),
() => props.cameraPoint?.index,
],
([visible]) => {
if (visible) void openPreview()

View File

@@ -0,0 +1,188 @@
/** 3D 机房摄像头品牌。 */
export type ThreeMachineRoomCameraBrand = 'hikvision' | 'huawei'
/** 3D 机房模型局部坐标。 */
export interface ThreeMachineRoomCameraPosition {
x: number
y: number
z: number
}
/** 3D 机房固定摄像头点位。 */
export interface ThreeMachineRoomCameraPoint {
index: number
name: string
shortName: string
brand: ThreeMachineRoomCameraBrand
host: string
username: string
password: string
modelMeshName?: string
cloneFromMeshName?: string
position?: ThreeMachineRoomCameraPosition
playable: boolean
}
const CAMERA_USERNAME = 'admin'
const CAMERA_PASSWORD = 'Xzrmyy@12'
const CAMERA_LOCAL_Y = 2717.48
/** 3D 模型固定摄像头点位,顺序与现场摄像头清单保持一致。 */
export const THREE_MACHINE_ROOM_CAMERA_POINTS: ThreeMachineRoomCameraPoint[] = [
{
index: 1,
name: '单通道出口',
shortName: '单出',
brand: 'hikvision',
host: '192.168.101.200',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
modelMeshName: 'Obj3d66_8099876_1_913',
playable: true,
},
{
index: 2,
name: '单通道入口',
shortName: '单入',
brand: 'hikvision',
host: '192.168.101.201',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
modelMeshName: 'Obj3d66_8099876_1_916',
playable: true,
},
{
index: 3,
name: '电池间入口',
shortName: '电入',
brand: 'hikvision',
host: '192.168.101.202',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
modelMeshName: 'Obj3d66_8099876_1_918',
playable: true,
},
{
index: 4,
name: '双通道出口',
shortName: '双出',
brand: 'hikvision',
host: '192.168.101.203',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
modelMeshName: 'Obj3d66_8099876_1_914',
playable: true,
},
{
index: 5,
name: '双通道入口',
shortName: '双入',
brand: 'hikvision',
host: '192.168.101.204',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
modelMeshName: 'Obj3d66_8099876_1_915',
playable: true,
},
{
index: 6,
name: '电池间内部',
shortName: '电内',
brand: 'hikvision',
host: '192.168.101.205',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
modelMeshName: 'Obj3d66_8099876_1_917',
playable: true,
},
{
index: 7,
name: '单通道内入口',
shortName: '单内入',
brand: 'huawei',
host: '192.168.101.206',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
cloneFromMeshName: 'Obj3d66_8099876_1_916',
position: { x: -25050, y: CAMERA_LOCAL_Y, z: 1200 },
playable: false,
},
{
index: 8,
name: '单通道内出口',
shortName: '单内出',
brand: 'huawei',
host: '192.168.101.207',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
cloneFromMeshName: 'Obj3d66_8099876_1_913',
position: { x: -23650, y: CAMERA_LOCAL_Y, z: -3250 },
playable: false,
},
{
index: 9,
name: '双通道内入口',
shortName: '双内入',
brand: 'huawei',
host: '192.168.101.208',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
cloneFromMeshName: 'Obj3d66_8099876_1_915',
position: { x: -19550, y: CAMERA_LOCAL_Y, z: 1650 },
playable: false,
},
{
index: 10,
name: '双通道内出口',
shortName: '双内出',
brand: 'huawei',
host: '192.168.101.209',
username: CAMERA_USERNAME,
password: CAMERA_PASSWORD,
cloneFromMeshName: 'Obj3d66_8099876_1_914',
position: { x: -21200, y: CAMERA_LOCAL_Y, z: -3000 },
playable: false,
},
]
const CAMERA_POINTS_BY_INDEX = new Map(THREE_MACHINE_ROOM_CAMERA_POINTS.map((point) => [point.index, point]))
const CAMERA_POINTS_BY_MODEL_MESH = new Map(
THREE_MACHINE_ROOM_CAMERA_POINTS.filter((point) => point.modelMeshName).map((point) => [point.modelMeshName, point])
)
/** 归一化摄像头名称,用于匹配资产名称和模型点位。 */
export function normalizeThreeMachineRoomCameraName(name: unknown): string {
return String(name || '')
.replace(/\s+/g, '')
.replace(/[()()【】\[\]_-]/g, '')
.replace(/摄像头|摄像机|监控/g, '')
.toLowerCase()
}
/** 按点位序号获取固定摄像头配置。 */
export function getThreeMachineRoomCameraPointByIndex(index: number | undefined): ThreeMachineRoomCameraPoint | null {
if (!Number.isInteger(index)) return null
return CAMERA_POINTS_BY_INDEX.get(Number(index)) || null
}
/** 按导入模型网格名称获取固定摄像头配置。 */
export function getThreeMachineRoomCameraPointByModelMeshName(meshName: string): ThreeMachineRoomCameraPoint | null {
return CAMERA_POINTS_BY_MODEL_MESH.get(meshName) || null
}
/** 返回当前阶段可播放的海康摄像头点位。 */
export function getPlayableThreeMachineRoomCameraPoints(): ThreeMachineRoomCameraPoint[] {
return THREE_MACHINE_ROOM_CAMERA_POINTS.filter((point) => point.playable && point.brand === 'hikvision')
}
/** 按资产名称或编码匹配固定摄像头点位。 */
export function findThreeMachineRoomCameraPointByText(...values: unknown[]): ThreeMachineRoomCameraPoint | null {
const normalizedValues = values.map((value) => normalizeThreeMachineRoomCameraName(value)).filter(Boolean)
if (!normalizedValues.length) return null
return (
THREE_MACHINE_ROOM_CAMERA_POINTS.find((point) => {
const pointNames = [point.name, point.shortName].map((value) => normalizeThreeMachineRoomCameraName(value))
return normalizedValues.some((value) => pointNames.some((pointName) => value.includes(pointName) || pointName.includes(value)))
}) || null
)
}

View File

@@ -55,7 +55,7 @@
{{
insideAisle
? '当前为机柜过道第一视角;点击设备可近距离查看,双击柜内设备可抽出或归位'
: '双击门板可开合;机柜开门后双击设备可抽出或归位,点击摄像头播放监控、长按可拖动'
: '双击门板可开合;机柜开门后双击设备可抽出或归位,点击海康摄像头播放监控、长按可拖动'
}}
</span>
</div>
@@ -128,9 +128,8 @@
<camera-preview-dialog
v-model:visible="cameraPreviewVisible"
:device="selectedDevice"
:devices="sceneCameraDevices"
:camera-index="selectedCameraIndex"
:camera-count="sceneSummary.importedCameraCount"
:camera-point="selectedCameraPoint"
@manage="handleOpenDeviceManagement"
/>
<device-management-dialog
@@ -171,6 +170,11 @@ import {
import { getMockDeviceObservability, MOCK_ROOM_SCENE, MOCK_ROOM_SIGNALS } from './scene/MockThreeMachineRoom'
import ThreeMap from './scene/ThreeMap'
import { ThreeData } from './scene/ThreeData'
import {
findThreeMachineRoomCameraPointByText,
getThreeMachineRoomCameraPointByIndex,
type ThreeMachineRoomCameraPoint,
} from './config/camera-points'
/** 场景渲染摘要。 */
interface SceneSummary {
@@ -200,6 +204,10 @@ interface DeviceAlertCalloutContext {
interface ImportedCameraContext {
index: number
name: string
shortName?: string
brand?: string
playable?: boolean
host?: string
}
const route = useRoute()
@@ -226,6 +234,7 @@ const roomScene = ref<ThreeMachineRoomScene | null>(null)
const selectedRack = ref<ThreeMachineRoomRack | null>(null)
const selectedDevice = ref<ThreeMachineRoomDevice | null>(null)
const selectedCameraIndex = ref<number>()
const selectedCameraPoint = ref<ThreeMachineRoomCameraPoint | null>(null)
const activeRack = ref<ThreeMachineRoomRack | null>(null)
const rackPlacementVisible = ref(false)
const rackLayoutVisible = ref(false)
@@ -342,11 +351,6 @@ async function loadRoomScene(): Promise<boolean> {
usingMockData.value = false
roomScene.value = scene
machineRoomMap?.setRoomScene(scene)
const roomConfigured = [scene.room.scene_length, scene.room.scene_width, scene.room.scene_height].every((value) => Number(value) > 0)
if (!roomConfigured) {
sceneConfigVisible.value = true
Message.info('该机房尚未完成 3D 初始化,请先配置机房尺寸和机柜位置')
}
await loadRoomSignals(true)
return true
} catch (error) {
@@ -455,12 +459,36 @@ function getSceneCameraDevices(): ThreeMachineRoomDevice[] {
return Array.from(deviceMap.values()).sort((left, right) => Number(left.asset_id) - Number(right.asset_id))
}
/** 点击模型内置摄像头时,按序关联场景摄像头资产并打开对应通道。 */
/** 从资产名称或编码匹配固定摄像头点位。 */
function getCameraPointByDevice(device: ThreeMachineRoomDevice | null): ThreeMachineRoomCameraPoint | null {
if (!device) return null
return findThreeMachineRoomCameraPointByText(device.asset_name, device.asset_code, device.category_name, device.category_code)
}
/** 根据固定点位匹配接口中的摄像头资产,用于从视频浮层进入设备管理。 */
function getCameraDeviceByPoint(point: ThreeMachineRoomCameraPoint | null): ThreeMachineRoomDevice | null {
if (!point) return null
return sceneCameraDevices.value.find((device) => getCameraPointByDevice(device)?.index === point.index) || null
}
/** 判断固定点位是否可播放,华为摄像头现阶段只展示点位。 */
function isPlayableCameraPoint(point: ThreeMachineRoomCameraPoint | null): boolean {
return Boolean(point && point.playable && point.brand === 'hikvision')
}
/** 点击模型内置摄像头时,使用前端固定点位打开对应海康通道。 */
function handleImportedCameraSelected(camera: ImportedCameraContext): void {
const cameraIndex = Number(camera.index)
if (!Number.isInteger(cameraIndex) || cameraIndex <= 0) return
const cameraPoint = getThreeMachineRoomCameraPointByIndex(cameraIndex)
selectedCameraIndex.value = cameraIndex
selectedDevice.value = sceneCameraDevices.value[cameraIndex - 1] || null
selectedCameraPoint.value = cameraPoint
selectedDevice.value = getCameraDeviceByPoint(cameraPoint)
if (!cameraPoint || !isPlayableCameraPoint(cameraPoint)) {
cameraPreviewVisible.value = false
Message.info(`${cameraPoint?.name || camera.name || '该'}摄像头暂未接入,当前仅支持海康视频查看`)
return
}
cameraPreviewVisible.value = true
}
@@ -468,10 +496,19 @@ function handleImportedCameraSelected(camera: ImportedCameraContext): void {
function handleDeviceSelected(device: ThreeMachineRoomDevice): void {
selectedDevice.value = device
if (isCameraDevice(device)) {
selectedCameraIndex.value = undefined
const cameraPoint = getCameraPointByDevice(device)
selectedCameraIndex.value = cameraPoint?.index
selectedCameraPoint.value = cameraPoint
if (!cameraPoint || !isPlayableCameraPoint(cameraPoint)) {
cameraPreviewVisible.value = false
Message.info(`${cameraPoint?.name || device.asset_name || '该'}摄像头暂未接入,当前仅支持海康视频查看`)
return
}
cameraPreviewVisible.value = true
return
}
selectedCameraIndex.value = undefined
selectedCameraPoint.value = null
if (usingMockData.value) {
void loadDeviceObservability(device)
Message.warning('演示数据不支持资源绑定和位置管理')

View File

@@ -11,10 +11,10 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { TGALoader } from 'three/examples/jsm/loaders/TGALoader'
import TWEEN from '@tweenjs/tween.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' //r100及以上
import { THREE_MACHINE_ROOM_CAMERA_POINTS, getThreeMachineRoomCameraPointByModelMeshName } from '../config/camera-points'
const ROOM_DEVICE_LONG_PRESS_DURATION = 300
const ROOM_DEVICE_DRAG_MOVE_THRESHOLD = 6
const IMPORTED_CAMERA_NAME_PATTERN = /^Obj3d66_8099876_1_91[3-8]$/
const VIEW_OCCLUSION_UPDATE_INTERVAL = 60
const VIEW_OCCLUDER_OPACITY = 0.055
const RACK_EQUIPMENT_PULL_OUT_RATIO = 0.58
@@ -2182,10 +2182,15 @@ export default class ThreeMap {
const isClick = pressDuration < ROOM_DEVICE_LONG_PRESS_DURATION && drag.maxDistance < ROOM_DEVICE_DRAG_MOVE_THRESHOLD
if (isClick) {
if (drag.equipment.userData.importedCamera) {
const cameraPoint = drag.equipment.userData.importedCameraPoint || null
if (typeof this.props.onImportedCameraSelected === 'function') {
this.props.onImportedCameraSelected({
index: drag.equipment.userData.importedCameraIndex,
name: drag.equipment.name,
name: cameraPoint ? cameraPoint.name : drag.equipment.name,
shortName: cameraPoint ? cameraPoint.shortName : '',
brand: cameraPoint ? cameraPoint.brand : '',
playable: cameraPoint ? cameraPoint.playable : false,
host: cameraPoint ? cameraPoint.host : '',
})
}
} else {
@@ -2197,10 +2202,14 @@ export default class ThreeMap {
const equipment = drag.equipment
if (equipment.userData.importedCamera) {
const cameraPoint = equipment.userData.importedCameraPoint || null
if (typeof this.props.onImportedCameraMoved === 'function') {
this.props.onImportedCameraMoved({
index: equipment.userData.importedCameraIndex,
name: equipment.name,
name: cameraPoint ? cameraPoint.name : equipment.name,
shortName: cameraPoint ? cameraPoint.shortName : '',
brand: cameraPoint ? cameraPoint.brand : '',
playable: cameraPoint ? cameraPoint.playable : false,
position: {
x: equipment.position.x,
y: equipment.position.y,
@@ -2363,30 +2372,40 @@ export default class ThreeMap {
})
}
/** 将模型内置的 6 个摄像头网格转换为可独立拖动的对象。 */
prepareImportedCameras(meshEntries, modelBox) {
this.importedCameras = meshEntries
.filter((entry) => IMPORTED_CAMERA_NAME_PATTERN.test(entry.mesh.name))
.sort((a, b) => a.mesh.name.localeCompare(b.mesh.name))
.map((entry, index) => {
const mesh = entry.mesh
const geometryCenter = entry.box.getCenter(new THREE.Vector3())
/** 标记一个导入摄像头网格,并补充点击热区和固定点位信息。 */
prepareImportedCameraMesh(mesh, point, modelBox, geometryBox) {
if (!mesh || !point) {
return null
}
if (geometryBox) {
const geometryCenter = geometryBox.getCenter(new THREE.Vector3())
const geometry = mesh.geometry.clone()
geometry.translate(-geometryCenter.x, -geometryCenter.y, -geometryCenter.z)
geometry.computeBoundingBox()
geometry.computeBoundingSphere()
mesh.geometry = geometry
mesh.position.add(geometryCenter)
const cameraSize = geometry.boundingBox.getSize(new THREE.Vector3())
} else if (mesh.geometry) {
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox()
if (!mesh.geometry.boundingSphere) mesh.geometry.computeBoundingSphere()
}
const cameraBox = this.getGeometryBox(mesh)
const cameraSize = cameraBox ? cameraBox.getSize(new THREE.Vector3()) : new THREE.Vector3(120, 120, 120)
const hitRadius = Math.max(cameraSize.x, cameraSize.y, cameraSize.z) * 0.85
const hitArea = new THREE.Mesh(
new THREE.SphereGeometry(hitRadius, 12, 8),
new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false })
)
hitArea.name = 'imported_camera_hit_area_' + (index + 1)
hitArea.name = 'imported_camera_hit_area_' + point.index
mesh.add(hitArea)
mesh.userData.importedCamera = true
mesh.userData.importedCameraIndex = index + 1
mesh.userData.importedCameraIndex = point.index
mesh.userData.importedCameraName = point.name
mesh.userData.importedCameraShortName = point.shortName
mesh.userData.importedCameraBrand = point.brand
mesh.userData.importedCameraPlayable = point.playable
mesh.userData.importedCameraPoint = point
mesh.userData.placementType = 'imported-camera'
mesh.userData.draggable = true
mesh.userData.roomPlacement = {
@@ -2394,7 +2413,48 @@ export default class ThreeMap {
}
mesh.userData.originalModelPosition = mesh.position.clone()
return mesh
}
/** 克隆现有摄像头网格,补齐模型缺失的通道内摄像头。 */
createSupplementalImportedCamera(point, sourceMesh, modelBox) {
if (!point || !point.position || !sourceMesh || !sourceMesh.parent) {
return null
}
const mesh = sourceMesh.clone(false)
mesh.name = 'supplemental_imported_camera_' + point.index
mesh.geometry = sourceMesh.geometry
mesh.material = sourceMesh.material
mesh.position.set(point.position.x, point.position.y, point.position.z)
mesh.rotation.copy(sourceMesh.rotation)
mesh.scale.copy(sourceMesh.scale)
sourceMesh.parent.add(mesh)
return this.prepareImportedCameraMesh(mesh, point, modelBox, null)
}
/** 将固定摄像头清单映射到模型点位,并补齐 4 个通道内摄像头。 */
prepareImportedCameras(meshEntries, modelBox) {
const sourceMeshes = new Map()
const importedCameras = meshEntries
.map((entry) => {
const point = getThreeMachineRoomCameraPointByModelMeshName(entry.mesh.name)
return point ? { entry: entry, point: point } : null
})
.filter(Boolean)
.sort((a, b) => a.point.index - b.point.index)
.map(({ entry, point }) => {
const mesh = this.prepareImportedCameraMesh(entry.mesh, point, modelBox, entry.box)
if (mesh) sourceMeshes.set(entry.mesh.name, mesh)
return mesh
})
.filter(Boolean)
THREE_MACHINE_ROOM_CAMERA_POINTS.filter((point) => point.position && point.cloneFromMeshName).forEach((point) => {
const sourceMesh = sourceMeshes.get(point.cloneFromMeshName) || importedCameras[0]
const mesh = this.createSupplementalImportedCamera(point, sourceMesh, modelBox)
if (mesh) importedCameras.push(mesh)
})
this.importedCameras = importedCameras.sort((a, b) => a.userData.importedCameraIndex - b.userData.importedCameraIndex)
return this.importedCameras
}