{"id":"2WGMmNGaDg","url":"https://pastebin.ca/2WGMmNGaDg","raw_url":"https://raw.anybin.ca/2WGMmNGaDg","visibility":"public","access":"public","created_at":1781096069133,"expires_at":1781700869133,"fetch_limit":null,"fetches_used":0,"reads_remaining":null,"size_bytes":84510,"syntax_hint":null,"title":null,"filename":null,"change_note":null,"cipher":null,"cipher_meta":null,"parent_id":null,"root_id":"2WGMmNGaDg","version":1,"owner_id":null,"recipient_id":null,"body":"import { ArcRotateCamera } from \"@babylonjs/core/Cameras/arcRotateCamera\";\nimport { Engine } from \"@babylonjs/core/Engines/engine\";\nimport { HighlightLayer } from \"@babylonjs/core/Layers/highlightLayer\";\nimport { DirectionalLight } from \"@babylonjs/core/Lights/directionalLight\";\nimport { HemisphericLight } from \"@babylonjs/core/Lights/hemisphericLight\";\nimport { ShadowGenerator } from \"@babylonjs/core/Lights/Shadows/shadowGenerator\";\nimport { CascadedShadowGenerator } from \"@babylonjs/core/Lights/Shadows/cascadedShadowGenerator\";\nimport { ImageProcessingConfiguration } from \"@babylonjs/core/Materials/imageProcessingConfiguration\";\nimport { PBRMaterial } from \"@babylonjs/core/Materials/PBR/pbrMaterial\";\nimport { CubeTexture } from \"@babylonjs/core/Materials/Textures/cubeTexture\";\nimport { RawCubeTexture } from \"@babylonjs/core/Materials/Textures/rawCubeTexture\";\nimport { DynamicTexture } from \"@babylonjs/core/Materials/Textures/dynamicTexture\";\nimport { Color3, Color4 } from \"@babylonjs/core/Maths/math.color\";\nimport { Matrix, Quaternion, Vector3 } from \"@babylonjs/core/Maths/math.vector\";\nimport { Mesh } from \"@babylonjs/core/Meshes/mesh\";\nimport { MeshBuilder } from \"@babylonjs/core/Meshes/meshBuilder\";\nimport { Scene } from \"@babylonjs/core/scene\";\nimport { SSAO2RenderingPipeline } from \"@babylonjs/core/PostProcesses/RenderPipeline/Pipelines/ssao2RenderingPipeline\";\nimport { FxaaPostProcess } from \"@babylonjs/core/PostProcesses/fxaaPostProcess\";\nimport { SceneInstrumentation } from \"@babylonjs/core/Instrumentation/sceneInstrumentation\";\nimport \"@babylonjs/core/Meshes/thinInstanceMesh\";\n// Side-effect imports for Babylon.js v9 tree-shaking\n// Without these, Vite/Rolldown strips registration of core components\nimport \"@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManagerSceneComponent\";\nimport \"@babylonjs/core/Rendering/edgesRenderer\";\nimport \"@babylonjs/core/Meshes/instancedMesh\";\nimport \"@babylonjs/core/Culling/ray\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport {\n\tcomputeRackGridLines,\n\tcomputeSrmGridLines,\n\tdetectEntityState,\n\tENTITY_COLORS,\n\tgetDefaultTransparent,\n\tgetSrmBackgroundColor,\n\thashColorHue,\n\tITEM_FILL_COLOR,\n\tITEM_LINE_COLOR,\n\tnormalizeRotation,\n\tresolveShape3D,\n} from \"@/lib/entityRenderUtils\";\nimport type { EntityShape3D } from \"@/lib/entityRenderUtils\";\nimport { useSimStore } from \"@/store/useSimStore\";\nimport type { EntityDTO } from \"@/types/simulation\";\nimport {\n\tbuildShapeMesh,\n\tbuildCardboardBoxMesh,\n\tbuildLoadUnitMesh,\n\tbuildToteMesh,\n\tcreateQuickMat,\n\tclearMaterialCache,\n\tC,\n} from \"../scene3d/meshBuilders\";\nimport type { ShapeKind } from \"../scene3d/meshBuilders\";\nimport { ThinInstanceManager } from \"../scene3d/thinInstanceManager\";\n\nfunction hexToColor3(\n\tcolor: string | null | undefined,\n\tfallback: Color3,\n): Color3 {\n\tif (color == null || color === \"\") return fallback;\n\tconst hex = color.replace(\"#\", \"\");\n\tif (hex.length < 6) return fallback;\n\tconst r = parseInt(hex.substring(0, 2), 16) / 255;\n\tconst g = parseInt(hex.substring(2, 4), 16) / 255;\n\tconst b = parseInt(hex.substring(4, 6), 16) / 255;\n\tif (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b))\n\t\treturn fallback;\n\treturn new Color3(r, g, b);\n}\n\nfunction hexToColor4(hex: string | null | undefined): Color4 {\n\tif (hex == null || hex === \"\") return new Color4(1, 1, 1, 1);\n\tconst h = hex.replace(\"#\", \"\");\n\tif (h.length < 6) return new Color4(1, 1, 1, 1);\n\tconst r = parseInt(h.substring(0, 2), 16) / 255;\n\tconst g = parseInt(h.substring(2, 4), 16) / 255;\n\tconst b = parseInt(h.substring(4, 6), 16) / 255;\n\tif (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b))\n\t\treturn new Color4(1, 1, 1, 1);\n\treturn new Color4(r, g, b, 1);\n}\n\n/** Group entity type strings into broad categories for performance analysis. */\nfunction categorizeEntityType(type: string): string {\n\tif (type.startsWith(\"Bin\")) return \"Bin\";\n\tif (type.startsWith(\"Pallet\")) return \"Pallet\";\n\tif (type.startsWith(\"Rack\")) return \"Rack\";\n\tif (type.startsWith(\"Conveyor\")) return \"Conveyor\";\n\tif (type.startsWith(\"SRM\") || type.startsWith(\"Srm\")) return \"SRM\";\n\tif (type.startsWith(\"Lift\")) return \"Lift\";\n\tif (type.startsWith(\"Robot\")) return \"Robot\";\n\tif (type.startsWith(\"Shuttle\")) return \"Shuttle\";\n\tif (type.startsWith(\"Connector\")) return \"Connector\";\n\tif (type.startsWith(\"Virtual\")) return \"Virtual\";\n\tif (type.startsWith(\"Shape\")) return \"Shape\";\n\tif (type.startsWith(\"Overhead\")) return \"Overhead\";\n\tif (type.startsWith(\"Emulator\")) return \"Emulator\";\n\tif (type.startsWith(\"Symbol\")) return \"Symbol\";\n\tif (type.startsWith(\"Scanner\")) return \"Scanner\";\n\tif (type.startsWith(\"Dispatcher\")) return \"Dispatcher\";\n\tif (type.startsWith(\"Processor\")) return \"Processor\";\n\tif (type.startsWith(\"Buffer\")) return \"Buffer\";\n\tif (type.startsWith(\"Diverter\")) return \"Diverter\";\n\tif (type.startsWith(\"Merge\")) return \"Merge\";\n\tif (type.startsWith(\"Turntable\")) return \"Turntable\";\n\tif (type.startsWith(\"Stacker\")) return \"Stacker\";\n\tif (type.startsWith(\"Destacker\")) return \"Destacker\";\n\tif (type.startsWith(\"Feeding\")) return \"Feeding\";\n\tif (type.startsWith(\"Removal\")) return \"Removal\";\n\tif (type.startsWith(\"Waypoint\")) return \"Waypoint\";\n\tif (type.startsWith(\"PCX\")) return \"PCX\";\n\tif (type.startsWith(\"SDXP\")) return \"SDXP\";\n\tif (type.startsWith(\"TT\")) return \"TT\";\n\tif (type.startsWith(\"VMax\")) return \"VMax\";\n\tif (type.startsWith(\"Cuby\")) return \"Cuby\";\n\tif (type.startsWith(\"Weasel\")) return \"Weasel\";\n\tif (type.startsWith(\"PPS\")) return \"PPS\";\n\tif (type.startsWith(\"Pbl\")) return \"Pbl\";\n\treturn type;\n}\n\n/**\n * Convert HSL values to Babylon Color3.\n * Uses the same algorithm as hashColor() in entityRenderUtils for consistent\n * per-entity coloring between 2D and 3D.\n */\nfunction hslToColor3(hue: number, sat: number, light: number): Color3 {\n\tconst c = (1 - Math.abs(2 * light - 1)) * sat;\n\tconst x = c * (1 - Math.abs(((hue / 60) % 2) - 1));\n\tconst m = light - c / 2;\n\tlet r = 0,\n\t\tg = 0,\n\t\tb = 0;\n\tif (hue < 60) { r = c; g = x; }\n\telse if (hue < 120) { r = x; g = c; }\n\telse if (hue < 180) { g = c; b = x; }\n\telse if (hue < 240) { g = x; b = c; }\n\telse if (hue < 300) { r = x; b = c; }\n\telse { r = c; b = x; }\n\treturn new Color3(r + m, g + m, b + m);\n}\n\n/**\n * Per-entity-type PBR material parameters for realistic rendering.\n * Conveyors → matte industrial (low metallic, medium roughness)\n * Racks → painted metal (medium metallic, medium roughness)\n * SRM → machinery (higher metallic, lower roughness)\n * Robots → smooth surfaces (medium metallic, low roughness)\n * SCS → electronics (low metallic, medium roughness)\n * Shuttles → painted (medium metallic, medium roughness)\n * Scanners → equipment (medium metallic, medium roughness)\n */\nfunction getKindMaterialParams(kind: EntityShape3D): { metallic: number; roughness: number } {\n\tswitch (kind) {\n\t\tcase \"srm\":\n\t\t\treturn { metallic: 0.2, roughness: 0.45 };  // painted metal\n\t\tcase \"robot\":\n\t\t\treturn { metallic: 0.15, roughness: 0.5 };   // painted body\n\t\tcase \"shuttle\":\n\t\t\treturn { metallic: 0.2, roughness: 0.45 };   // painted metal\n\t\tcase \"rack\":\n\t\t\treturn { metallic: 0.15, roughness: 0.55 };  // painted steel\n\t\tcase \"scanner\":\n\t\tcase \"lift\":\n\t\t\treturn { metallic: 0.15, roughness: 0.5 };   // painted equipment\n\t\tcase \"scs\":\n\t\t\treturn { metallic: 0.1, roughness: 0.55 };   // plastic/electronic\n\t\tcase \"conveyor\":\n\t\tcase \"diverter\":\n\t\tcase \"merge\":\n\t\t\treturn { metallic: 0.05, roughness: 0.6 };   // painted steel\n\t\tcase \"processor\":\n\t\tcase \"dispatcher\":\n\t\t\treturn { metallic: 0.15, roughness: 0.5 };   // painted equipment\n\t\tcase \"source\":\n\t\tcase \"sink\":\n\t\t\treturn { metallic: 0.1, roughness: 0.55 };   // painted\n\t\tcase \"turntable\":\n\t\t\treturn { metallic: 0.15, roughness: 0.45 };  // painted metal\n\t\tcase \"cylinder\":\n\t\t\treturn { metallic: 0.15, roughness: 0.5 };   // painted\n\t\tdefault:\n\t\t\treturn { metallic: 0.1, roughness: 0.55 };\n\t}\n}\n\nfunction adjustColorForLayer(color: Color3, layer: number): Color3 {\n\tif (layer === 0) return color;\n\tconst shift = layer * 0.04;\n\treturn new Color3(\n\t\tMath.min(1, color.r + shift * 0.5),\n\t\tMath.min(1, color.g + shift * 0.3),\n\t\tMath.min(1, color.b - shift * 0.4),\n\t);\n}\n\nfunction createArrowCone(\n\thead: Vector3,\n\tdir: Vector3,\n\tname: string,\n\tscene: Scene,\n): Mesh {\n\tconst coneLength = 0.35;\n\tconst coneRadius = 0.18;\n\tconst conePos = head.subtract(dir.scale(coneLength * 0.5));\n\tconst cone = MeshBuilder.CreateCylinder(\n\t\tname,\n\t\t{\n\t\t\theight: coneLength,\n\t\t\tdiameterTop: 0,\n\t\t\tdiameterBottom: coneRadius * 2,\n\t\t\ttessellation: 8,\n\t\t},\n\t\tscene,\n\t);\n\tcone.position = conePos;\n\tconst yAxis = Vector3.Up();\n\tconst dot = Vector3.Dot(yAxis, dir);\n\tif (Math.abs(dot) > 0.9999) {\n\t\tif (dot < 0) {\n\t\t\tcone.rotation.x = Math.PI;\n\t\t}\n\t} else {\n\t\tconst axis = Vector3.Cross(yAxis, dir).normalize();\n\t\tconst angle = Math.acos(dot);\n\t\tcone.rotationQuaternion = Quaternion.RotationAxis(axis, angle);\n\t}\n\tcone.isPickable = false;\n\treturn cone;\n}\n\n/**\n * Create a procedural cube-map environment texture for PBR reflections.\n * Generates a studio-style lighting environment (soft gradient from\n * warm gray ceiling → neutral walls → dark cool floor).\n * This gives PBR materials something to reflect without requiring\n * an external HDR/EXR file.\n */\nfunction createProceduralEnvTexture(scene: Scene, size = 128): CubeTexture | null {\n\ttry {\n\t\tconst faceSize = size;\n\t\tconst faces: ArrayBufferView[] = [];\n\t\tconst faceDataSize = faceSize * faceSize * 4;\n\n\t\tfor (let face = 0; face < 6; face++) {\n\t\t\tconst data = new Uint8Array(faceDataSize);\n\t\t\tfor (let y = 0; y < faceSize; y++) {\n\t\t\t\tfor (let x = 0; x < faceSize; x++) {\n\t\t\t\t\tconst idx = (y * faceSize + x) * 4;\n\t\t\t\t\tconst u = x / faceSize;\n\t\t\t\t\tconst v = y / faceSize;\n\n\t\t\t\t\tlet r: number, g: number, b: number;\n\t\t\t\t\tswitch (face) {\n\t\t\t\t\t\tcase 2: { // +Y (top/ceiling): warehouse ceiling with bright LED lights\n\t\t\t\t\t\t\t// Base ceiling color (warm white)\n\t\t\t\t\t\t\tr = 180; g = 185; b = 195;\n\t\t\t\t\t\t\t// Simulate industrial LED light strips (rows of bright rectangles)\n\t\t\t\t\t\t\tconst stripeY = Math.floor(v * 8);\n\t\t\t\t\t\t\tconst stripeX = Math.floor(u * 6);\n\t\t\t\t\t\t\tconst inStripe = (v * 8 - stripeY) > 0.45 && (v * 8 - stripeY) < 0.55;\n\t\t\t\t\t\t\tconst brightSpot = (u * 6 - stripeX) > 0.3 && (u * 6 - stripeX) < 0.7;\n\t\t\t\t\t\t\tif (inStripe && brightSpot) {\n\t\t\t\t\t\t\t\t// Very bright warm white LED\n\t\t\t\t\t\t\t\tconst intensity = 0.9 + 0.1 * Math.sin(u * 40) * Math.cos(v * 40);\n\t\t\t\t\t\t\t\tr = Math.round(255 * intensity);\n\t\t\t\t\t\t\t\tg = Math.round(245 * intensity);\n\t\t\t\t\t\t\t\tb = Math.round(230 * intensity);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcase 3: // -Y (bottom/floor): dark warm gray with subtle reflections\n\t\t\t\t\t\t\tr = Math.round(50 + 20 * (1 - v));\n\t\t\t\t\t\t\tg = Math.round(52 + 20 * (1 - v));\n\t\t\t\t\t\t\tb = Math.round(60 + 20 * (1 - v));\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tdefault: {\n\t\t\t\t\t\t\t// sides: warehouse walls with warm ambient\n\t\t\t\t\t\t\tconst mix = v;\n\t\t\t\t\t\t\t// Top = ceiling warm, mid = wall neutral, bottom = floor dark\n\t\t\t\t\t\t\tconst topR = 175, topG = 178, topB = 188;\n\t\t\t\t\t\t\tconst midR = 120, midG = 122, midB = 135;\n\t\t\t\t\t\t\tconst botR = 45, botG = 48, botB = 58;\n\t\t\t\t\t\t\tif (mix < 0.35) {\n\t\t\t\t\t\t\t\tconst t = mix / 0.35;\n\t\t\t\t\t\t\t\tr = Math.round(topR + (midR - topR) * t);\n\t\t\t\t\t\t\t\tg = Math.round(topG + (midG - topG) * t);\n\t\t\t\t\t\t\t\tb = Math.round(topB + (midB - topB) * t);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tconst t = (mix - 0.35) / 0.65;\n\t\t\t\t\t\t\t\tr = Math.round(midR + (botR - midR) * t);\n\t\t\t\t\t\t\t\tg = Math.round(midG + (botG - midG) * t);\n\t\t\t\t\t\t\t\tb = Math.round(midB + (botB - midB) * t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Subtle horizontal bands (warehouse structural beams)\n\t\t\t\t\t\t\tif (Math.abs(v - 0.35) < 0.02 || Math.abs(v - 0.65) < 0.02) {\n\t\t\t\t\t\t\t\tr = Math.round(r * 0.85);\n\t\t\t\t\t\t\t\tg = Math.round(g * 0.85);\n\t\t\t\t\t\t\t\tb = Math.round(b * 0.85);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tdata[idx] = Math.min(255, Math.max(0, r));\n\t\t\t\t\tdata[idx + 1] = Math.min(255, Math.max(0, g));\n\t\t\t\t\tdata[idx + 2] = Math.min(255, Math.max(0, b));\n\t\t\t\t\tdata[idx + 3] = 255;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfaces.push(data);\n\t\t}\n\n\t\tconst texture = new RawCubeTexture(\n\t\t\tscene,\n\t\t\tfaces,\n\t\t\tfaceSize,\n\t\t\t5, // Engine.TEXTUREFORMAT_RGBA\n\t\t\t0, // Engine.TEXTURETYPE_UNSIGNED_BYTE\n\t\t\ttrue, // generateMipMaps for smoother reflections\n\t\t);\n\t\ttexture.gammaSpace = false;\n\t\treturn texture;\n\t} catch (e) {\n\t\tconsole.warn(\"[Scene3D] Failed to create procedural env texture:\", e);\n\t\treturn null;\n\t}\n}\n\n/**\n * Create a dynamic ground texture with a subtle grid pattern\n * to give the ground plane visual detail.\n */\nfunction createGroundTexture(scene: Scene): DynamicTexture {\n\tconst tex = new DynamicTexture(\"groundDetail\", 512, scene, false);\n\tconst ctx = tex.getContext();\n\tconst w = tex.getSize().width;\n\tconst h = tex.getSize().height;\n\n\t// Dark industrial floor background\n\tctx.fillStyle = \"#1a1a22\";\n\tctx.fillRect(0, 0, w, h);\n\n\t// Subtle grid lines every 32px (25 world units at default zoom)\n\tctx.strokeStyle = \"#2a2a38\";\n\tctx.lineWidth = 1;\n\tconst step = 32;\n\tfor (let x = 0; x <= w; x += step) {\n\t\tctx.beginPath();\n\t\tctx.moveTo(x, 0);\n\t\tctx.lineTo(x, h);\n\t\tctx.stroke();\n\t}\n\tfor (let y = 0; y <= h; y += step) {\n\t\tctx.beginPath();\n\t\tctx.moveTo(0, y);\n\t\tctx.lineTo(w, y);\n\t\tctx.stroke();\n\t}\n\n\t// Slightly brighter grid lines every 128px (major divisions)\n\tctx.strokeStyle = \"#3a3a4a\";\n\tctx.lineWidth = 1.5;\n\tconst majorStep = step * 4;\n\tfor (let x = 0; x <= w; x += majorStep) {\n\t\tctx.beginPath();\n\t\tctx.moveTo(x, 0);\n\t\tctx.lineTo(x, h);\n\t\tctx.stroke();\n\t}\n\tfor (let y = 0; y <= h; y += majorStep) {\n\t\tctx.beginPath();\n\t\tctx.moveTo(0, y);\n\t\tctx.lineTo(w, y);\n\t\tctx.stroke();\n\t}\n\n\ttex.update();\n\treturn tex;\n}\n\nexport function Scene3D() {\n\tconst canvasRef = useRef<HTMLCanvasElement>(null);\n\tconst labelRef = useRef<HTMLDivElement>(null);\n\tconst engineRef = useRef<Engine | null>(null);\n\tconst sceneRef = useRef<Scene | null>(null);\n\tconst entityMeshesRef = useRef<Map<string, Mesh>>(new Map());\n\tconst itemMeshesRef = useRef<Map<string, Mesh>>(new Map());\n\tconst linkMeshRef = useRef<Mesh | null>(null);\n\tconst arrowCacheRef = useRef<Map<string, Mesh>>(new Map());\n\tconst flowParticlesRef = useRef<Mesh[]>([]);\n\tconst materialCacheRef = useRef<Map<string, PBRMaterial>>(new Map());\n\tconst selectedIdRef = useRef<string | null>(null);\n\tconst highlightRef = useRef<HighlightLayer | null>(null);\n\tconst disposedRef = useRef(false);\n\tconst groundRef = useRef<Mesh | null>(null);\n\tconst dragMeshRef = useRef<Mesh | null>(null);\n\tconst dragOrigPosRef = useRef<Vector3 | null>(null);\n\tconst isDraggingRef = useRef(false);\n\tconst isRotatingRef = useRef(false);\n\tconst pointerDownPosRef = useRef({ x: 0, y: 0 });\n\tconst rotateOrigYRef = useRef(0);\n\tconst rotateStartXRef = useRef(0);\n\tconst entitiesRef = useRef<Map<string, EntityDTO>>(new Map());\n\tconst entityShapeKindsRef = useRef<Map<string, ShapeKind>>(new Map());\n\tconst entitySizesRef = useRef<\n\t\tMap<string, { sx: number; sy: number; sz: number }>\n\t>(new Map());\n\tconst prevPositionsRef = useRef<\n\t\tMap<string, { px: number; py: number; pz: number; t: number }>\n\t>(new Map());\n\tconst overlayMeshesRef = useRef<Map<string, Mesh>>(new Map());\n\tconst overlayHashRef = useRef<Map<string, string>>(new Map());\n\tconst cameraRef = useRef<ArcRotateCamera | null>(null);\n\tconst metricsRef = useRef<HTMLDivElement | null>(null);\n\tconst thinManagerRef = useRef<ThinInstanceManager | null>(null);\n\tlet frameCounter = 0;\n\n\tconst selectEntity = useSimStore((s) => s.selectEntity);\n\tconst selectedEntityId = useSimStore((s) => s.selectedEntityId);\n\tconst updateEntity = useSimStore((s) => s.updateEntity);\n\tconst addEntity = useSimStore((s) => s.addEntity);\n\tconst simTime = useSimStore((s) => s.simTime);\n\tconst _simRunning = useSimStore((s) => s.simRunning);\n\tconst simTimeRef = useRef(0);\n\tconst simTimeWallRef = useRef(0);\n\n\tuseEffect(() => {\n\t\tsimTimeRef.current = simTime;\n\t\tsimTimeWallRef.current = performance.now();\n\t}, [simTime]);\n\n\t// Sync 3D viewport to store immediately when switching away from 3D\n\t// and restore viewport from 2D when switching TO 3D\n\tconst viewMode = useSimStore((s) => s.viewMode);\n\tconst prevViewModeRef = useRef<\"2d\" | \"3d\">(\"2d\");\n\tuseEffect(() => {\n\t\tif (viewMode === \"3d\" && prevViewModeRef.current === \"2d\") {\n\t\t\t// Restore viewport from 2D state when entering 3D\n\t\t\tconst cam = cameraRef.current;\n\t\t\tif (cam) {\n\t\t\t\tconst vp = useSimStore.getState();\n\t\t\t\tcam.target.set(vp.viewCenterX, 0, vp.viewCenterZ);\n\t\t\t\tcam.radius = 60 / Math.max(vp.viewZoom, 0.1);\n\t\t\t}\n\t\t} else if (viewMode !== \"3d\") {\n\t\t\t// Save current 3D viewport to store when leaving 3D\n\t\t\tconst cam = cameraRef.current;\n\t\t\tif (cam) {\n\t\t\t\tuseSimStore.setState({\n\t\t\t\t\tviewCenterX: cam.target.x,\n\t\t\t\t\tviewCenterZ: cam.target.z,\n\t\t\t\t\tviewZoom: 60 / cam.radius,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tprevViewModeRef.current = viewMode;\n\t}, [viewMode]);\n\n\tselectedIdRef.current = selectedEntityId;\n\n\tconst getMaterial = useCallback(\n\t\t(\n\t\t\tscene: Scene,\n\t\t\tkey: string,\n\t\t\tcolor: Color3,\n\t\t\talpha: number,\n\t\t\twireframe: boolean,\n\t\t\tmetallic = 0.0,\n\t\t\troughness = 0.7,\n\t\t): PBRMaterial => {\n\t\t\tconst cache = materialCacheRef.current;\n\t\t\tconst matKey = `${key}_${metallic}_${roughness}`;\n\t\t\tlet mat = cache.get(matKey);\n\t\t\tif (mat) {\n\t\t\t\t// LRU: re-insert to move to end (Map preserves insertion order)\n\t\t\t\tcache.delete(matKey);\n\t\t\t\tcache.set(matKey, mat);\n\t\t\t\treturn mat;\n\t\t\t}\n\t\t\t// Cap cache to 500 materials — evict oldest (Map first key) on overflow\n\t\t\tif (cache.size >= 500) {\n\t\t\t\tconst firstKey = cache.keys().next().value as string | undefined;\n\t\t\t\tif (firstKey) {\n\t\t\t\t\tconst oldMat = cache.get(firstKey);\n\t\t\t\t\tif (oldMat) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\toldMat.dispose();\n\t\t\t\t\t\t} catch {}\n\t\t\t\t\t}\n\t\t\t\t\tcache.delete(firstKey);\n\t\t\t\t}\n\t\t\t}\n\t\t\tmat = new PBRMaterial(matKey, scene);\n\t\t\tmat.albedoColor = color;\n\t\t\tmat.metallic = metallic;\n\t\t\tmat.roughness = roughness;\n\t\t\tmat.environmentIntensity = 0.6;\n\t\t\tmat.alpha = alpha;\n\t\t\tmat.wireframe = wireframe;\n\t\t\tmat.freeze();\n\t\t\tcache.set(matKey, mat);\n\t\t\treturn mat;\n\t\t},\n\t\t[],\n\t);\n\n\tuseEffect(() => {\n\t\tconst canvas = canvasRef.current;\n\t\tconst labelEl = labelRef.current;\n\t\tif (!canvas) return;\n\t\tdisposedRef.current = false;\n\n\t\tconst engine = new Engine(canvas, true, {\n\t\t\tpreserveDrawingBuffer: false,\n\t\t\tstencil: true,\n\t\t\tpowerPreference: \"high-performance\",\n\t\t\tadaptToDeviceRatio: true,  // auto-scale to device pixel ratio\n\t\t});\n\t\tengine.setHardwareScalingLevel(1 / window.devicePixelRatio);  // crisp rendering on HiDPI\n\t\tengineRef.current = engine;\n\n\t\tconst scene = new Scene(engine);\n\t\t// Deep navy blue sky background (matching reference industrial warehouse render)\n\t\tscene.clearColor = new Color4(0.039, 0.055, 0.157, 1.0); // #0a0e28\n\t\tscene.ambientColor = new Color3(0.2, 0.2, 0.25);\n\t\tsceneRef.current = scene;\n\n\t\t// ── Procedural environment texture for PBR reflections (IBL) ──\n\t\t// Higher resolution + higher intensity for realistic metal reflections\n\t\tconst envTex = createProceduralEnvTexture(scene, 1024);\n\t\tif (envTex) {\n\t\t\tscene.environmentTexture = envTex;\n\t\t\tscene.environmentIntensity = 1.0;  // IBL strength for edge highlights on metal\n\t\t}\n\n\t\t// ── Fog for depth perception (deep navy blue atmosphere) ──\n\t\tscene.fogMode = Scene.FOGMODE_EXP2;\n\t\tscene.fogDensity = 0.0025;\n\t\tscene.fogColor = new Color3(0.039, 0.055, 0.157); // match sky\n\n\t\t// ── Image processing / tone mapping ──\n\t\tscene.imageProcessingConfiguration.toneMappingEnabled = true;\n\t\tscene.imageProcessingConfiguration.toneMappingType =\n\t\t\tImageProcessingConfiguration.TONEMAPPING_ACES;\n\t\tscene.imageProcessingConfiguration.contrast = 1.05;\n\t\tscene.imageProcessingConfiguration.exposure = 1.1;\n\t\tscene.imageProcessingConfiguration.vignetteEnabled = true;\n\t\tscene.imageProcessingConfiguration.vignetteWeight = 0.06;\n\t\tscene.imageProcessingConfiguration.vignetteCameraFov = 0.85;\n\n\t\tconst camera = new ArcRotateCamera(\n\t\t\t\"cam\",\n\t\t\t-Math.PI / 3,\n\t\t\tMath.PI / 4,\n\t\t\t60,\n\t\t\tnew Vector3(0, 0, 0),\n\t\t\tscene,\n\t\t);\n\t\tcamera.attachControl(true);\n\t\tcamera.lowerRadiusLimit = 2;\n\t\tcamera.upperRadiusLimit = 500;\n\t\tcamera.lowerBetaLimit = 0.1;\n\t\tcamera.upperBetaLimit = Math.PI / 2.1;\n\t\tcamera.wheelDeltaPercentage = 0.01;\n\t\tcamera.minZ = 0.1;\n\t\tcamera.maxZ = 1000;\n\t\tcamera.panningSensibility = 40;\n\t\tcamera.angularSensibilityX = 800;\n\t\tcamera.angularSensibilityY = 800;\n\n\t\t// Restore viewport from 2D scene (shared XZ plane)\n\t\tconst vp = useSimStore.getState();\n\t\tcamera.target.set(vp.viewCenterX, 0, vp.viewCenterZ);\n\t\tcamera.radius = 60 / Math.max(vp.viewZoom, 0.1);\n\n\t\tcameraRef.current = camera;\n\n\t\tconst thinManager = new ThinInstanceManager();\n\t\tthinManagerRef.current = thinManager;\n\n\t\t// ── Industrial warehouse lighting setup ──\n\t\t// Main directional light: angled to produce visible shadows on ground\n\t\t// Keep intensity moderate to avoid shadow getting dirty when SSAO stacks\n\t\tconst sun = new DirectionalLight(\n\t\t\t\"sun\",\n\t\t\tnew Vector3(-0.5, -1, 0.3).normalize(),\n\t\t\tscene,\n\t\t);\n\t\tsun.intensity = 1.2;\n\t\tsun.diffuse = new Color3(0.95, 0.96, 1.0); // cool white LED\n\t\tsun.specular = new Color3(0.5, 0.52, 0.6); // soft highlight\n\n\t\t// Fill light: subtle cool bounce from opposite side\n\t\tconst fill = new DirectionalLight(\n\t\t\t\"fill\",\n\t\t\tnew Vector3(0.4, -0.6, -0.5).normalize(),\n\t\t\tscene,\n\t\t);\n\t\tfill.intensity = 0.3;\n\t\tfill.diffuse = new Color3(0.7, 0.75, 0.9);\n\t\tfill.specular = new Color3(0.15, 0.18, 0.25);\n\n\t\t// Hemisphere light: simulates warehouse ceiling light panels\n\t\tconst hemi = new HemisphericLight(\"hemi\", new Vector3(0, 1, 0), scene);\n\t\themi.intensity = 0.5;\n\t\themi.diffuse = new Color3(0.92, 0.94, 0.98);\n\t\themi.groundColor = new Color3(0.08, 0.09, 0.12);\n\n\t\t// ── Cascaded Shadow Map (for large warehouse scenes 100m+) ──\n\t\t// CSM splits frustum into cascades for consistent shadow quality at all distances\n\t\tconst shadowGen = new CascadedShadowGenerator(2048, sun);\n\t\tshadowGen.numCascades = 3;\n\t\tshadowGen.stabilizeCascades = true;\n\t\tshadowGen.lambda = 0.5;\n\t\tshadowGen.filter = ShadowGenerator.FILTER_PCF;\n\t\tshadowGen.setDarkness(0.3);\n\t\tshadowGen.bias = 0.003;\n\t\tshadowGen.normalBias = 0.02;\n\t\tshadowGen.freezeShadowCastersBoundingInfo = true;\n\t\t// Limit shadow distance to 50m - far racks don't need shadows\n\t\tshadowGen.setMinMaxDistance(0, 50);\n\n\t\tconst hl = new HighlightLayer(\"hl\", scene, {\n\t\t\tmainTextureFixedSize: 512,\n\t\t\tblurHorizontalSize: 1.2,\n\t\t\tblurVerticalSize: 1.2,\n\t\t});\n\t\t(hl as any).outerGlow = true;\n\t\t(hl as any).innerGlow = false;\n\t\thighlightRef.current = hl;\n\n\t\t// ── SSAO2 Pipeline (critical for rack depth/layering) ──\n\t\t// Warehouse scenes need large radius (4-6) because shelf bays are 1-2m wide\n\t\t// Without AO, rack bays look flat; SSAO2 adds shadow in corners/crevices\n\t\tconst ssao = new SSAO2RenderingPipeline(\n\t\t\t\"ssao\",\n\t\t\tscene,\n\t\t\t{\n\t\t\t\tssaoRatio: 0.5,        // half-res for performance\n\t\t\t\tblurRatio: 1.0,        // full-res blur\n\t\t\t},\n\t\t\t[camera],\n\t\t);\n\t\tssao.totalStrength = 1.5;      // stronger AO for industrial scenes\n\t\tssao.radius = 5.0;             // large radius for warehouse-scale geometry\n\t\tssao.base = 0.05;              // minimum AO (prevents fully black)\n\t\tssao.expensiveBlur = true;     // smoother AO\n\t\tssao.samples = 24;             // higher quality for large radius\n\t\tssao.maxZ = 150;               // affect geometry within 150m\n\n\t\t// ── FXAA Anti-aliasing ──\n\t\tconst fxaa = new FxaaPostProcess(\"fxaa\", 1.0, camera);\n\n\t\t// ── Performance Instrumentation ──\n\t\tconst sceneInstr = new SceneInstrumentation(scene);\n\t\tsceneInstr.captureActiveMeshesEvaluationTime = true;\n\t\tsceneInstr.captureFrameTime = true;\n\t\tsceneInstr.captureRenderTime = true;\n\t\tsceneInstr.captureInterFrameTime = true;\n\n\t\tconst ground = MeshBuilder.CreateGround(\n\t\t\t\"__ground__\",\n\t\t\t{ width: 500, height: 500, subdivisions: 1 },\n\t\t\tscene,\n\t\t);\n\t\tground.receiveShadows = true;\n\t\tconst groundMat = new PBRMaterial(\"groundMat\", scene);\n\t\t// Dark asphalt / smooth concrete (matching reference warehouse render)\n\t\tgroundMat.albedoColor = new Color3(0.12, 0.13, 0.16); // #1e2128 dark asphalt\n\t\tgroundMat.roughness = 0.85; // mostly matte, slight micro-reflectivity\n\t\tgroundMat.metallic = 0.0;\n\t\tgroundMat.environmentIntensity = 0.15; // very subtle environment reflection\n\t\tground.material = groundMat;\n\t\tground.isPickable = true;\n\t\tground.metadata = { isGround: true };\n\t\tground.position.y = -0.05;\n\t\tgroundRef.current = ground;\n\n\t\t// ─ Yellow floor safety lines along conveyor lanes ──\n\t\t// These are built per-conveyor during render loop for dynamic positioning\n\t\tscene.onPointerDown = (_evt, pickResult) => {\n\t\t\tif (disposedRef.current) return;\n\t\t\tif (_evt.button !== 0) return;\n\t\t\tconst locked = useSimStore.getState().modelLocked;\n\t\t\tif (pickResult?.hit && pickResult.pickedMesh?.metadata?.entityId) {\n\t\t\t\tconst mesh = pickResult.pickedMesh;\n\t\t\t\tconst entityId = mesh.metadata.entityId as string;\n\t\t\t\tselectEntity(entityId);\n\t\t\t\tif (!locked || _evt.altKey) {\n\t\t\t\t\tdragMeshRef.current = mesh as Mesh;\n\t\t\t\t\tdragOrigPosRef.current = (mesh as Mesh).position.clone();\n\t\t\t\t\tpointerDownPosRef.current = { x: scene.pointerX, y: scene.pointerY };\n\t\t\t\t\tisDraggingRef.current = false;\n\t\t\t\t}\n\t\t\t\tif (_evt.altKey) {\n\t\t\t\t\tisRotatingRef.current = true;\n\t\t\t\t\trotateStartXRef.current = scene.pointerX;\n\t\t\t\t\trotateOrigYRef.current = mesh.rotation.y;\n\t\t\t\t\tcamera.detachControl();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tselectEntity(null);\n\t\t\t\tdragMeshRef.current = null;\n\t\t\t}\n\t\t};\n\n\t\tscene.onPointerMove = () => {\n\t\t\tif (disposedRef.current) return;\n\t\t\tconst dragMesh = dragMeshRef.current;\n\t\t\tif (!dragMesh || dragMesh.isDisposed()) return;\n\t\t\tif (isRotatingRef.current) {\n\t\t\t\tconst dx = scene.pointerX - rotateStartXRef.current;\n\t\t\t\tdragMesh.rotation.y = rotateOrigYRef.current + dx * 0.02;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!isDraggingRef.current) {\n\t\t\t\tconst dx = scene.pointerX - pointerDownPosRef.current.x;\n\t\t\t\tconst dy = scene.pointerY - pointerDownPosRef.current.y;\n\t\t\t\tif (Math.sqrt(dx * dx + dy * dy) > 5) {\n\t\t\t\t\tisDraggingRef.current = true;\n\t\t\t\t\tcamera.detachControl();\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (isDraggingRef.current) {\n\t\t\t\tconst gnd = groundRef.current;\n\t\t\t\tif (!gnd) return;\n\t\t\t\ttry {\n\t\t\t\t\tconst pick = scene.pick(\n\t\t\t\t\t\tscene.pointerX,\n\t\t\t\t\t\tscene.pointerY,\n\t\t\t\t\t\t(m) => m === gnd,\n\t\t\t\t\t);\n\t\t\t\t\tif (pick?.pickedPoint) {\n\t\t\t\t\t\tdragMesh.position.x = pick.pickedPoint.x;\n\t\t\t\t\t\tdragMesh.position.z = pick.pickedPoint.z;\n\t\t\t\t\t}\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t};\n\n\t\tscene.onPointerUp = () => {\n\t\t\tif (disposedRef.current) return;\n\t\t\tconst dragMesh = dragMeshRef.current;\n\t\t\tif (isRotatingRef.current && dragMesh) {\n\t\t\t\tconst eid = dragMesh.metadata?.entityId as string;\n\t\t\t\tif (eid) {\n\t\t\t\t\tconst rotDeg = (-dragMesh.rotation.y * 180) / Math.PI;\n\t\t\t\t\tupdateEntity(eid, { rotation: rotDeg });\n\t\t\t\t}\n\t\t\t\tcamera.attachControl(true);\n\t\t\t} else if (isDraggingRef.current && dragMesh) {\n\t\t\t\tconst eid = dragMesh.metadata?.entityId as string;\n\t\t\t\tif (eid) {\n\t\t\t\t\tupdateEntity(eid, {\n\t\t\t\t\t\tposx: Math.round(dragMesh.position.x * 100) / 100,\n\t\t\t\t\t\tposy: Math.round(dragMesh.position.z * 100) / 100,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tcamera.attachControl(true);\n\t\t\t}\n\t\t\tdragMeshRef.current = null;\n\t\t\tisDraggingRef.current = false;\n\t\t\tisRotatingRef.current = false;\n\t\t};\n\n\t\tengine.runRenderLoop(() => {\n\t\t\ttry {\n\t\t\t\tif (disposedRef.current) return;\n\t\t\t\tif (scene.isDisposed) return;\n\t\t\t\tif (engine.isDisposed) return;\n\t\t\t\tconst storeState = useSimStore.getState();\n\t\t\t\tconst entities = storeState.entities;\n\t\t\t\tentitiesRef.current = entities;\n\n\t\t\t\t// ThinInstance: rebuild if entity set changed, get handled IDs\n\t\t\t\tconst thinManager = thinManagerRef.current;\n\t\t\t\tconst sc = storeState.serverConfig;\n\t\t\t\tif (thinManager) {\n\t\t\t\t\tthinManager.checkRebuild(entities, scene);\n\t\t\t\t}\n\t\t\t\tconst thinIds = thinManager?.handledIds ?? new Set<string>();\n\t\t\t\tconst entityMeshes = entityMeshesRef.current;\n\t\t\t\tconst itemMeshes = itemMeshesRef.current;\n\t\t\t\tconst currentIds = new Set<string>();\n\t\t\t\tconst now = performance.now();\n\n\t\t\t\t// Performance boundary: cap complex (non-instanced) entities at 2000 per frame.\n\t\t\t\t// ThinInstance entities bypass this cap since they share a single draw call.\n\t\t\t\tconst MAX_COMPLEX_PER_FRAME = 2000;\n\t\t\t\tlet entityCount = 0;\n\t\t\t\tlet complexCount = 0;\n\n\t\t\t\t// Sync 3D viewport to store every ~10 frames (throttled)\n\t\t\t\tframeCounter++;\n\t\t\t\tif (frameCounter % 10 === 0) {\n\t\t\t\t\tconst cam = cameraRef.current;\n\t\t\t\t\tif (cam) {\n\t\t\t\t\t\tuseSimStore\n\t\t\t\t\t\t\t.getState()\n\t\t\t\t\t\t\t.setViewport(cam.target.x, cam.target.z, 60 / cam.radius);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (const [id, entity] of entities) {\n\t\t\t\t\tentityCount++;\n\t\t\t\t\tcurrentIds.add(id);\n\t\t\t\t\t// ThinInstance entities: skip mesh creation/material/overlay\n\t\t\t\t\tif (thinIds.has(id)) continue;\n\t\t\t\t\t// Cap complex (non-instanced) entities to protect GPU/CPU\n\t\t\t\t\tcomplexCount++;\n\t\t\t\t\tif (complexCount > MAX_COMPLEX_PER_FRAME) continue;\n\t\t\t\t\tconst activeLayer = useSimStore.getState().activeLayer ?? 0;\n\t\t\t\t\tconst entityLayer = entity.layer ?? 0;\n\t\t\t\t\tconst multiLayers = (entity as unknown as Record<string, unknown>)\n\t\t\t\t\t\t.multiLayers as number[] | undefined;\n\t\t\t\t\tif (\n\t\t\t\t\t\tentityLayer !== activeLayer &&\n\t\t\t\t\t\t!multiLayers?.includes(activeLayer)\n\t\t\t\t\t)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tif (\n\t\t\t\t\t\t!Number.isFinite(entity.posx) ||\n\t\t\t\t\t\t!Number.isFinite(entity.posy) ||\n\t\t\t\t\t\t!Number.isFinite(entity.sizex ?? 1) ||\n\t\t\t\t\t\t!Number.isFinite(entity.sizey ?? 1)\n\t\t\t\t\t)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t// Boundary protection: skip entities at extreme positions\n\t\t\t\t\t// (beyond ±10000 world units) to prevent floating-point\n\t\t\t\t\t// precision artifacts and camera targeting issues.\n\t\t\t\t\tif (\n\t\t\t\t\t\tMath.abs(entity.posx) > 10000 ||\n\t\t\t\t\t\tMath.abs(entity.posy) > 10000 ||\n\t\t\t\t\t\tMath.abs(entity.sizex ?? 1) > 10000 ||\n\t\t\t\t\t\tMath.abs(entity.sizey ?? 1) > 10000\n\t\t\t\t\t)\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tconst sx = Math.max(entity.sizex, 0.1);\n\t\t\t\t\tconst sy = Math.max(entity.sizey, 0.1);\n\t\t\t\t\tconst isz = Number.isFinite(entity.sizez) ? entity.sizez : 0.5;\n\t\t\t\t\tconst sz = Math.max(isz, 0.1);\n\t\t\t\t\tconst height = sz;\n\t\t\t\t\tconst px = entity.posx;\n\t\t\t\t\tconst py = entity.posy;\n\t\t\t\t\tconst pz = Number.isFinite(entity.posz) ? entity.posz : 0;\n\n\t\t\t\t\tlet mesh = entityMeshes.get(id);\n\t\t\t\t\tconst kind = resolveShape3D(entity.entityType ?? \"\");\n\t\t\t\t\tconst prevKind = entityShapeKindsRef.current.get(id);\n\t\t\t\t\tconst prevSize = entitySizesRef.current.get(id);\n\t\t\t\t\tconst sizeChanged =\n\t\t\t\t\t\t!prevSize ||\n\t\t\t\t\t\tprevSize.sx !== sx ||\n\t\t\t\t\t\tprevSize.sy !== sy ||\n\t\t\t\t\t\tprevSize.sz !== sz;\n\n\t\t\t\t\tif (\n\t\t\t\t\t\t!mesh ||\n\t\t\t\t\t\tmesh.isDisposed() ||\n\t\t\t\t\t\t(prevKind !== undefined && prevKind !== kind) ||\n\t\t\t\t\t\tsizeChanged\n\t\t\t\t\t) {\n\t\t\t\t\t\tif (mesh && !mesh.isDisposed()) {\n\t\t\t\t\t\t\thl.removeMesh(mesh as any);\n\t\t\t\t\t\t\tmesh.getChildMeshes().forEach((child) => {\n\t\t\t\t\t\t\t\thl.removeMesh(child as any);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tmesh.dispose();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmesh = buildShapeMesh(kind, `e_${id}`, sx, sy, sz, scene, entity);\n\t\t\t\t\t\tmesh.metadata = { entityId: id };\n\t\t\t\t\t\tmesh.receiveShadows = true;\n\t\t\t\t\t\tentityMeshes.set(id, mesh);\n\t\t\t\t\t\tentityShapeKindsRef.current.set(id, kind);\n\t\t\t\t\t\tentitySizesRef.current.set(id, { sx, sy, sz });\n\t\t\t\t\t\tshadowGen.addShadowCaster(mesh);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!(isDraggingRef.current && selectedIdRef.current === id)) {\n\t\t\t\t\t\tconst prev = prevPositionsRef.current.get(id);\n\t\t\t\t\t\t// For rack/srm: 3D structures are built from ground up, so base Y = pz\n\t\t\t\t\t\t// For conveyor/scanner/robot: center vertically at height/2 + pz\n\t\t\t\t\t\t// For scs: flat box at Y=0 so overlay renders on top\n\t\t\t\t\t\tconst meshY =\n\t\t\t\t\t\t\tkind === \"rack\" || kind === \"srm\"\n\t\t\t\t\t\t\t\t? pz\n\t\t\t\t\t\t\t\t: kind === \"scs\"\n\t\t\t\t\t\t\t\t\t? pz\n\t\t\t\t\t\t\t\t\t: height / 2 + pz;\n\t\t\t\t\t\tif (prev && now - prev.t < 200) {\n\t\t\t\t\t\t\tconst alpha = (now - prev.t) / 200;\n\t\t\t\t\t\t\tmesh.position.x = prev.px + (px - prev.px) * alpha;\n\t\t\t\t\t\t\tmesh.position.z = prev.py + (py - prev.py) * alpha;\n\t\t\t\t\t\t\tmesh.position.y = meshY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tmesh.position.set(px, meshY, py);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprevPositionsRef.current.set(id, { px, py, pz, t: now });\n\t\t\t\t\t}\n\n\t\t\t\t\tmesh.rotation.y =\n\t\t\t\t\t\t-(\n\t\t\t\t\t\t\t(Number.isFinite(entity.rotation)\n\t\t\t\t\t\t\t\t? normalizeRotation(entity.rotation)\n\t\t\t\t\t\t\t\t: 0) * Math.PI\n\t\t\t\t\t\t) / 180;\n\t\t\t\t\tif (kind !== \"box\" && kind !== \"conveyor\") {\n\t\t\t\t\t\tmesh.rotation.x = 0;\n\t\t\t\t\t\tmesh.rotation.z = 0;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst stateInfo = detectEntityState(\n\t\t\t\t\t\tentity.state ?? \"\",\n\t\t\t\t\t\tentity.itemCount ?? 0,\n\t\t\t\t\t\tentity.items?.some((item) => item.moving) ?? false,\n\t\t\t\t\t);\n\t\t\t\t\tconst entityProps2 = entity.properties as\n\t\t\t\t\t\t| Record<string, unknown>\n\t\t\t\t\t\t| undefined;\n\t\t\t\t\tconst isTransparent =\n\t\t\t\t\t\tentity.transparent ??\n\t\t\t\t\t\t(entityProps2?.transparent as boolean) ??\n\t\t\t\t\t\tgetDefaultTransparent(kind);\n\n\t\t\t\t\t// Java AbstractSRM2.getBackgroundColor: special colors for\n\t\t\t\t\t// ignoreCompletion (#A9A9A9) and disableStatusMessage (#EE82EE)\n\t\t\t\t\tconst srmProps = entity.properties as\n\t\t\t\t\t\t| Record<string, unknown>\n\t\t\t\t\t\t| undefined;\n\t\t\t\t\tconst isSRMKind = kind === \"srm\";\n\t\t\t\t\tconst ignoreCompletion =\n\t\t\t\t\t\tisSRMKind && ((srmProps?.ignoreCompletion as boolean) ?? false);\n\t\t\t\t\tconst disableStatusMessage =\n\t\t\t\t\t\tisSRMKind && ((srmProps?.disableStatusMessage as boolean) ?? false);\n\n\t\t\t\t\tlet baseColor: Color3;\n\t\t\t\t\tconst srmBgHex = getSrmBackgroundColor(\n\t\t\t\t\t\tignoreCompletion,\n\t\t\t\t\t\tdisableStatusMessage,\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t);\n\t\t\t\t\tconst sc = useSimStore.getState().serverConfig;\n\t\t\t\t\tif (srmBgHex) {\n\t\t\t\t\t\tbaseColor = hexToColor3(srmBgHex, Color3.Black());\n\t\t\t\t\t} else if (stateInfo.isError) {\n\t\t\t\t\t\tbaseColor = hexToColor3(sc?.entityErrorColor, new Color3(1, 0, 0));\n\t\t\t\t\t} else if (stateInfo.isMoving) {\n\t\t\t\t\t\tbaseColor = hexToColor3(\n\t\t\t\t\t\t\tsc?.entityBusyColor,\n\t\t\t\t\t\t\tnew Color3(0, 0.392, 1),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else if (stateInfo.hasItems) {\n\t\t\t\t\t\tbaseColor = hexToColor3(\n\t\t\t\t\t\t\tsc?.entityOccupiedColor1,\n\t\t\t\t\t\t\tnew Color3(1, 1, 0),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else if (stateInfo.isInterrupted) {\n\t\t\t\t\t\tbaseColor = hexToColor3(\n\t\t\t\t\t\t\tsc?.entityInterruptColor,\n\t\t\t\t\t\t\tnew Color3(0.753, 0.753, 0.753),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Check randomItemColors / colorR/G/B/T custom colors (matching 2D)\n\t\t\t\t\t\tconst props = entity.properties as\n\t\t\t\t\t\t\t| Record<string, unknown>\n\t\t\t\t\t\t\t| undefined;\n\t\t\t\t\t\tconst colorR = props?.colorR as number | undefined;\n\t\t\t\t\t\tconst colorG = props?.colorG as number | undefined;\n\t\t\t\t\t\tconst colorB = props?.colorB as number | undefined;\n\t\t\t\t\t\tconst colorT = props?.colorT as number | undefined;\n\t\t\t\t\t\tif (sc?.randomItemColors && colorR === undefined && colorG === undefined && colorB === undefined) {\n\t\t\t\t\t\t\t// Hash-based per-entity color (matching 2D hashColor)\n\t\t\t\t\t\t\tconst hue = hashColorHue(entity.id);\n\t\t\t\t\t\t\tbaseColor = hslToColor3(hue, 0.6, 0.65);\n\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\tcolorR !== undefined &&\n\t\t\t\t\t\t\tcolorG !== undefined &&\n\t\t\t\t\t\t\tcolorB !== undefined &&\n\t\t\t\t\t\t\tcolorT !== undefined &&\n\t\t\t\t\t\t\t!(colorR === 255 && colorG === 255 && colorB === 255 && colorT === 255)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tconst r = (colorR ?? 255) / 255;\n\t\t\t\t\t\t\tconst g = (colorG ?? 255) / 255;\n\t\t\t\t\t\t\tconst b = (colorB ?? 255) / 255;\n\t\t\t\t\t\t\tbaseColor = new Color3(r, g, b);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst idleHex = sc?.entityIdleColor || \"#FFFFFF\";\n\t\t\t\t\t\t\tbaseColor = hexToColor3(\n\t\t\t\t\t\t\t\tentity.backgroundColor,\n\t\t\t\t\t\t\t\thexToColor3(idleHex, Color3.White()),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst color = adjustColorForLayer(baseColor, entity.layer ?? 0);\n\t\t\t\t\tconst wireframeMode = useSimStore.getState().wireframe;\n\t\t\t\t\t// Determine if entity type uses overlay rendering (grids, waypoints, trays etc.)\n\t\t\t\t\t// Rack and SRM now have full 3D structures, so overlay grid lines are redundant.\n\t\t\t\t\t// Keep SCS (waypoints/trays) and conveyor overlays since they convey state info.\n\t\t\t\t\tconst hasOverlay =\n\t\t\t\t\t\tkind === \"scs\" ||\n\t\t\t\t\t\tkind === \"conveyor\";\n\n\t\t\t\t\t// SCS entities don't draw an entity box in Java (BinSCS2.onDraw):\n\t\t\t\t\t// only waypoints, head, and trays.\n\t\t\t\t\t// Rack and SRM now have full 3D structures, so they're always visible\n\t\t\t\t\t// (transparent ones use lower alpha material instead of hiding).\n\t\t\t\t\t// Use isVisible=false so the box mesh is skipped during\n\t\t\t\t\t// rendering while overlay child meshes (which have their\n\t\t\t\t\t// own isVisible=true) still render. (Note: visibility=0\n\t\t\t\t\t// would propagate to children — see Babylon.js docs.)\n\t\t\t\t\tif (kind === \"scs\") {\n\t\t\t\t\t\tmesh.isVisible = false;\n\t\t\t\t\t} else if (isTransparent && !wireframeMode && !hasOverlay && kind !== \"rack\" && kind !== \"srm\") {\n\t\t\t\t\t\tmesh.visibility = 0;\n\t\t\t\t\t\t// Still apply highlight if selected.\n\t\t\t\t\t\tconst isSelected3 = selectedIdRef.current === id;\n\t\t\t\t\t\tif (isSelected3 && !hl.hasMesh(mesh as any)) {\n\t\t\t\t\t\t\thl.addMesh(mesh as any, Color3.Yellow());\n\t\t\t\t\t\t} else if (!isSelected3 && hl.hasMesh(mesh as any)) {\n\t\t\t\t\t\t\thl.removeMesh(mesh as any);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmesh.visibility = 1;\n\t\t\t\t\t\t// For transparent rack/SRM 3D structures, use lower alpha\n\t\t\t\t\t\tconst isTransparentRackSrm = (kind === \"rack\" || kind === \"srm\") && isTransparent;\n\t\t\t\t\t\tconst matAlpha = isTransparentRackSrm ? 0.4 : (isTransparent ? 0.15 : 1);\n\t\t\t\t\t\t// Per-entity-type PBR parameters for realistic material look\n\t\t\t\t\t\tconst metallicRoughness = getKindMaterialParams(kind);\n\t\t\t\t\t\tconst matKey = `${color.toHexString()}_${matAlpha}_${wireframeMode ? \"w\" : \"s\"}_${metallicRoughness.metallic}_${metallicRoughness.roughness}`;\n\t\t\t\t\t\tconst mat = getMaterial(\n\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\tmatKey,\n\t\t\t\t\t\t\tcolor,\n\t\t\t\t\t\t\tmatAlpha,\n\t\t\t\t\t\t\twireframeMode,\n\t\t\t\t\t\t\tmetallicRoughness.metallic,\n\t\t\t\t\t\t\tmetallicRoughness.roughness,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tmesh.material = mat;\n\n\t\t\t\t\t\tconst isSelected = selectedIdRef.current === id;\n\t\t\t\t\t\tif (isSelected) {\n\t\t\t\t\t\t\tif (!hl.hasMesh(mesh as any)) {\n\t\t\t\t\t\t\t\thl.addMesh(mesh as any, Color3.Yellow());\n\t\t\t\t\t\t\t\tmesh.getChildMeshes().forEach((child) => {\n\t\t\t\t\t\t\t\t\thl.addMesh(child as any, Color3.Yellow());\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (hl.hasMesh(mesh as any)) {\n\t\t\t\t\t\t\t\thl.removeMesh(mesh as any);\n\t\t\t\t\t\t\t\tmesh.getChildMeshes().forEach((child) => {\n\t\t\t\t\t\t\t\t\thl.removeMesh(child as any);\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (entity.items) {\n\t\t\t\t\t\tfor (const item of entity.items) {\n\t\t\t\t\t\t\tconst itemKey = `${id}_item_${item.id}`;\n\t\t\t\t\t\t\tlet itemMesh = itemMeshes.get(itemKey);\n\t\t\t\t\t\t\tconst isx = Math.max(\n\t\t\t\t\t\t\t\tNumber.isFinite(item.sizex) ? (item.sizex ?? 0.5) : 0.5,\n\t\t\t\t\t\t\t\t0.05,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst isy = Math.max(\n\t\t\t\t\t\t\t\tNumber.isFinite(item.sizey) ? (item.sizey ?? 0.5) : 0.5,\n\t\t\t\t\t\t\t\t0.05,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst isz = Number.isFinite(item.sizez) ? Math.max(item.sizez ?? 0.5, 0.05) : 0.5;\n\n\t\t\t\t\t\t\t// Determine item type based on dimensions and context\n\t\t\t\t\t\t\t// Large items (height > 0.8) → LU (box on pallet)\n\t\t\t\t\t\t\t// Medium items (height > 0.3, width > 0.6) → cardboard box\n\t\t\t\t\t\t\t// Tall thin items → plastic tote\n\t\t\t\t\t\t\t// Small flat items → simple box\n\t\t\t\t\t\t\tconst isLargeLU = isz > 0.8 && isx > 0.6 && isy > 0.6;\n\t\t\t\t\t\t\tconst isCardboard = isz > 0.25 && !isLargeLU;\n\t\t\t\t\t\t\tconst isTote = isz > isx * 1.3 && isz > isy * 1.3;\n\n\t\t\t\t\t\t\tconst needsRebuild = !itemMesh || itemMesh.isDisposed();\n\t\t\t\t\t\t\tif (needsRebuild) {\n\t\t\t\t\t\t\t\tif (itemMesh && !itemMesh.isDisposed()) {\n\t\t\t\t\t\t\t\t\titemMesh.dispose();\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (isLargeLU) {\n\t\t\t\t\t\t\t\t\titemMesh = buildLoadUnitMesh(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`,\n\t\t\t\t\t\t\t\t\t\tisx,\n\t\t\t\t\t\t\t\t\t\tisy,\n\t\t\t\t\t\t\t\t\t\tisz,\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else if (isCardboard) {\n\t\t\t\t\t\t\t\t\titemMesh = buildCardboardBoxMesh(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`,\n\t\t\t\t\t\t\t\t\t\tisx,\n\t\t\t\t\t\t\t\t\t\tisy,\n\t\t\t\t\t\t\t\t\t\tisz,\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else if (isTote) {\n\t\t\t\t\t\t\t\t\titemMesh = buildToteMesh(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`,\n\t\t\t\t\t\t\t\t\t\tisx,\n\t\t\t\t\t\t\t\t\t\tisy,\n\t\t\t\t\t\t\t\t\t\tisz,\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Small items: simple box with item color\n\t\t\t\t\t\t\t\t\titemMesh = MeshBuilder.CreateBox(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`,\n\t\t\t\t\t\t\t\t\t\t{ width: isx, height: isz, depth: isy },\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\titemMesh.metadata = { entityId: id };\n\t\t\t\t\t\t\t\titemMeshes.set(itemKey, itemMesh);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!itemMesh) continue;\n\n\t\t\t\t\t\t\t// Java Item.getGlobalPolygon:\n\t\t\t\t\t\t// 1. Get local polygon (item pos + rotation)\n\t\t\t\t\t\t// 2. Rotate by entity rotation: cosR*localX - sinR*localY\n\t\t\t\t\t\t// 3. Add entity position\n\t\t\t\t\t\t// The server computes globalInterpolatedPosx/y/z which includes all transforms.\n\t\t\t\t\t\tconst entityRotRad = (entity.rotation ?? 0) * Math.PI / 180;\n\t\t\t\t\t\tconst cosR = Math.cos(entityRotRad);\n\t\t\t\t\t\tconst sinR = Math.sin(entityRotRad);\n\n\t\t\t\t\t\tlet ipx: number;\n\t\t\t\t\t\tlet ipy: number;\n\t\t\t\t\t\tlet ipz: number;\n\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\titem.moving &&\n\t\t\t\t\t\t\titem.moveStartTime != null &&\n\t\t\t\t\t\t\titem.moveStopTime != null &&\n\t\t\t\t\t\t\titem.moveStopTime > item.moveStartTime\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tconst wallElapsed = simTimeWallRef.current\n\t\t\t\t\t\t\t\t? performance.now() - simTimeWallRef.current\n\t\t\t\t\t\t\t\t: 0;\n\t\t\t\t\t\t\tconst effSimTime = simTimeRef.current + wallElapsed;\n\t\t\t\t\t\t\tconst startT = item.moveStartTime;\n\t\t\t\t\t\t\tconst stopT = item.moveStopTime;\n\t\t\t\t\t\t\tconst f = Math.min(\n\t\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\t\tMath.max(0, (effSimTime - startT) / (stopT - startT)),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst sx = item.moveStartx ?? item.posx ?? 0;\n\t\t\t\t\t\t\tconst sy = item.moveStarty ?? item.posy ?? 0;\n\t\t\t\t\t\t\tconst sz = item.moveStartz ?? item.posz ?? 0;\n\t\t\t\t\t\t\tconst ex = item.moveStopx ?? item.posx ?? 0;\n\t\t\t\t\t\t\tconst ey = item.moveStopy ?? item.posy ?? 0;\n\t\t\t\t\t\t\tconst ez = item.moveStopz ?? item.posz ?? 0;\n\t\t\t\t\t\t\t// Interpolate local position, then rotate by entity rotation (matching Java getGlobalPolygon)\n\t\t\t\t\t\t\tconst localX = sx + (ex - sx) * f;\n\t\t\t\t\t\t\tconst localY = sy + (ey - sy) * f;\n\t\t\t\t\t\t\tconst localZ = sz + (ez - sz) * f;\n\t\t\t\t\t\t\tipx = px + cosR * localX - sinR * localY;\n\t\t\t\t\t\t\tipy = py + sinR * localX + cosR * localY;\n\t\t\t\t\t\t\t// For tall structures (rack/SRM): base is at pz, items stack from ground up\n\t\t\t\t\t\t\t// For other entities: center at height/2 + pz\n\t\t\t\t\t\t\tconst baseZ = (kind === \"rack\" || kind === \"srm\") ? (pz ?? 0) : (pz ?? 0) + height / 2;\n\t\t\t\t\t\t\tipz = baseZ + isz / 2 + localZ;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Use server-computed globalInterpolatedPosx/y when available (includes entity rotation)\n\t\t\t\t\t\t\tif (Number.isFinite(item.globalInterpolatedPosx) && Number.isFinite(item.globalInterpolatedPosy)) {\n\t\t\t\t\t\t\t\tipx = item.globalInterpolatedPosx!;\n\t\t\t\t\t\t\t\tipy = item.globalInterpolatedPosy!;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Fallback: entity pos + rotate(item local pos) — matching Java getGlobalPolygon\n\t\t\t\t\t\t\t\tconst lx = Number.isFinite(item.posx) ? item.posx : 0;\n\t\t\t\t\t\t\t\tconst ly = Number.isFinite(item.posy) ? item.posy : 0;\n\t\t\t\t\t\t\t\tipx = px + cosR * lx - sinR * ly;\n\t\t\t\t\t\t\t\tipy = py + sinR * lx + cosR * ly;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// For tall structures (rack/SRM): base is at pz, items stack from ground up\n\t\t\t\t\t\t\tconst baseZ = (kind === \"rack\" || kind === \"srm\") ? (pz ?? 0) : (pz ?? 0) + height / 2;\n\t\t\t\t\t\t\tipz = baseZ + isz / 2 + (Number.isFinite(item.posz) ? item.posz : 0);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\titemMesh.position.set(ipx, ipz, ipy);\n\n\t\t\t\t\t\t\tif (item.moving) {\n\t\t\t\t\t\t\t\tconst pulse =\n\t\t\t\t\t\t\t\t\t1 +\n\t\t\t\t\t\t\t\t\t0.15 *\n\t\t\t\t\t\t\t\t\t\tMath.sin(now * 0.01 + (item.id.charCodeAt(0) || 0) * 0.3);\n\t\t\t\t\t\t\t\titemMesh.scaling.setAll(pulse);\n\t\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\t\titemMesh.scaling.x !== 1 ||\n\t\t\t\t\t\t\t\titemMesh.scaling.y !== 1 ||\n\t\t\t\t\t\t\t\titemMesh.scaling.z !== 1\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\titemMesh.scaling.setAll(1);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Only apply material/edges to simple box items.\n\t\t\t\t\t\t\t// Realistic models (cardboard, pallet, tote, LU) have their own materials.\n\t\t\t\t\t\t\tconst isSimpleBox = itemMesh.getClassName() === \"Mesh\" && !itemMesh.getChildMeshes().length;\n\t\t\t\t\t\t\tif (isSimpleBox) {\n\t\t\t\t\t\t\t\t// Java Item.checkInitColors: backGroundColor defaults to core.getItemFillColor() = gray #C0C0C0\n\t\t\t\t\t\t\t\t// 3D material uses backGroundColor as fill\n\t\t\t\t\t\t\t\tconst itemFillHex = item.backGroundColor || sc?.itemFillColor || ITEM_FILL_COLOR;\n\t\t\t\t\t\t\t\tconst ic = hexToColor3(itemFillHex, new Color3(192 / 255, 192 / 255, 192 / 255));\n\t\t\t\t\t\t\t\tconst imKey = `item_${ic.toHexString()}`;\n\t\t\t\t\t\t\t\titemMesh.material = getMaterial(scene, imKey, ic, 1, false);\n\t\t\t\t\t\t\t\t// Java Item.onDraw uses foreGroundColor for drawPolygon outline.\n\t\t\t\t\t\t\t\t// In 3D we approximate with box edges rendering.\n\t\t\t\t\t\t\t\tconst fgHex = item.foreGroundColor || sc?.itemLineColor || ITEM_LINE_COLOR;\n\t\t\t\t\t\t\t\tconst fgColor = hexToColor3(fgHex, new Color3(0, 0, 0));\n\t\t\t\t\t\t\t\titemMesh.enableEdgesRendering();\n\t\t\t\t\t\t\t\titemMesh.edgesWidth = 1;\n\t\t\t\t\t\t\t\titemMesh.edgesColor = fgColor.toColor4(1);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Overlay rendering\n\t\t\t\t\tconst needOverlay =\n\t\t\t\t\t\tkind === \"conveyor\" ||\n\t\t\t\t\t\tkind === \"rack\" ||\n\t\t\t\t\t\tkind === \"srm\" ||\n\t\t\t\t\t\tkind === \"scs\";\n\t\t\t\t\tconst entityProps = entity.properties as\n\t\t\t\t\t\t| Record<string, unknown>\n\t\t\t\t\t\t| undefined;\n\t\t\t\t\tconst connColor3 = hexToColor3(\n\t\t\t\t\t\tsc.connectionColor,\n\t\t\t\t\t\tnew Color3(128 / 255, 128 / 255, 128 / 255), // Java connectionColor = (128,128,128)\n\t\t\t\t\t);\n\t\t\t\t\tconst busyColor3 = hexToColor3(\n\t\t\t\t\t\tsc.entityBusyColor,\n\t\t\t\t\t\tnew Color3(0, 100 / 255, 1), // Java entityBusyColor = (0,100,255)\n\t\t\t\t\t);\n\n\t\t\t\t\tlet overlay = overlayMeshesRef.current.get(id);\n\t\t\t\t\tif (needOverlay) {\n\t\t\t\t\t\tif (!overlay || overlay.isDisposed() || overlay.parent !== mesh) {\n\t\t\t\t\t\t\tif (overlay && !overlay.isDisposed()) overlay.dispose();\n\t\t\t\t\t\t\toverlay = new Mesh(`overlay_${id}`, scene);\n\t\t\t\t\t\t\toverlay.parent = mesh;\n\t\t\t\t\t\t\toverlayMeshesRef.current.set(id, overlay);\n\t\t\t\t\t\t\toverlayHashRef.current.delete(id);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Ensure overlay is visible even when parent mesh is hidden (e.g. SCS)\n\t\t\t\t\t\toverlay.isVisible = true;\n\t\t\t\t\t\t// Make overlay children pickable so entities can be selected\n\t\t\t\t\t\t// even when the main mesh is hidden (SCS, transparent rack/SRM)\n\t\t\t\t\t\toverlay.metadata = { entityId: id };\n\t\t\t\t\t\toverlay.isPickable = true;\n\n\t\t\t\t\t\tconst wpPos = entity.waypointPositions;\n\t\t\t\t\t\tconst wpHash = wpPos\n\t\t\t\t\t\t\t? wpPos.map((w) => `${w.x},${w.y}`).join(\";\")\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst trays = entity.trays;\n\t\t\t\t\t\tconst trayHash = trays\n\t\t\t\t\t\t\t? trays.map((t) => `${t.trayx}:${t.isNext ? 1 : 0}`).join(\";\")\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\tconst overlayKey = `${kind}_${entity.transparent}_${entity.entityType}_${entity.foregroundColor}_${entity.backgroundColor}_${entityProps?.showSpace}_${JSON.stringify(entityProps?.coordinates)}_${entityProps?.orientation}_${entityProps?.invertx}_${entityProps?.invertz}_${wpHash}_${entity.traySizex}_${entity.traySizey}_${entity.traySizez}_${entity.trayCountx}_${trayHash}_${entityProps?.placesx}_${entityProps?.placesz}_${entity.posz ?? 0}`;\n\t\t\t\t\t\tconst prevHash = overlayHashRef.current.get(id);\n\n\t\t\t\t\t\tif (prevHash === overlayKey) {\n\t\t\t\t\t\t\t// overlay unchanged, no rebuild needed\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\toverlayHashRef.current.set(id, overlayKey);\n\t\t\t\t\t\t\toverlay.getChildMeshes().forEach((c) => c.dispose());\n\n\t\t\t\t\t\t\t// Overlay Y offset: flat box height = min(sz, 0.1), so top\n\t\t\t\t\t\t\t// face is at Y=0.05. All overlay lines must be above this.\n\t\t\t\t\t\t\tconst oY = 0.06;\n\t\t\t\t\t\t\tconst oYm = oY - 0.005; // slightly below outline for grid lines\n\n\t\t\t\t\t\t\tif (kind === \"conveyor\") {\n\t\t\t\t\t\t\t} else if (kind === \"rack\") {\n\t\t\t\t\t\t\t\tconst showSpace = entityProps?.showSpace as\n\t\t\t\t\t\t\t\t\t| string\n\t\t\t\t\t\t\t\t\t| null\n\t\t\t\t\t\t\t\t\t| undefined;\n\t\t\t\t\t\t\t\tconst coords = entityProps?.coordinates as\n\t\t\t\t\t\t\t\t\t| Array<{\n\t\t\t\t\t\t\t\t\t\t\tfirstx?: number;\n\t\t\t\t\t\t\t\t\t\t\tlastx?: number;\n\t\t\t\t\t\t\t\t\t\t\tfirstDepth?: number;\n\t\t\t\t\t\t\t\t\t\t\tlastDepth?: number;\n\t\t\t\t\t\t\t\t\t\t\tinvertx?: boolean;\n\t\t\t\t\t\t\t\t\t\t\tinverty?: boolean;\n\t\t\t\t\t\t\t\t\t\t\tinvertDepth?: boolean;\n\t\t\t\t\t\t\t\t\t  }>\n\t\t\t\t\t\t\t\t\t| undefined;\n\n\t\t\t\t\t\t\t\t// Java drawRackBounds: outer outline always drawn\n\t\t\t\t\t\t\t\t// (getForegroundColor = entityLineColor = BLACK by default).\n\t\t\t\t\t\t\t\tconst outlineColor = hexToColor3(\n\t\t\t\t\t\t\t\t\tentity.foregroundColor,\n\t\t\t\t\t\t\t\t\tC.dark, // entityLineColor default = BLACK in Java\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tconst outlineLines: Vector3[][] = [\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\tconst outlineSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t`rack_outline_${id}`,\n\t\t\t\t\t\t\t\t\t{ lines: outlineLines },\n\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\toutlineSys.color = outlineColor;\n\t\t\t\t\t\t\t\toutlineSys.parent = overlay;\n\n\t\t\t\t\t\t\t\t// Grid lines (Java onPlacesDraw → connectionColor)\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\tshowSpace &&\n\t\t\t\t\t\t\t\t\tshowSpace !== \"false\" &&\n\t\t\t\t\t\t\t\t\tshowSpace.trim() !== \"\" &&\n\t\t\t\t\t\t\t\t\tcoords?.[0]\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\tconst c0 = coords[0];\n\t\t\t\t\t\t\t\t\tconst firstx = c0.firstx ?? 1;\n\t\t\t\t\t\t\t\t\tconst lastx = c0.lastx ?? 1;\n\t\t\t\t\t\t\t\t\tconst firstDepth = c0.firstDepth ?? 1;\n\t\t\t\t\t\t\t\t\tconst lastDepth = c0.lastDepth ?? 1;\n\t\t\t\t\t\t\t\t\tconst orientation =\n\t\t\t\t\t\t\t\t\t\t(entityProps?.orientation as string) || \"x\";\n\t\t\t\t\t\t\t\t\tconst isInvertx = c0.invertx ?? false;\n\t\t\t\t\t\t\t\t\tconst isInvertDepth = c0.invertDepth ?? false;\n\n\t\t\t\t\t\t\t\t\tconst gridLines: Vector3[][] = [];\n\t\t\t\t\t\t\t\t\tconst { xLines, zLines } = computeRackGridLines({\n\t\t\t\t\t\t\t\t\t\tfirstx,\n\t\t\t\t\t\t\t\t\t\tlastx,\n\t\t\t\t\t\t\t\t\t\tfirstDepth,\n\t\t\t\t\t\t\t\t\t\tlastDepth,\n\t\t\t\t\t\t\t\t\t\tsizex: sx,\n\t\t\t\t\t\t\t\t\t\tsizey: sy,\n\t\t\t\t\t\t\t\t\t\torientation,\n\t\t\t\t\t\t\t\t\t\tinvertx: isInvertx,\n\t\t\t\t\t\t\t\t\t\tinvertDepth: isInvertDepth,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tfor (const offset of xLines) {\n\t\t\t\t\t\t\t\t\t\tgridLines.push([\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(offset, oYm, -sy / 2),\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(offset, oYm, sy / 2),\n\t\t\t\t\t\t\t\t\t\t]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tfor (const offset of zLines) {\n\t\t\t\t\t\t\t\t\t\tgridLines.push([\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oYm, offset),\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oYm, offset),\n\t\t\t\t\t\t\t\t\t\t]);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif (gridLines.length > 0) {\n\t\t\t\t\t\t\t\t\t\tconst gridLineSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t\t\t`rack_grid_${id}`,\n\t\t\t\t\t\t\t\t\t\t\t{ lines: gridLines },\n\t\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tgridLineSys.color = connColor3;\n\t\t\t\t\t\t\t\t\t\tgridLineSys.parent = overlay;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (kind === \"scs\") {\n\t\t\t\t\t\t\t\t// Java BinSCS2: transparent defaults to false (Entity base class)\n\t\t\t\t\t\t\t\tconst scsIsTransparent = entity.transparent ?? false;\n\t\t\t\t\t\t\t\t// Waypoint connection lines (closed loop, matching Java onDraw)\n\t\t\t\t\t\t\t\tconst wpPositions = entity.waypointPositions;\n\t\t\t\t\t\t\t\tconst trayCountx = Math.min(\n\t\t\t\t\t\t\t\t\tentity.trayCountx ?? wpPositions?.length ?? 0,\n\t\t\t\t\t\t\t\t\twpPositions?.length ?? 0,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tif (wpPositions && wpPositions.length > 1) {\n\t\t\t\t\t\t\t\t\tconst wpLines: Vector3[][] = [];\n\t\t\t\t\t\t\t\t\tfor (let i = 0; i < trayCountx; i++) {\n\t\t\t\t\t\t\t\t\t\tconst j = (i + 1) % trayCountx;\n\t\t\t\t\t\t\t\t\t\tconst wp1 = wpPositions[i];\n\t\t\t\t\t\t\t\t\t\tconst wp2 = wpPositions[j];\n\t\t\t\t\t\t\t\t\t\twpLines.push([\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(\n\t\t\t\t\t\t\t\t\t\t\t\twp1.x - entity.posx,\n\t\t\t\t\t\t\t\t\t\t\t\toY + 0.04,\n\t\t\t\t\t\t\t\t\t\t\t\twp1.y - entity.posy,\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(\n\t\t\t\t\t\t\t\t\t\t\t\twp2.x - entity.posx,\n\t\t\t\t\t\t\t\t\t\t\t\toY + 0.04,\n\t\t\t\t\t\t\t\t\t\t\t\twp2.y - entity.posy,\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tconst wpLineSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t\t`scs_wp_${id}`,\n\t\t\t\t\t\t\t\t\t\t{ lines: wpLines },\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\twpLineSys.color = connColor3;\n\t\t\t\t\t\t\t\t\twpLineSys.parent = overlay;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Waypoint dots - DISABLED for performance debugging\n\t\t\t\t\t\t\t\t// Will re-enable with proper optimization once root cause is found\n\t\t\t\t\t\t\t\t// if (wpPositions && wpPositions.length > 0) {\n\t\t\t\t\t\t\t\t//   ... ThinInstance code ...\n\t\t\t\t\t\t\t\t// }\n\t\t\t\t\t\t\t\tconsole.debug(\"SKIP_WAYPOINT_DOTS: \", id, wpPositions?.length ?? 0);\n\n\t\t\t\t\t\t\t\t// Head/station (Java onHeadDraw: slightly larger than tray,\n\t\t\t\t\t\t\t\t// filled + outlined with connectionColor)\n\t\t\t\t\t\t\t\tconst traySX = entity.traySizex ?? 0.5;\n\t\t\t\t\t\t\t\tconst traySY = entity.traySizey ?? 0.5;\n\t\t\t\t\t\t\t\tconst traySZ = entity.traySizez ?? 0.1;\n\t\t\t\t\t\t\t\tif (wpPositions && wpPositions.length > 0) {\n\t\t\t\t\t\t\t\t\tconst wp0 = wpPositions[0];\n\t\t\t\t\t\t\t\t\tconst headBox = MeshBuilder.CreateBox(\n\t\t\t\t\t\t\t\t\t\t`scs_head_${id}`,\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\twidth: traySX + 0.05,\n\t\t\t\t\t\t\t\t\t\t\theight: traySZ + 0.02,\n\t\t\t\t\t\t\t\t\t\t\tdepth: traySY + 0.05,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\theadBox.position = new Vector3(\n\t\t\t\t\t\t\t\t\t\twp0.x - entity.posx,\n\t\t\t\t\t\t\t\t\t\toY + traySZ / 2,\n\t\t\t\t\t\t\t\t\t\twp0.y - entity.posy,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\theadBox.material = createQuickMat(scene, connColor3);\n\t\t\t\t\t\t\t\t\theadBox.parent = overlay;\n\t\t\t\t\t\t\t\t\theadBox.enableEdgesRendering();\n\t\t\t\t\t\t\t\t\theadBox.edgesWidth = 1;\n\t\t\t\t\t\t\t\t\theadBox.edgesColor = connColor3.toColor4(1);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Tray boxes with InstancedMesh (shared geometry per entity)\n\t\t\t\t\t\t\t\t// All trays of the SAME entity have the same size → share 1 mesh\n\t\t\t\t\t\t\t\tconst trayBgColor = hexToColor3(\n\t\t\t\t\t\t\t\t\tentity.backgroundColor,\n\t\t\t\t\t\t\t\t\tC.white,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tconst trayFgColor = hexToColor3(entity.foregroundColor, C.dark);\n\t\t\t\t\t\t\t\tconst trays = entity.trays;\n\t\t\t\t\t\t\t\tif (trays) {\n\t\t\t\t\t\t\t\t\t// Create ONE template tray geometry for this entity\n\t\t\t\t\t\t\t\t\tconst trayTemplate = MeshBuilder.CreateBox(\n\t\t\t\t\t\t\t\t\t\t`scs_tray_tpl_${id}`,\n\t\t\t\t\t\t\t\t\t\t{ width: traySX, height: traySZ, depth: traySY },\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t// Set material on template (instances inherit it)\n\t\t\t\t\t\t\t\t\tif (!scsIsTransparent) {\n\t\t\t\t\t\t\t\t\t\ttrayTemplate.material = createQuickMat(scene, trayBgColor);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\ttrayTemplate.setEnabled(false); // hide template\n\n\t\t\t\t\t\t\t\t\tfor (const tray of trays) {\n\t\t\t\t\t\t\t\t\t\tconst tx = tray.interpolatedPosx - entity.posx;\n\t\t\t\t\t\t\t\t\t\tconst tz = tray.interpolatedPosy - entity.posy;\n\t\t\t\t\t\t\t\t\t\tconst ty =\n\t\t\t\t\t\t\t\t\t\t\toY +\n\t\t\t\t\t\t\t\t\t\t\t(tray.interpolatedPosz - (entity.posz ?? 0)) +\n\t\t\t\t\t\t\t\t\t\t\ttraySZ / 2;\n\n\t\t\t\t\t\t\t\t\t\tif (!scsIsTransparent) {\n\t\t\t\t\t\t\t\t\t\t\tconst inst = trayTemplate.createInstance(\n\t\t\t\t\t\t\t\t\t\t\t\t`scs_tray_${id}_${tray.trayx}`,\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\tinst.position = new Vector3(tx, ty, tz);\n\t\t\t\t\t\t\t\t\t\t\tinst.parent = overlay;\n\t\t\t\t\t\t\t\t\t\t\tinst.isPickable = true;\n\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\tconst hsx = traySX / 2;\n\t\t\t\t\t\t\t\t\t\t\tconst hsy = traySY / 2;\n\t\t\t\t\t\t\t\t\t\t\tconst trayOutline: Vector3[][] = [\n\t\t\t\t\t\t\t\t\t\t\t\t[new Vector3(tx - hsx, ty, tz - hsy), new Vector3(tx + hsx, ty, tz - hsy)],\n\t\t\t\t\t\t\t\t\t\t\t\t[new Vector3(tx + hsx, ty, tz - hsy), new Vector3(tx + hsx, ty, tz + hsy)],\n\t\t\t\t\t\t\t\t\t\t\t\t[new Vector3(tx + hsx, ty, tz + hsy), new Vector3(tx - hsx, ty, tz + hsy)],\n\t\t\t\t\t\t\t\t\t\t\t\t[new Vector3(tx - hsx, ty, tz + hsy), new Vector3(tx - hsx, ty, tz - hsy)],\n\t\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t\t\tconst trayLineSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t\t\t\t`scs_tray_${id}_${tray.trayx}`,\n\t\t\t\t\t\t\t\t\t\t\t\t{ lines: trayOutline },\n\t\t\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\ttrayLineSys.color = trayFgColor;\n\t\t\t\t\t\t\t\t\t\t\ttrayLineSys.parent = overlay;\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tif (tray.isNext) {\n\t\t\t\t\t\t\t\t\t\t\tconst l = traySX / 6;\n\t\t\t\t\t\t\t\t\t\t\tconst nextInd = MeshBuilder.CreateBox(\n\t\t\t\t\t\t\t\t\t\t\t\t`scs_next_${id}_${tray.trayx}`,\n\t\t\t\t\t\t\t\t\t\t\t\t{ width: l, height: 0.02, depth: l },\n\t\t\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\tnextInd.position = new Vector3(\n\t\t\t\t\t\t\t\t\t\t\t\ttx + traySX / 2 - l / 2 - 0.03,\n\t\t\t\t\t\t\t\t\t\t\t\tty + traySZ / 2 + 0.01,\n\t\t\t\t\t\t\t\t\t\t\t\ttz + traySY / 2 - l / 2 - 0.03,\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\tnextInd.material = createQuickMat(scene, busyColor3);\n\t\t\t\t\t\t\t\t\t\t\tnextInd.parent = overlay;\n\t\t\t\t\t\t\t\t\t\t\tnextInd.enableEdgesRendering();\n\t\t\t\t\t\t\t\t\t\t\tnextInd.edgesWidth = 1;\n\t\t\t\t\t\t\t\t\t\t\tnextInd.edgesColor = trayFgColor.toColor4(1);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (kind === \"srm\") {\n\t\t\t\t\t\t\t\tconst etype = (entity.entityType || \"\").toLowerCase();\n\t\t\t\t\t\t\t\tconst isSCS2Lift = etype === \"binscs2lift\";\n\t\t\t\t\t\t\t\tconst placesX = (entityProps?.placesx as number) ?? 0;\n\t\t\t\t\t\t\t\tconst placesZ = (entityProps?.placesz as number) ?? 0;\n\t\t\t\t\t\t\t\tconst srmOrientation =\n\t\t\t\t\t\t\t\t\t(entityProps?.orientation as string) || \"x\";\n\t\t\t\t\t\t\t\tconst isInvertx = (entityProps?.invertx as boolean) ?? false;\n\t\t\t\t\t\t\t\tconst isInvertz = (entityProps?.invertz as boolean) ?? false;\n\n\t\t\t\t\t\t\t\t// Java AbstractSRM2.onDraw: double outline (outer +\n\t\t\t\t\t\t\t\t// inner shrunk by 2px). In 3D we approximate with\n\t\t\t\t\t\t\t\t// two line-system rectangles at slightly different Y.\n\t\t\t\t\t\t\t\tconst srmOutlineColor = hexToColor3(\n\t\t\t\t\t\t\t\t\tentity.foregroundColor,\n\t\t\t\t\t\t\t\t\tC.dark,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tconst outerLines: Vector3[][] = [\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, sy / 2),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oY, -sy / 2),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\tconst outerSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t`srm_outline_${id}`,\n\t\t\t\t\t\t\t\t\t{ lines: outerLines },\n\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\touterSys.color = srmOutlineColor;\n\t\t\t\t\t\t\t\touterSys.parent = overlay;\n\n\t\t\t\t\t\t\t\t// Inner outline (shrunk ~0.06 in 3D units ≈ 2px at default zoom)\n\t\t\t\t\t\t\t\tconst inset = 0.06;\n\t\t\t\t\t\t\t\tconst innerLines: Vector3[][] = [\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2 + inset, oY + 0.005, -sy / 2 + inset),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2 - inset, oY + 0.005, -sy / 2 + inset),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2 - inset, oY + 0.005, -sy / 2 + inset),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2 - inset, oY + 0.005, sy / 2 - inset),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2 - inset, oY + 0.005, sy / 2 - inset),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2 + inset, oY + 0.005, sy / 2 - inset),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2 + inset, oY + 0.005, sy / 2 - inset),\n\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2 + inset, oY + 0.005, -sy / 2 + inset),\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\tconst innerSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t`srm_inner_${id}`,\n\t\t\t\t\t\t\t\t\t{ lines: innerLines },\n\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tinnerSys.color = srmOutlineColor;\n\t\t\t\t\t\t\t\tinnerSys.parent = overlay;\n\n\t\t\t\t\t\t\t\tif (isSCS2Lift) {\n\t\t\t\t\t\t\t\t\t// Java BinSCS2Lift.onPlacesDraw: single center line\n\t\t\t\t\t\t\t\t\tconst placeLines: Vector3[][] = [];\n\t\t\t\t\t\t\t\t\tconst orient = srmOrientation;\n\t\t\t\t\t\t\t\t\tif (orient === \"east\" || orient === \"west\") {\n\t\t\t\t\t\t\t\t\t\tplaceLines.push([\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(0, oYm, -sy / 2),\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(0, oYm, sy / 2),\n\t\t\t\t\t\t\t\t\t\t]);\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tplaceLines.push([\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oYm, 0),\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oYm, 0),\n\t\t\t\t\t\t\t\t\t\t]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif (placeLines.length > 0) {\n\t\t\t\t\t\t\t\t\t\tconst lineSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t\t\t`srm_place_${id}`,\n\t\t\t\t\t\t\t\t\t\t\t{ lines: placeLines },\n\t\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tlineSys.color = connColor3;\n\t\t\t\t\t\t\t\t\t\tlineSys.parent = overlay;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tconst placeLines: Vector3[][] = [];\n\t\t\t\t\t\t\t\t\tconst { xLines, zLines } = computeSrmGridLines({\n\t\t\t\t\t\t\t\t\t\tplacesX,\n\t\t\t\t\t\t\t\t\t\tplacesZ,\n\t\t\t\t\t\t\t\t\t\tsizex: sx,\n\t\t\t\t\t\t\t\t\t\tsizey: sy,\n\t\t\t\t\t\t\t\t\t\torientation: srmOrientation,\n\t\t\t\t\t\t\t\t\t\tinvertx: isInvertx,\n\t\t\t\t\t\t\t\t\t\tinvertz: isInvertz,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tfor (const offset of xLines) {\n\t\t\t\t\t\t\t\t\t\tplaceLines.push([\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(offset, oYm, -sy / 2),\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(offset, oYm, sy / 2),\n\t\t\t\t\t\t\t\t\t\t]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tfor (const offset of zLines) {\n\t\t\t\t\t\t\t\t\t\tplaceLines.push([\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(-sx / 2, oYm, offset),\n\t\t\t\t\t\t\t\t\t\t\tnew Vector3(sx / 2, oYm, offset),\n\t\t\t\t\t\t\t\t\t\t]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif (placeLines.length > 0) {\n\t\t\t\t\t\t\t\t\t\tconst gridLineSys = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\t\t\t\t\t`srm_place_${id}`,\n\t\t\t\t\t\t\t\t\t\t\t{ lines: placeLines },\n\t\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\tgridLineSys.color = connColor3;\n\t\t\t\t\t\t\t\t\t\tgridLineSys.parent = overlay;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Selection highlight for entities with hidden main mesh:\n\t\t\t\t\t\t// SCS, transparent rack, transparent SRM — the main mesh\n\t\t\t\t\t\t// is isVisible=false so highlight the overlay children.\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tkind === \"scs\" ||\n\t\t\t\t\t\t\t(kind === \"rack\" && isTransparent) ||\n\t\t\t\t\t\t\t(kind === \"srm\" && isTransparent)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tconst isSel = selectedIdRef.current === id;\n\t\t\t\t\t\t\tconst children = overlay.getChildMeshes();\n\t\t\t\t\t\t\tfor (const child of children) {\n\t\t\t\t\t\t\t\tif (isSel && !hl.hasMesh(child as any)) {\n\t\t\t\t\t\t\t\t\thl.addMesh(child as any, Color3.Yellow());\n\t\t\t\t\t\t\t\t} else if (!isSel && hl.hasMesh(child as any)) {\n\t\t\t\t\t\t\t\t\thl.removeMesh(child as any);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (overlay && !overlay.isDisposed()) {\n\t\t\t\t\t\t\toverlay.dispose();\n\t\t\t\t\t\t\toverlayMeshesRef.current.delete(id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Second pass: render items for ThinInstance entities\n\t\t\t\tif (thinManager && thinManager.handledIds.size > 0) {\n\t\t\t\t\tfor (const [id, entity] of entities) {\n\t\t\t\t\t\tif (!thinIds.has(id)) continue;\n\t\t\t\t\t\tif (!entity.items || entity.items.length === 0) continue;\n\t\t\t\t\t\tconst sx = Math.max(entity.sizex, 0.1);\n\t\t\t\t\t\tconst sy = Math.max(entity.sizey, 0.1);\n\t\t\t\t\t\tconst isz = Number.isFinite(entity.sizez) ? entity.sizez : 0.5;\n\t\t\t\t\t\tconst sz = Math.max(isz, 0.1);\n\t\t\t\t\t\tconst height = sz;\n\t\t\t\t\t\tconst px = entity.posx;\n\t\t\t\t\t\tconst py = entity.posy;\n\t\t\t\t\t\tconst pz = Number.isFinite(entity.posz) ? entity.posz : 0;\n\t\t\t\t\t\tconst kind = resolveShape3D(entity.entityType ?? \"\");\n\t\t\t\t\t\tconst isSRMKind = kind === \"srm\";\n\t\t\t\t\t\tconst entityProps = entity.properties as\n\t\t\t\t\t\t\t| Record<string, unknown>\n\t\t\t\t\t\t\t| undefined;\n\t\t\t\t\t\tfor (const item of entity.items) {\n\t\t\t\t\t\t\tconst itemKey = `${id}_item_${item.id}`;\n\t\t\t\t\t\t\tlet itemMesh = itemMeshes.get(itemKey);\n\t\t\t\t\t\t\tconst isx = Math.max(\n\t\t\t\t\t\t\t\tNumber.isFinite(item.sizex) ? (item.sizex ?? 0.5) : 0.5,\n\t\t\t\t\t\t\t\t0.05,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst isy = Math.max(\n\t\t\t\t\t\t\t\tNumber.isFinite(item.sizey) ? (item.sizey ?? 0.5) : 0.5,\n\t\t\t\t\t\t\t\t0.05,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst iisz = Number.isFinite(item.sizez) ? Math.max(item.sizez ?? 0.5, 0.05) : 0.5;\n\t\t\t\t\t\t\tconst isLargeLU = iisz > 0.8 && isx > 0.6 && isy > 0.6;\n\t\t\t\t\t\t\tconst isCardboard = iisz > 0.25 && !isLargeLU;\n\t\t\t\t\t\t\tconst isTote = iisz > isx * 1.3 && iisz > isy * 1.3;\n\t\t\t\t\t\t\tconst needsRebuild = !itemMesh || itemMesh.isDisposed();\n\t\t\t\t\t\t\tif (needsRebuild) {\n\t\t\t\t\t\t\t\tif (itemMesh && !itemMesh.isDisposed()) {\n\t\t\t\t\t\t\t\t\titemMesh.dispose();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (isLargeLU) {\n\t\t\t\t\t\t\t\t\titemMesh = buildLoadUnitMesh(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`, isx, isy, iisz, scene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else if (isCardboard) {\n\t\t\t\t\t\t\t\t\titemMesh = buildCardboardBoxMesh(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`, isx, isy, iisz, scene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else if (isTote) {\n\t\t\t\t\t\t\t\t\titemMesh = buildToteMesh(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`, isx, isy, iisz, scene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\titemMesh = MeshBuilder.CreateBox(\n\t\t\t\t\t\t\t\t\t\t`item_${itemKey}`,\n\t\t\t\t\t\t\t\t\t\t{ width: isx, height: iisz, depth: isy },\n\t\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\titemMesh.metadata = { entityId: id };\n\t\t\t\t\t\t\t\titemMeshes.set(itemKey, itemMesh);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!itemMesh) continue;\n\t\t\t\t\t\t\tconst entityRotRad = (entity.rotation ?? 0) * Math.PI / 180;\n\t\t\t\t\t\t\tconst cosR = Math.cos(entityRotRad);\n\t\t\t\t\t\t\tconst sinR = Math.sin(entityRotRad);\n\t\t\t\t\t\t\tlet ipx: number, ipy: number, ipz: number;\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\titem.moving &&\n\t\t\t\t\t\t\t\titem.moveStartTime != null &&\n\t\t\t\t\t\t\t\titem.moveStopTime != null &&\n\t\t\t\t\t\t\t\titem.moveStopTime > item.moveStartTime\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tconst wallElapsed = simTimeWallRef.current\n\t\t\t\t\t\t\t\t\t? performance.now() - simTimeWallRef.current\n\t\t\t\t\t\t\t\t\t: 0;\n\t\t\t\t\t\t\t\tconst effSimTime = simTimeRef.current + wallElapsed;\n\t\t\t\t\t\t\t\tconst startT = item.moveStartTime;\n\t\t\t\t\t\t\t\tconst stopT = item.moveStopTime;\n\t\t\t\t\t\t\t\tconst f = Math.min(\n\t\t\t\t\t\t\t\t\t1, Math.max(0, (effSimTime - startT) / (stopT - startT)),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tconst sx2 = item.moveStartx ?? item.posx ?? 0;\n\t\t\t\t\t\t\t\tconst sy2 = item.moveStarty ?? item.posy ?? 0;\n\t\t\t\t\t\t\t\tconst sz2 = item.moveStartz ?? item.posz ?? 0;\n\t\t\t\t\t\t\t\tconst ex = item.moveStopx ?? item.posx ?? 0;\n\t\t\t\t\t\t\t\tconst ey = item.moveStopy ?? item.posy ?? 0;\n\t\t\t\t\t\t\t\tconst ez = item.moveStopz ?? item.posz ?? 0;\n\t\t\t\t\t\t\t\tconst localX = sx2 + (ex - sx2) * f;\n\t\t\t\t\t\t\t\tconst localY = sy2 + (ey - sy2) * f;\n\t\t\t\t\t\t\t\tconst localZ = sz2 + (ez - sz2) * f;\n\t\t\t\t\t\t\t\tipx = px + cosR * localX - sinR * localY;\n\t\t\t\t\t\t\t\tipy = py + sinR * localX + cosR * localY;\n\t\t\t\t\t\t\t\tconst baseZ = (kind === \"rack\" || kind === \"srm\") ? (pz ?? 0) : (pz ?? 0) + height / 2;\n\t\t\t\t\t\t\t\tipz = baseZ + iisz / 2 + localZ;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (Number.isFinite(item.globalInterpolatedPosx) && Number.isFinite(item.globalInterpolatedPosy)) {\n\t\t\t\t\t\t\t\t\tipx = item.globalInterpolatedPosx!;\n\t\t\t\t\t\t\t\t\tipy = item.globalInterpolatedPosy!;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tconst lx = Number.isFinite(item.posx) ? item.posx : 0;\n\t\t\t\t\t\t\t\t\tconst ly = Number.isFinite(item.posy) ? item.posy : 0;\n\t\t\t\t\t\t\t\t\tipx = px + cosR * lx - sinR * ly;\n\t\t\t\t\t\t\t\t\tipy = py + sinR * lx + cosR * ly;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst baseZ = (kind === \"rack\" || kind === \"srm\") ? (pz ?? 0) : (pz ?? 0) + height / 2;\n\t\t\t\t\t\t\t\tipz = baseZ + iisz / 2 + (Number.isFinite(item.posz) ? item.posz : 0);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\titemMesh.position.set(ipx, ipz, ipy);\n\t\t\t\t\t\t\tif (item.moving) {\n\t\t\t\t\t\t\t\tconst pulse = 1 + 0.15 * Math.sin(now * 0.01 + (item.id.charCodeAt(0) || 0) * 0.3);\n\t\t\t\t\t\t\t\titemMesh.scaling.setAll(pulse);\n\t\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\t\titemMesh.scaling.x !== 1 ||\n\t\t\t\t\t\t\t\titemMesh.scaling.y !== 1 ||\n\t\t\t\t\t\t\t\titemMesh.scaling.z !== 1\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\titemMesh.scaling.setAll(1);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst isSimpleBox = itemMesh.getClassName() === \"Mesh\" && !itemMesh.getChildMeshes().length;\n\t\t\t\t\t\t\tif (isSimpleBox) {\n\t\t\t\t\t\t\t\tconst itemFillHex = item.backGroundColor || sc?.itemFillColor || ITEM_FILL_COLOR;\n\t\t\t\t\t\t\t\tconst ic = hexToColor3(itemFillHex, new Color3(192 / 255, 192 / 255, 192 / 255));\n\t\t\t\t\t\t\t\tconst imKey = `item_${ic.toHexString()}`;\n\t\t\t\t\t\t\t\titemMesh.material = getMaterial(scene, imKey, ic, 1, false);\n\t\t\t\t\t\t\t\tconst fgHex = item.foreGroundColor || sc?.itemLineColor || ITEM_LINE_COLOR;\n\t\t\t\t\t\t\t\tconst fgColor = hexToColor3(fgHex, new Color3(0, 0, 0));\n\t\t\t\t\t\t\t\titemMesh.enableEdgesRendering();\n\t\t\t\t\t\t\t\titemMesh.edgesWidth = 1;\n\t\t\t\t\t\t\t\titemMesh.edgesColor = fgColor.toColor4(1);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Update ThinInstance buffers (position + color for instanced entities)\n\t\t\t\tif (thinManager) {\n\t\t\t\t\tthinManager.update(entities, sc);\n\t\t\t\t}\n\n\t\t\t\tfor (const [id, mesh] of entityMeshes) {\n\t\t\t\t\tif (!currentIds.has(id)) {\n\t\t\t\t\t\thl.removeMesh(mesh as any);\n\t\t\t\t\t\tmesh.dispose();\n\t\t\t\t\t\tentityMeshes.delete(id);\n\t\t\t\t\t\tentityShapeKindsRef.current.delete(id);\n\t\t\t\t\t\tentitySizesRef.current.delete(id);\n\t\t\t\t\t\tprevPositionsRef.current.delete(id);\n\t\t\t\t\t\tconst ov = overlayMeshesRef.current.get(id);\n\t\t\t\t\t\tif (ov && !ov.isDisposed()) ov.dispose();\n\t\t\t\t\t\toverlayMeshesRef.current.delete(id);\n\t\t\t\t\t\toverlayHashRef.current.delete(id);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst activeEntityIds = new Set(entities.keys());\n\t\t\t\tfor (const key of prevPositionsRef.current.keys()) {\n\t\t\t\t\tif (!activeEntityIds.has(key)) {\n\t\t\t\t\t\tprevPositionsRef.current.delete(key);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst prevItemKeys = new Set(itemMeshes.keys());\n\t\t\t\tconst activeItemKeys = new Set<string>();\n\t\t\t\tfor (const [, entity] of entities) {\n\t\t\t\t\tif (entity.items) {\n\t\t\t\t\t\tfor (const item of entity.items) {\n\t\t\t\t\t\t\tactiveItemKeys.add(`${entity.id}_item_${item.id}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const key of prevItemKeys) {\n\t\t\t\t\tif (!activeItemKeys.has(key)) {\n\t\t\t\t\t\tconst m = itemMeshes.get(key);\n\t\t\t\t\t\tif (m) {\n\t\t\t\t\t\t\tm.dispose();\n\t\t\t\t\t\t\titemMeshes.delete(key);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (linkMeshRef.current && !linkMeshRef.current.isDisposed()) {\n\t\t\t\t\tlinkMeshRef.current.dispose();\n\t\t\t\t\tlinkMeshRef.current = null;\n\t\t\t\t}\n\n\t\t\t\tconst linkLines: Vector3[][] = [];\n\t\t\t\tconst currentLinkKeys = new Set<string>();\n\t\t\t\tlet arrowMat = scene.getMaterialByName(\n\t\t\t\t\t\"_arrowMat\",\n\t\t\t\t) as PBRMaterial | null;\n\t\t\t\tfor (const [id, entity] of entities) {\n\t\t\t\t\tif (!entity.linksOut) continue;\n\t\t\t\t\tfor (const link of entity.linksOut) {\n\t\t\t\t\t\tconst tgt = entities.get(link.targetId);\n\t\t\t\t\t\tif (!tgt) continue;\n\t\t\t\t\t\tconst linkKey = `${id}->${link.targetId}`;\n\t\t\t\t\t\tcurrentLinkKeys.add(linkKey);\n\t\t\t\t\t\tconst sz =\n\t\t\t\t\t\t\t(entity.posz ?? 0) + Math.max(entity.sizez ?? 0.5, 0.1) / 2;\n\t\t\t\t\t\tconst tz = (tgt.posz ?? 0) + Math.max(tgt.sizez ?? 0.5, 0.1) / 2;\n\t\t\t\t\t\tconst start = new Vector3(entity.posx, sz + 0.3, entity.posy);\n\t\t\t\t\t\tconst end = new Vector3(tgt.posx, tz + 0.3, tgt.posy);\n\t\t\t\t\t\tlinkLines.push([start, end]);\n\t\t\t\t\t\tconst dir = end.subtract(start);\n\t\t\t\t\t\tif (dir.length() > 0.001) {\n\t\t\t\t\t\t\tdir.normalize();\n\t\t\t\t\t\t\tif (!arrowMat) {\n\t\t\t\t\t\t\t\tarrowMat = new PBRMaterial(\"_arrowMat\", scene);\n\t\t\t\t\t\t\t\tarrowMat.albedoColor = new Color3(0.3, 0.5, 0.8);\n\t\t\t\t\t\t\t\tarrowMat.emissiveColor = new Color3(0.1, 0.2, 0.4);\n\t\t\t\t\t\t\t\tarrowMat.metallic = 0.2;\n\t\t\t\t\t\t\t\tarrowMat.roughness = 0.4;\n\t\t\t\t\t\t\t\tarrowMat.alpha = 0.8;\n\t\t\t\t\t\t\t\tarrowMat.freeze();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst coneLength = 0.35;\n\t\t\t\t\t\t\tconst conePos = end.subtract(dir.scale(coneLength * 0.5));\n\t\t\t\t\t\t\tlet arrowMesh = arrowCacheRef.current.get(linkKey);\n\t\t\t\t\t\t\tif (arrowMesh && !arrowMesh.isDisposed()) {\n\t\t\t\t\t\t\t\tarrowMesh.position.copyFrom(conePos);\n\t\t\t\t\t\t\t\tconst yAxis = Vector3.Up();\n\t\t\t\t\t\t\t\tconst dot = Vector3.Dot(yAxis, dir);\n\t\t\t\t\t\t\t\tif (Math.abs(dot) > 0.9999) {\n\t\t\t\t\t\t\t\t\tarrowMesh.rotationQuaternion = null;\n\t\t\t\t\t\t\t\t\tarrowMesh.rotation.x = dot < 0 ? Math.PI : 0;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tconst axis = Vector3.Cross(yAxis, dir).normalize();\n\t\t\t\t\t\t\t\t\tconst angle = Math.acos(dot);\n\t\t\t\t\t\t\t\t\tarrowMesh.rotationQuaternion = Quaternion.RotationAxis(\n\t\t\t\t\t\t\t\t\t\taxis,\n\t\t\t\t\t\t\t\t\t\tangle,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (arrowMesh && !arrowMesh.isDisposed()) arrowMesh.dispose();\n\t\t\t\t\t\t\t\tarrowMesh = createArrowCone(\n\t\t\t\t\t\t\t\t\tend.clone(),\n\t\t\t\t\t\t\t\t\tdir.clone(),\n\t\t\t\t\t\t\t\t\t`arrow_${linkKey}`,\n\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tarrowMesh.material = arrowMat;\n\t\t\t\t\t\t\t\tarrowMesh.isPickable = false;\n\t\t\t\t\t\t\t\tarrowCacheRef.current.set(linkKey, arrowMesh);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst staleArrowKeys: string[] = [];\n\t\t\t\tfor (const [key, mesh] of arrowCacheRef.current) {\n\t\t\t\t\tif (!currentLinkKeys.has(key)) {\n\t\t\t\t\t\tif (!mesh.isDisposed()) mesh.dispose();\n\t\t\t\t\t\tstaleArrowKeys.push(key);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const key of staleArrowKeys) {\n\t\t\t\t\tarrowCacheRef.current.delete(key);\n\t\t\t\t}\n\t\t\t\tif (linkLines.length > 0) {\n\t\t\t\t\tconst linkMesh = MeshBuilder.CreateLineSystem(\n\t\t\t\t\t\t\"__links__\",\n\t\t\t\t\t\t{ lines: linkLines },\n\t\t\t\t\t\tscene,\n\t\t\t\t\t);\n\t\t\t\t\tlet linkMat = scene.getMaterialByName(\n\t\t\t\t\t\t\"linkMat\",\n\t\t\t\t\t) as PBRMaterial | null;\n\t\t\t\t\tif (!linkMat) {\n\t\t\t\t\t\tlinkMat = new PBRMaterial(\"linkMat\", scene);\n\t\t\t\t\t\tlinkMat.albedoColor = new Color3(0.2, 0.4, 0.7);\n\t\t\t\t\t\tlinkMat.emissiveColor = new Color3(0.1, 0.2, 0.35);\n\t\t\t\t\t\tlinkMat.alpha = 0.65;\n\t\t\t\t\t\tlinkMat.freeze();\n\t\t\t\t\t}\n\t\t\t\t\tlinkMesh.material = linkMat;\n\t\t\t\t\tlinkMesh.isPickable = false;\n\t\t\t\t\tlinkMeshRef.current = linkMesh;\n\t\t\t\t}\n\n\t\t\t\tfor (const mesh of flowParticlesRef.current) {\n\t\t\t\t\tmesh.setEnabled(false);\n\t\t\t\t}\n\t\t\t\tif (storeState.simRunning && linkLines.length > 0) {\n\t\t\t\t\tlet flowMat = scene.getMaterialByName(\n\t\t\t\t\t\t\"_flowMat\",\n\t\t\t\t\t) as PBRMaterial | null;\n\t\t\t\t\tif (!flowMat) {\n\t\t\t\t\t\tflowMat = new PBRMaterial(\"_flowMat\", scene);\n\t\t\t\t\t\tflowMat.albedoColor = new Color3(0.0, 0.8, 1.0);\n\t\t\t\t\t\tflowMat.emissiveColor = new Color3(0.0, 0.4, 0.6);\n\t\t\t\t\t\tflowMat.alpha = 0.9;\n\t\t\t\t\t\tflowMat.freeze();\n\t\t\t\t\t}\n\t\t\t\t\tlet poolIdx = 0;\n\t\t\t\t\tfor (let li = 0; li < linkLines.length; li++) {\n\t\t\t\t\t\tconst [start, end] = linkLines[li];\n\t\t\t\t\t\tconst numParticles = 3;\n\t\t\t\t\t\tfor (let pi = 0; pi < numParticles; pi++) {\n\t\t\t\t\t\t\tconst t = (engine.frameId * 0.02 + pi / numParticles) % 1.0;\n\t\t\t\t\t\t\tconst pos = start.add(end.subtract(start).scale(t));\n\t\t\t\t\t\t\tlet p: Mesh;\n\t\t\t\t\t\t\tif (poolIdx < flowParticlesRef.current.length) {\n\t\t\t\t\t\t\t\tp = flowParticlesRef.current[poolIdx];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tp = MeshBuilder.CreateSphere(\n\t\t\t\t\t\t\t\t\t`_flow_${poolIdx}`,\n\t\t\t\t\t\t\t\t\t{ diameter: 0.12, segments: 4 },\n\t\t\t\t\t\t\t\t\tscene,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tp.material = flowMat;\n\t\t\t\t\t\t\t\tp.isPickable = false;\n\t\t\t\t\t\t\t\tflowParticlesRef.current.push(p);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tp.position = pos;\n\t\t\t\t\t\t\tp.setEnabled(true);\n\t\t\t\t\t\t\tpoolIdx++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tfor (let i = poolIdx; i < flowParticlesRef.current.length; i++) {\n\t\t\t\t\t\tflowParticlesRef.current[i].dispose();\n\t\t\t\t\t}\n\t\t\t\t\tflowParticlesRef.current.length = poolIdx;\n\t\t\t\t}\n\n\t\t\t\tscene.render();\n\n\t\t\t\t// ── Render Metrics Overlay (every 30 frames) ──\n\t\t\t\tconst metricsEl = metricsRef.current;\n\t\t\t\tif (metricsEl && engine.frameId % 30 === 0) {\n\t\t\t\t\tconst fps = engine.getFps();\n\t\t\t\t\tconst totalMeshes = scene.meshes.length;\n\t\t\t\t\tconst activeMeshes = scene.getActiveMeshes().length;\n\t\t\t\t\tconst drawCalls = sceneInstr.drawCallsCounter.current;\n\t\t\t\t\tconst frameTime = sceneInstr.frameTimeCounter.lastSecAverage.toFixed(1);\n\t\t\t\t\tconst renderTime = sceneInstr.renderTimeCounter.lastSecAverage.toFixed(1);\n\t\t\t\t\tconst entityCount = entities.size;\n\t\t\t\t\tconst labelCount = labelEl ? labelEl.querySelectorAll(\"span\").length : 0;\n\n\t\t\t\t\t// Entity + Mesh counts by type category (top 8 by mesh count)\n\t\t\t\t\t// ThinInstance entities count as 1 draw call per group, not per entity.\n\t\t\t\t\tconst typeEnt = new Map<string, number>();\n\t\t\t\t\tconst typeMesh = new Map<string, number>();\n\t\t\t\t\tconst thinCats = new Set<string>();\n\t\t\t\t\tfor (const [id, entity] of entities) {\n\t\t\t\t\t\tconst cat = categorizeEntityType(entity.entityType ?? \"?\");\n\t\t\t\t\t\ttypeEnt.set(cat, (typeEnt.get(cat) ?? 0) + 1);\n\t\t\t\t\t\tconst mesh = entityMeshes.get(id);\n\t\t\t\t\t\tif (mesh && !mesh.isDisposed()) {\n\t\t\t\t\t\t\tlet mc = 1;\n\t\t\t\t\t\t\tmc += mesh.getChildMeshes().filter(c => !c.isDisposed()).length;\n\t\t\t\t\t\t\ttypeMesh.set(cat, (typeMesh.get(cat) ?? 0) + mc);\n\t\t\t\t\t\t} else if (thinIds.has(id)) {\n\t\t\t\t\t\t\tthinCats.add(cat);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// ThinInstance categories: each counts as 1 mesh (the template)\n\t\t\t\t\tfor (const cat of thinCats) {\n\t\t\t\t\t\tif (!typeMesh.has(cat)) {\n\t\t\t\t\t\t\ttypeMesh.set(cat, 1);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst sortedTypes = [...typeMesh.entries()]\n\t\t\t\t\t\t.sort((a, b) => b[1] - a[1])\n\t\t\t\t\t\t.slice(0, 8);\n\t\t\t\t\tconst typeRows = sortedTypes\n\t\t\t\t\t\t.map(([cat, mc]) => {\n\t\t\t\t\t\t\tconst ec = typeEnt.get(cat) ?? 0;\n\t\t\t\t\t\t\treturn `<div><span style=\"color:#aac\">${mc}</span><span style=\"color:#666\">/${ec}</span> <span style=\"color:#888\">${cat}</span></div>`;\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.join(\"\");\n\n\t\t\t\t\tmetricsEl.innerHTML =\n\t\t\t\t\t\t`<div style=\"font-family:'Consolas',monospace;font-size:11px;line-height:1.6\">` +\n\t\t\t\t\t\t`<div><b style=\"color:${fps >= 50 ? '#4f8' : fps >= 30 ? '#fa0' : '#f44'}\">${Math.round(fps)}</b> <span style=\"color:#888\">FPS</span></div>` +\n\t\t\t\t\t\t`<div><span style=\"color:#aac\">${activeMeshes}</span><span style=\"color:#666\">/${totalMeshes}</span> <span style=\"color:#888\">Meshes</span></div>` +\n\t\t\t\t\t\t`<div><span style=\"color:#aac\">${drawCalls}</span> <span style=\"color:#888\">DrawCalls</span></div>` +\n\t\t\t\t\t\t`<div><span style=\"color:#aac\">${frameTime}</span>ms <span style=\"color:#888\">Frame</span> ` +\n\t\t\t\t\t\t`<span style=\"color:#aac\">${renderTime}</span>ms <span style=\"color:#888\">Render</span></div>` +\n\t\t\t\t\t\t`<div><span style=\"color:#aac\">${entityCount}</span> <span style=\"color:#888\">Entities</span> ` +\n\t\t\t\t\t\t`<span style=\"color:#aac\">${labelCount}</span> <span style=\"color:#888\">Labels</span></div>` +\n\t\t\t\t\t\t(typeRows ? `<div style=\"margin-top:4px;border-top:1px solid rgba(255,255,255,0.08);padding-top:3px\">${typeRows}</div>` : \"\") +\n\t\t\t\t\t\t`</div>`;\n\t\t\t\t}\n\n\t\t\t\t// ── Label LOD (Level of Detail) for entity id text ──\n\t\t\t\t// Near (<20m): full label, larger font\n\t\t\t\t// Mid  (20-50m): entity type only (Conv/Rack/Lift...), medium font\n\t\t\t\t// Far  (>50m): hidden (eliminates visual noise)\n\n\t\t\t\tif (labelEl && engine.frameId % 3 === 0) {\n\t\t\t\t\tconst viewport = camera.viewport;\n\t\t\t\t\tconst engineW = engine.getRenderWidth();\n\t\t\t\t\tconst engineH = engine.getRenderHeight();\n\t\t\t\t\tconst vp = viewport.toGlobal(engineW, engineH);\n\t\t\t\t\tconst camPos = camera.position;\n\t\t\t\t\tconst parts: string[] = [];\n\n\t\t\t\t\tfor (const [eid, ent] of entities) {\n\t\t\t\t\t\tconst wpos = new Vector3(\n\t\t\t\t\t\t\tent.posx,\n\t\t\t\t\t\t\t(ent.posz ?? 0) + Math.max(ent.sizez ?? 0.5, 0.1) + 0.5,\n\t\t\t\t\t\t\tent.posy,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst dist = Math.max(1, Vector3.Distance(wpos, camPos));\n\n\t\t\t\t\t\t// LOD: skip labels beyond 50m\n\t\t\t\t\t\tif (dist > 50) continue;\n\n\t\t\t\t\t\tconst screen = Vector3.Project(\n\t\t\t\t\t\t\twpos,\n\t\t\t\t\t\t\tMatrix.IdentityReadOnly,\n\t\t\t\t\t\t\tscene.getTransformMatrix(),\n\t\t\t\t\t\t\tvp,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (screen.z < 0 || screen.z > 1) continue;\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tscreen.x < -100 ||\n\t\t\t\t\t\t\tscreen.x > engineW + 100 ||\n\t\t\t\t\t\t\tscreen.y < -100 ||\n\t\t\t\t\t\t\tscreen.y > engineH + 100\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t\t// LOD: font size & label text based on distance\n\t\t\t\t\t\tconst isSel = eid === selectedIdRef.current;\n\t\t\t\t\t\tlet fontSize: number;\n\t\t\t\t\t\tlet label: string;\n\t\t\t\t\t\tif (dist < 15) {\n\t\t\t\t\t\t\t// Near: full label, large font\n\t\t\t\t\t\t\tfontSize = Math.max(10, Math.min(18, 120 / dist));\n\t\t\t\t\t\t\tlabel = eid.length > 24 ? `${eid.slice(0, 22)}..` : eid;\n\t\t\t\t\t\t} else if (dist < 35) {\n\t\t\t\t\t\t\t// Mid: entity type abbreviation\n\t\t\t\t\t\t\tfontSize = Math.max(9, Math.min(13, 80 / dist));\n\t\t\t\t\t\t\tconst kind = resolveShape3D(ent.entityType ?? \"\");\n\t\t\t\t\t\t\tconst typeNames: Record<string, string> = {\n\t\t\t\t\t\t\t\tconveyor: \"Conv\", diverter: \"Dvrt\", merge: \"Mrg\",\n\t\t\t\t\t\t\t\tlift: \"Lift\", scanner: \"Scan\", rack: \"Rack\",\n\t\t\t\t\t\t\t\tsrm: \"SRM\", robot: \"AGV\", stacker: \"Stk\",\n\t\t\t\t\t\t\t\tshuttle: \"Shl\", processor: \"Prc\", turntable: \"Trn\",\n\t\t\t\t\t\t\t\tbuffer: \"Buf\", source: \"Src\", sink: \"Sink\",\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tlabel = typeNames[kind] || kind.slice(0, 4);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Far: just a tiny dot marker, no text\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst escaped = label\n\t\t\t\t\t\t\t.replace(/&/g, \"&amp;\")\n\t\t\t\t\t\t\t.replace(/</g, \"&lt;\")\n\t\t\t\t\t\t\t.replace(/>/g, \"&gt;\")\n\t\t\t\t\t\t\t.replace(/\"/g, \"&quot;\");\n\t\t\t\t\t\tparts.push(\n\t\t\t\t\t\t\t`<span style=\"position:absolute;left:${screen.x}px;top:${screen.y}px;` +\n\t\t\t\t\t\t\t\t`transform:translate(-50%,-50%);font-size:${fontSize}px;color:#ccc;` +\n\t\t\t\t\t\t\t\t`background:${isSel ? \"rgba(64,128,255,0.8)\" : \"rgba(0,0,0,0.5)\"};` +\n\t\t\t\t\t\t\t\t`padding:1px 4px;border-radius:2px;white-space:nowrap;pointer-events:none;` +\n\t\t\t\t\t\t\t\t`font-family:'Segoe UI',sans-serif;` +\n\t\t\t\t\t\t\t\t`border:${isSel ? \"1px solid rgba(255,255,255,0.3)\" : \"none\"}\">${escaped}</span>`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tlabelEl.innerHTML = parts.join(\"\");\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"[Scene3D] render loop error:\", err);\n\t\t\t}\n\t\t});\n\n\t\tconst handleDragOver = (e: DragEvent) => {\n\t\t\te.preventDefault();\n\t\t\tif (e.dataTransfer) e.dataTransfer.dropEffect = \"copy\";\n\t\t};\n\t\tconst handleDrop = (e: DragEvent) => {\n\t\t\te.preventDefault();\n\t\t\tconst entityType = e.dataTransfer?.getData(\"entityType\");\n\t\t\tif (!entityType) return;\n\t\t\tconst rect = canvas.getBoundingClientRect();\n\t\t\tconst x = e.clientX - rect.left;\n\t\t\tconst y = e.clientY - rect.top;\n\t\t\ttry {\n\t\t\t\tconst pick = scene.pick(x, y, (m) => m === groundRef.current);\n\t\t\t\tif (pick?.pickedPoint) {\n\t\t\t\t\taddEntity(entityType, pick.pickedPoint.x, pick.pickedPoint.z);\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t};\n\n\t\tcanvas.addEventListener(\"dragover\", handleDragOver);\n\t\tcanvas.addEventListener(\"drop\", handleDrop);\n\n\t\tconst onResize = () => engine.resize();\n\t\twindow.addEventListener(\"resize\", onResize);\n\t\tconst resizeObs = new ResizeObserver(() => engine.resize());\n\t\tresizeObs.observe(canvas);\n\n\t\tconst onZoomIn = () => {\n\t\t\tif (cameraRef.current) cameraRef.current.radius *= 0.8;\n\t\t};\n\t\tconst onZoomOut = () => {\n\t\t\tif (cameraRef.current) cameraRef.current.radius *= 1.25;\n\t\t};\n\t\tconst onFitView = () => {\n\t\t\tif (!cameraRef.current) return;\n\t\t\tconst { entities, activeLayer } = useSimStore.getState();\n\t\t\tif (entities.size === 0) return;\n\t\t\tlet minX = Infinity,\n\t\t\t\tminZ = Infinity,\n\t\t\t\tmaxX = -Infinity,\n\t\t\t\tmaxZ = -Infinity;\n\t\t\tlet found = false;\n\n\t\t\t// Match Java Viewport.fit() logic: consider rotation (getGlobalPolygon)\n\t\t\tfor (const e of entities.values()) {\n\t\t\t\tif ((e.layer ?? 0) !== activeLayer) continue;\n\t\t\t\tfound = true;\n\t\t\t\tconst sx = e.sizex ?? 1;\n\t\t\t\tconst sy = e.sizey ?? 1;\n\t\t\t\tconst rotation = e.rotation ?? 0;\n\n\t\t\t\tconst halfW = sx / 2;\n\t\t\t\tconst halfH = sy / 2;\n\t\t\t\tconst corners = [\n\t\t\t\t\t{ x: -halfW, y: -halfH },\n\t\t\t\t\t{ x: halfW, y: -halfH },\n\t\t\t\t\t{ x: halfW, y: halfH },\n\t\t\t\t\t{ x: -halfW, y: halfH },\n\t\t\t\t];\n\n\t\t\t\tconst cosR = Math.cos(rotation);\n\t\t\t\tconst sinR = Math.sin(rotation);\n\t\t\t\tfor (const corner of corners) {\n\t\t\t\t\tconst rx = corner.x * cosR - corner.y * sinR;\n\t\t\t\t\tconst ry = corner.x * sinR + corner.y * cosR;\n\t\t\t\t\tconst wx = e.posx + rx;\n\t\t\t\t\tconst wz = e.posy + ry; // posy maps to Z in 3D\n\t\t\t\t\tif (wx < minX) minX = wx;\n\t\t\t\t\tif (wz < minZ) minZ = wz;\n\t\t\t\t\tif (wx > maxX) maxX = wx;\n\t\t\t\t\tif (wz > maxZ) maxZ = wz;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!found) return;\n\t\t\tconst cx = (minX + maxX) / 2;\n\t\t\tconst cz = (minZ + maxZ) / 2;\n\t\t\tcameraRef.current.target.set(cx, 0, cz);\n\t\t\tconst span = Math.max(maxX - minX, maxZ - minZ);\n\t\t\tcameraRef.current.radius = Math.max(span * 1.5 + 5, 10);\n\t\t};\n\t\tconst onShiftLeft = () => {\n\t\t\tif (cameraRef.current) cameraRef.current.target.x -= 2;\n\t\t};\n\t\tconst onShiftRight = () => {\n\t\t\tif (cameraRef.current) cameraRef.current.target.x += 2;\n\t\t};\n\t\tconst onShiftUp = () => {\n\t\t\tif (cameraRef.current) cameraRef.current.target.z -= 2;\n\t\t};\n\t\tconst onShiftDown = () => {\n\t\t\tif (cameraRef.current) cameraRef.current.target.z += 2;\n\t\t};\n\t\tconst noop = () => {};\n\n\t\tconst viewEvents: [string, EventListener][] = [\n\t\t\t[\"zoomIn\", onZoomIn],\n\t\t\t[\"zoomOut\", onZoomOut],\n\t\t\t[\"fitView\", onFitView],\n\t\t\t[\"shiftLeft\", onShiftLeft],\n\t\t\t[\"shiftRight\", onShiftRight],\n\t\t\t[\"shiftUp\", onShiftUp],\n\t\t\t[\"shiftDown\", onShiftDown],\n\t\t\t[\"toggleDetailLevel\", noop],\n\t\t\t[\"viewLayer\", noop],\n\t\t\t[\"toggleShadowView\", noop],\n\t\t\t[\"savePointOfView\", noop],\n\t\t\t[\"deletePointOfView\", noop],\n\t\t];\n\t\tfor (const [name, handler] of viewEvents) {\n\t\t\twindow.addEventListener(name, handler);\n\t\t}\n\n\t\treturn () => {\n\t\t\tdisposedRef.current = true;\n\n\t\t\t// Sync current 3D viewport to store before unmount\n\t\t\tconst cam = cameraRef.current;\n\t\t\tif (cam) {\n\t\t\t\tconst cx = cam.target.x;\n\t\t\t\tconst cz = cam.target.z;\n\t\t\t\tconst zoom = 60 / cam.radius;\n\t\t\t\tuseSimStore.getState().setViewport(cx, cz, zoom);\n\t\t\t}\n\n\t\t\tcanvas.removeEventListener(\"dragover\", handleDragOver);\n\t\t\tcanvas.removeEventListener(\"drop\", handleDrop);\n\t\t\twindow.removeEventListener(\"resize\", onResize);\n\t\t\tfor (const [name, handler] of viewEvents) {\n\t\t\t\twindow.removeEventListener(name, handler);\n\t\t\t}\n\t\t\tresizeObs.disconnect();\n\n\t\t\t// Stop render loop FIRST to prevent any further access to scene/meshes\n\t\t\tengine.stopRenderLoop();\n\n\t\t\t// Dispose scene (cascades to all meshes, materials, layers)\n\t\t\ttry {\n\t\t\t\tif (!scene.isDisposed) scene.dispose();\n\t\t\t} catch {}\n\n\t\t\t// Dispose engine (releases WebGL context)\n\t\t\ttry {\n\t\t\t\tif (!engine.isDisposed) engine.dispose();\n\t\t\t} catch {}\n\n\t\t\t// Clear all refs (meshes are already disposed via scene.dispose())\n\t\t\tflowParticlesRef.current = [];\n\t\t\tarrowCacheRef.current.clear();\n\t\t\tlinkMeshRef.current = null;\n\t\t\thighlightRef.current = null;\n\t\t\tentityMeshesRef.current.clear();\n\t\t\titemMeshesRef.current.clear();\n\t\t\toverlayMeshesRef.current.clear();\n\t\t\toverlayHashRef.current.clear();\n\t\t\tentityShapeKindsRef.current.clear();\n\t\t\tentitySizesRef.current.clear();\n\t\t\tprevPositionsRef.current.clear();\n\t\t\tmaterialCacheRef.current.clear();\n\t\t\tclearMaterialCache();\n\t\t\tthinManagerRef.current?.dispose();\n\t\t\tthinManagerRef.current = null;\n\t\t};\n\t}, [updateEntity, selectEntity, getMaterial, addEntity]);\n\n\tconst serverConfig = useSimStore((s) => s.serverConfig);\n\tuseEffect(() => {\n\t\tconst scene = sceneRef.current;\n\t\tif (!scene || scene.isDisposed) return;\n\t\tscene.clearColor = hexToColor4(serverConfig.backgroundColor);\n\t}, [serverConfig]);\n\n\treturn (\n\t\t<div style={{ position: \"relative\", width: \"100%\", height: \"100%\" }}>\n\t\t\t<canvas\n\t\t\t\tref={canvasRef}\n\t\t\t\taria-label=\"3D entity canvas\"\n\t\t\t\ttabIndex={-1}\n\t\t\t\tstyle={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100%\",\n\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\toutline: \"none\",\n\t\t\t\t\tbackground: serverConfig.backgroundColor || \"#ffffff\",\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<div\n\t\t\t\tref={labelRef}\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tinset: 0,\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<div\n\t\t\t\tref={metricsRef}\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\ttop: 8,\n\t\t\t\t\tleft: 8,\n\t\t\t\t\tpadding: \"6px 10px\",\n\t\t\t\t\tbackground: \"rgba(0,0,0,0.7)\",\n\t\t\t\t\tcolor: \"#aaccee\",\n\t\t\t\t\tfontSize: 11,\n\t\t\t\t\tfontFamily: \"'Consolas',monospace\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\tborder: \"1px solid rgba(255,255,255,0.1)\",\n\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t\tminWidth: 180,\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tbottom: 8,\n\t\t\t\t\tright: 8,\n\t\t\t\t\tpadding: \"4px 10px\",\n\t\t\t\t\tbackground: \"rgba(0,0,0,0.6)\",\n\t\t\t\t\tcolor: \"#aaccee\",\n\t\t\t\t\tfontSize: 13,\n\t\t\t\t\tfontFamily: \"'Segoe UI',sans-serif\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\tborder: \"1px solid rgba(255,255,255,0.1)\",\n\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t\tlineHeight: 1.4,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<span style={{ color: \"#888\", marginRight: 4 }}>TIME</span>\n\t\t\t\t{simTime.toFixed(1)}s\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"}