This implementation focuses on a digital twin system for a warehouse and container yard, covering both indoor and outdoor storage areas. The system integrates data from RFID, manual entry, and vehicle positioning systems to visualize assets and operations in 3D.
External Yard and Container Information
Hovering over containers or vehicles triggers an information display. This is handled by a mouse-over callback function that identifies the object and fetches its data.
WarehouseManager.prototype.handleMouseOver = function(targetObject) {
const controls = this.threeDControls;
controls.autoRotate = false;
let selectedObj = targetObject;
if (targetObject.name.includes('VEHICLE_')) {
selectedObj = targetObject.parent;
}
const assetId = selectedObj.name.split('_Asset_')[1];
this.highlightObject(selectedObj, 0x00ffff);
this.displayAssetInfo(assetId);
};
function displayCargoDetails(assetIdentifier) {
const infoHtml = '<div>Loading cargo data...</div>';
const tooltipConfig = {
tips: [1, '#003333'],
time: 0,
area: ['415px', '230px'],
success: function() {
setTimeout(() => {
fetchCargoData(assetIdentifier).then(data => {
populateTooltipContent(data, assetIdentifier);
}).catch(error => {
showDataError(assetIdentifier);
});
}, 200);
}
};
createTooltip(infoHtml, tooltipConfig);
}
Vehicle Access Management
Vehicle entry and exit are simulated with animated movemetns and real-time gate status updates.
function simulateVehicleEntry(vehicleName, gateName) {
const vehicle = findSceneObject(vehicleName);
const entryGate = findSceneObject(gateName);
vehicle.position.set(-858.7, 0, 19837.4);
new TWEEN.Tween(vehicle.position)
.to({ z: 14344.9 }, 5000)
.onComplete(() => {
new TWEEN.Tween(vehicle.rotation)
.to({ y: 0 }, 1000)
.start();
new TWEEN.Tween(vehicle.position)
.to({ x: -225.8, z: 13523.4 }, 1000)
.onComplete(() => {
showVehicleNotification('粤A17001', 'Internal Vehicle');
operateGate(entryGate, true, () => {
animateVehicleToParking(vehicle);
});
}).start();
}).start();
}
function operateGate(gateObject, openState, callback) {
const targetRotationX = openState ? Math.PI : -Math.PI / 2;
new TWEEN.Tween(gateObject.rotation)
.to({ x: targetRotationX }, 2000)
.onComplete(callback)
.start();
}
Indoor Warehouse Navigation
Double-clicking a building triggers a transition to its interior view, with floors expanding for detailed inspection.
WarehouseManager.prototype.enterBuilding = function(buildingId) {
this.floorState = 'opening';
const building = findSceneObject(buildingId);
const buildingOriginalY = building.position.y;
this.highlightObject(building, 0x00ffff);
this.moveCamera({ x: 3652.5, y: 990.8, z: 5107.4 }, { x: 1914.5, y: -723.9, z: 2181.6 }, 500);
this.fadeObject(building, 1, 0.1, 500, () => {
building.position.y = 1000000;
building.visible = false;
this.moveCamera({ x: -1181.7, y: 7695.8, z: 17124.2 }, { x: 7526.4, y: 2616.2, z: 7792.1 }, 500);
this.expandBuildingFloors(['zgx_102_f1', 'zgx_102_f2', 'zgx_102_f3', 'zgx_102_f4']);
});
};
WarehouseManager.prototype.expandBuildingFloors = function(floorNames) {
const floors = findSceneObjects(floorNames);
floors.forEach(floorObj => {
if (!floorObj.originalY) floorObj.originalY = floorObj.position.y;
if (floorObj.position.y > 100000) floorObj.position.y -= 1000000;
floorObj.visible = true;
});
setTimeout(() => {
floors.forEach(floorObj => {
const floorNumber = parseInt(floorObj.name.split('_f')[1]);
const targetHeight = (floorNumber - 1) * 1500 + 50;
new TWEEN.Tween(floorObj.position)
.to({ y: targetHeight }, 500)
.start();
});
}, 500);
};
Zone-Based Cargo Display
Cargo is filtered and displayed based on predefined storage zones.
WarehouseManager.prototype.filterCargoByZone = function(zoneCode) {
this.scene.children.forEach(child => {
if (child.name.startsWith('storage_location_')) {
child.visible = true;
if (child.originalY !== undefined) {
child.position.y = child.originalY;
}
}
if (child.name.startsWith('cargo_')) {
if (zoneCode === 'ALL') {
child.visible = true;
} else {
child.visible = child.name.includes(`_Zone_${zoneCode}`);
}
}
});
};
Cargo Search and Positioning
A search function isolates and highlights specific cargo items within the 3D scene.
WarehouseManager.prototype.locateCargo = function(searchResults) {
this.isSearchActive = true;
this.moveCamera({ x: 1138.7, y: 7190.8, z: 9690.7 }, { x: 5051.3, y: 678.7, z: 2255.8 }, 500, () => {
this.clearPreviousSearch(() => {
const resultMap = {};
searchResults.forEach(item => {
resultMap[`cargo_${item.id}`] = item;
});
const objectsToHide = [];
const newMarkerModels = [];
this.searchMarkers = [];
this.scene.children.forEach(child => {
if (!child.originalPositionY && child.originalPositionY !== 0) {
child.originalPositionY = child.position.y;
}
if (child.name.startsWith('storage_location_')) {
child.visible = false;
child.position.y = 1000000;
}
if (child.name.startsWith('cargo_')) {
objectsToHide.push(child);
const matchedItem = resultMap[child.name.split('_Zone_')[0]];
if (matchedItem) {
const markerId = `searchMarker_${matchedItem.id}_${matchedItem.name}`;
this.searchMarkers.push(markerId);
const cachedData = this.cargoCache[`cache_${matchedItem.id}`];
newMarkerModels.push(
this.createCargoMarker(markerId, matchedItem.name, child.position, child.scale, cachedData?.color)
);
child.visible = false;
child.position.y = 1000000;
}
}
});
if (newMarkerModels.length > 0) {
this.loadModels(newMarkerModels, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }, true, () => {
this.fadeObjects(objectsToHide, 1, 0.1, 500, () => {
hideLoadingIndicator();
});
});
} else {
this.fadeObjects(objectsToHide, 1, 0.1, 500, () => {
hideLoadingIndicator();
});
}
});
});
};
Forklift Real-Time Tracking
Forklift positions are updated based on data from positioning cards and base stations.
WarehouseManager.prototype.updateAssetPositions = function() {
if (this.isSearchActive) return;
fetchCargoList().then(cargoList => {
this.clearPreviousCargoModels();
const modelBlueprints = [];
const baseOffsetX = this.config.basePoints.warehouse.x;
const baseOffsetY = this.config.basePoints.warehouse.y;
if (cargoList && cargoList.length > 0) {
const usedColors = [];
const baseModelNames = [];
cargoList.forEach(item => {
this.cargoCache[`cache_${item.id}`] = item;
if (!usedColors.includes(item.color)) {
modelBlueprints.push(
this.createCargoModel(
`cargo_${item.id}_Zone_${item.zone}`,
item.name,
{ x: item.position.x + baseOffsetX, y: item.position.z, z: item.position.y + baseOffsetY },
{ x: item.size.x, y: item.size.z, z: item.size.y },
item.color
)
);
baseModelNames.push(`cargo_${item.id}_Zone_${item.zone}`);
usedColors.push(item.color);
}
});
this.previousCargoModels = baseModelNames;
this.loadModels(modelBlueprints, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }, true, () => {
this.cloneAndPositionCargoModels(cargoList, baseModelNames, usedColors, baseOffsetX, baseOffsetY);
});
}
});
setTimeout(() => this.updateAssetPositions(), this.config.updateInterval * 1000);
};
3D Model Creation
Models for the environment, buildings, storage areas, containres, and vehicles are defined using JSON configurations or procedural generation.
Creating a Storage Zone Model:
function createStorageZoneModel(zoneName, zoneTitle, zoneColor, boundaryPoints) {
const points = boundaryPoints.map(pt => ({
x: pt.x + config.baseOffset.x,
y: pt.y + config.baseOffset.y,
type: 'normal'
}));
const zoneModel = {
show: true,
name: `zone_${zoneName}`,
objType: 'ExtrudeGeometry',
position: { x: 0, y: 0, z: 0 },
style: {
skinColor: 0xff0000,
skin: {
skin_up: { skinColor: zoneColor, materialType: 'Phong', side: 1, opacity: 0.5 },
skin_down: { skinColor: zoneColor, side: 1, opacity: 1 },
skin_side: { skinColor: zoneColor, opacity: 1 }
}
},
scale: { x: 1, y: 1, z: 1 },
shapeParm: { points: points, holes: [] },
extrudeSettings: {
amount: 120,
curveSegments: 1,
steps: 1,
bevelEnabled: false
},
showSortNub: 40,
rotation: [
{ direction: 'x', degree: Math.PI / 2 },
{ direction: 'y', degree: 0 },
{ direction: 'z', degree: 0 }
]
};
return [zoneModel];
}
Creating a Cargo Box Model:
function createCargoBoxModel(modelName, position, dimensions, color) {
return {
show: true,
name: modelName,
objType: 'ExtrudeGeometry',
position: position,
style: {
skinColor: 0xff0000,
skin: {
skin_up: { skinColor: color, side: 1, opacity: 1 },
skin_down: { skinColor: 0xffffff, side: 1, opacity: 1 },
skin_side: { skinColor: color, opacity: 1 }
}
},
scale: {
x: dimensions.x / 100,
y: dimensions.y / 100,
z: dimensions.z / 100
},
shapeParm: {
points: [
{ x: 0, y: 0, type: 'normal' },
{ x: 0, y: 100, type: 'normal' },
{ x: 100, y: 100, type: 'normal' },
{ x: 100, y: 0, type: 'normal' }
],
holes: []
},
extrudeSettings: {
amount: 100,
curveSegments: 1,
steps: 1,
bevelEnabled: false
},
showSortNub: 100
};
}
Creating a Vehicle Model:
function createVehicleModel(vehicleId, position, rotation, scale, vehicleCategory) {
const modelConfig = {
name: vehicleId,
objType: 'objmodel',
position: position,
scale: scale,
visible: true,
rotation: [
{ direction: 'x', degree: rotation.x },
{ direction: 'y', degree: rotation.y - Math.PI / 2 },
{ direction: 'z', degree: rotation.z }
],
filePath: '../js/models/vehicles/',
mtlFileName: 'default.mtl',
objFileName: 'default.obj'
};
switch(vehicleCategory) {
case 'forklift':
modelConfig.filePath = '../js/models/forklift/';
modelConfig.mtlFileName = 'forklift.mtl';
modelConfig.objFileName = 'forklift.obj';
modelConfig.scale = { x: 0.1, y: 0.1, z: 0.1 };
break;
case 'truck':
modelConfig.filePath = '../js/models/truck/';
modelConfig.mtlFileName = 'truck.mtl';
modelConfig.objFileName = 'truck.obj';
modelConfig.scale = { x: 4.2, y: 4.2, z: 4.2 };
break;
}
return modelConfig;
}
Dynamic Model Generation from Data
Storage zones and cargo are generated dynamically based on backend data, ensuring the 3D scene reflects the current state.
WarehouseManager.prototype.generateStorageLayout = function() {
fetchZoneData().then(zoneData => {
const zoneModels = [];
if (zoneData && zoneData.length > 0) {
zoneData.forEach(zone => {
let zoneColor = zone.color;
if (!zoneColor) zoneColor = Math.floor(Math.random() * 16777215);
else zoneColor = parseInt(zoneColor.replace('#', '0x'), 16);
const zoneModel = createStorageZoneModel(zone.code, zone.name, zoneColor, zone.boundaryPoints);
zoneModels.push(...zoneModel);
});
}
this.loadModels(zoneModels, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }, true, () => {
console.log('Storage zones loaded.');
});
});
};
The core implementation involves five key steps: creating the 3D models, aligning coordinate systems with real-world data, loading configured assets, dynamically generating storage and cargo elements from live data, and implementing interactive business logic for navigation and search.