core_VoxelRenderer.js - hiro-nyon/cesium-heatbox GitHub Wiki

Source: core/VoxelRenderer.js

日本語 | English

English

See also: Class: VoxelRenderer

/**
 * Class responsible for rendering 3D voxels.
 * 3Dボクセルの描画を担当するクラス。
 * 
 * v0.1.11: ADR-0009準拠のアーキテクチャ - Single Responsibility Principle適用
 * 
 * **アーキテクチャ概要**:
 * - **オーケストレーション役**: 各専門クラスを統括し、描画プロセス全体を調整
 * - **ColorCalculator**: 色計算・カラーマップ処理の専門クラス (Phase 1)
 * - **VoxelSelector**: ボクセル選択戦略の専門クラス (Phase 2)  
 * - **AdaptiveController**: 適応制御ロジックの専門クラス (Phase 3)
 * - **GeometryRenderer**: ジオメトリ作成・エンティティ管理の専門クラス (Phase 4)
 * - **Phase 5**: 完全オーケストレーション化・性能最適化済み
 * 
 * **責任範囲**:
 * - 描画プロセスの統制・調整
 * - 各専門クラス間のデータ連携
 * - 高レベルAPIの提供・後方互換性維持
 * - エラーハンドリング・ログ管理
 */
import * as Cesium from 'cesium';
import { Logger } from '../utils/logger.js';
import { ColorCalculator } from './color/ColorCalculator.js';
import { VoxelSelector } from './selection/VoxelSelector.js';
import { AdaptiveController } from './adaptive/AdaptiveController.js';
import { GeometryRenderer } from './geometry/GeometryRenderer.js';
import { createClassifier } from '../utils/classification.js';

// v0.1.11: COLOR_MAPS moved to ColorCalculator (ADR-0009 Phase 1)
// v0.1.11: VoxelSelector added (ADR-0009 Phase 2)
// v0.1.11: AdaptiveController added (ADR-0009 Phase 3)
// v0.1.11: GeometryRenderer added (ADR-0009 Phase 4)

/**
 * VoxelRenderer - 3D voxel rendering orchestration class.
 * 3Dボクセル描画オーケストレーションクラス。
 * 
 * v0.1.11: Refactored for Single Responsibility Principle (ADR-0009).
 * Now serves as orchestrator delegating specialized tasks to:
 * ColorCalculator, VoxelSelector, AdaptiveController, and GeometryRenderer.
 * 
 * 各専門クラスに特化タスクを委譲するオーケストレーション役に特化。
 */
export class VoxelRenderer {
  /**
   * Constructor - Initialize VoxelRenderer orchestration system.
   * VoxelRendererオーケストレーションシステムを初期化します。
   * 
   * v0.1.11: Instantiates specialized classes for delegation:
   * - VoxelSelector: Voxel selection strategies (density, coverage, hybrid)
   * - AdaptiveController: Adaptive parameter calculation and preset logic  
   * - GeometryRenderer: Entity creation and management
   * - ColorCalculator: Used statically for color computation
   * 
   * 各専門クラスをインスタンス化し、委譲体制を構築:
   * - VoxelSelector: ボクセル選択戦略(密度・カバレッジ・ハイブリッド)
   * - AdaptiveController: 適応パラメータ計算・プリセットロジック
   * - GeometryRenderer: エンティティ作成・管理
   * - ColorCalculator: 色計算用の静的利用
   * 
   * @param {Cesium.Viewer} viewer - CesiumJS Viewer instance / CesiumJS Viewerインスタンス
   * @param {Object} options - Rendering options / 描画オプション
   * @param {Array} [options.minColor=[0,0,255]] - Minimum density color (RGB) / 最小密度色
   * @param {Array} [options.maxColor=[255,0,0]] - Maximum density color (RGB) / 最大密度色
   * @param {number} [options.opacity=0.8] - Base opacity / 基本透明度
   * @param {boolean} [options.showOutline=true] - Show voxel outlines / ボクセル枠線表示
   * @param {string} [options.voxelSelectionStrategy='density'] - Selection strategy / 選択戦略
   * @param {boolean} [options.adaptiveOutlines=false] - Enable adaptive outline control / 適応枠線制御
   */
  constructor(viewer, options = {}) {
    this.viewer = viewer;
    this.options = {
      minColor: [0, 0, 255],
      maxColor: [255, 0, 0],
      opacity: 0.8,
      emptyOpacity: 0.03,
      showOutline: true,
      showEmptyVoxels: false,
      wireframeOnly: false,    // 枠線のみ表示
      heightBased: false,      // 高さベース表現
      outlineWidth: 2,         // 枠線の太さ
      // v0.1.6.1: インセット枠線のデフォルト値
      outlineInset: 0,         // インセット枠線オフセット(メートル)
      outlineInsetMode: 'all', // インセット枠線適用範囲
      // v0.1.7: 新オプション
      outlineRenderMode: 'standard',
      emulationScope: 'off', // v0.1.12: emulation scope control
      adaptiveOutlines: false,
      outlineWidthPreset: 'medium', // v0.1.12: updated default
      ...options
    };
    if (!this.options.classification || typeof this.options.classification !== 'object') {
      this.options.classification = { enabled: false };
    }
    
    // v0.1.11-alpha: VoxelSelector instantiation (ADR-0009 Phase 2)
    this.voxelSelector = new VoxelSelector(this.options);
    this._selectionStats = null;
    
    // v0.1.11-alpha: AdaptiveController instantiation (ADR-0009 Phase 3)
    this.adaptiveController = new AdaptiveController(this.options);
    
    // v0.1.11-alpha: GeometryRenderer instantiation (ADR-0009 Phase 4)
    this.geometryRenderer = new GeometryRenderer(this.viewer, this.options);
    
    // Legacy compatibility: voxelEntities now delegates to GeometryRenderer
    Object.defineProperty(this, 'voxelEntities', {
      get: () => this.geometryRenderer.entities,
      enumerable: true,
      configurable: true
    });

    this._currentVoxelData = null;
    this._classifier = null;

    Logger.debug('VoxelRenderer initialized with options:', this.options);
  }

  /**
   * Compute adaptive outline parameters (v0.1.11).
   * 適応的枠線パラメータを計算 (v0.1.11-alpha)。
   * v0.1.11: AdaptiveControllerに委譲 (ADR-0009 Phase 3)
   * @param {Object} voxelInfo - Voxel info / ボクセル情報
   * @param {boolean} isTopN - Whether it is TopN / TopNボクセルかどうか
   * @param {Map} voxelData - All voxel data / 全ボクセルデータ
   * @param {Object} statistics - Statistics / 統計情報
  * @returns {Object} Adaptive params / 適応的パラメータ
  * @private
  */
  _calculateAdaptiveParams(voxelInfo, isTopN, voxelData, statistics, grid) {
    // v0.1.11: 新しいAdaptiveControllerに委譲しつつ、既存インターフェースを維持 (ADR-0009 Phase 3)
    return this.adaptiveController.calculateAdaptiveParams(
      voxelInfo,
      isTopN,
      voxelData,
      statistics,
      this.options,
      grid,
      this._classifier
    );
  }

  /**
   * Backward-compatible inset outline decision API.
   * 後方互換のためのインセット枠線適用判定メソッド。
   * v0.1.11: GeometryRenderer に委譲 (ADR-0009 Phase 4)
   * @param {boolean} isTopN
   * @returns {boolean}
   * @private
   */
  _shouldApplyInsetOutline(isTopN) {
    return this.geometryRenderer.shouldApplyInsetOutline(isTopN);
  }

  /**
   * Render voxel data - Orchestrated rendering process.
   * ボクセルデータ描画 - オーケストレーション化された描画プロセス。
   * 
   * v0.1.11: Fully orchestrated implementation (ADR-0009 Phase 5):
   * 
   * **Process Flow**:
   * 1. **GeometryRenderer.clear()** - Clear existing entities
   * 2. **VoxelSelector.selectVoxels()** - Apply selection strategy if needed
   * 3. **For each voxel**: Delegate to `_renderSingleVoxel()` for orchestration:
   *    - **AdaptiveController** - Calculate adaptive parameters
   *    - **ColorCalculator** - Compute colors based on density  
   *    - **GeometryRenderer** - Create voxel box, outlines, and polylines
   * 4. **Return count** - Number of successfully rendered voxels
   * 
   * **実行フロー**:
   * 1. **GeometryRenderer.clear()** - 既存エンティティのクリア
   * 2. **VoxelSelector.selectVoxels()** - 必要に応じて選択戦略適用
   * 3. **各ボクセル**: `_renderSingleVoxel()` へのオーケストレーション委譲:
   *    - **AdaptiveController** - 適応パラメータ計算
   *    - **ColorCalculator** - 密度ベース色計算
   *    - **GeometryRenderer** - ボクセルボックス・枠線・ポリライン作成
   * 4. **カウント返却** - 正常描画されたボクセル数
   * 
   * @param {Map} voxelData - Voxel data map / ボクセルデータマップ
   * @param {Object} bounds - Spatial bounds / 空間境界
   * @param {Object} grid - Grid configuration / グリッド設定
   * @param {Object} statistics - Density statistics / 密度統計
   * @returns {number} Number of rendered voxels / 実際に描画されたボクセル数
   */
  render(voxelData, bounds, grid, statistics) {
    // v0.1.11: GeometryRendererに委譲してエンティティクリア (ADR-0009 Phase 4)
    this.geometryRenderer.clear();
    Logger.debug('VoxelRenderer.render - Starting render with simplified approach', {
      voxelDataSize: voxelData.size,
      bounds,
      grid,
      statistics
    });

    this._currentVoxelData = voxelData;
    this._prepareClassifier(statistics);

    // バウンディングボックスのデバッグ表示制御(v0.1.5: debug.showBounds対応)
    const shouldShowBounds = this._shouldShowBounds();
    if (shouldShowBounds) {
      this.geometryRenderer.renderBoundingBox(bounds);
    }

    // 表示するボクセルのリスト
    let displayVoxels = [];
    const topNVoxels = new Set(); // v0.1.5: TopN強調表示用

    // 空ボクセルのフィルタリング
    if (this.options.showEmptyVoxels) {
      // 全ボクセルを生成(これは上限値が大きいとメモリ消費とパフォーマンスに影響する)
      const maxVoxels = Math.min(grid.totalVoxels, this.options.maxRenderVoxels || 10000);
      Logger.debug(`Generating grid for up to ${maxVoxels} voxels`);
      
      // 空のボクセルも含めて全ボクセルを追加
      for (let x = 0; x < grid.numVoxelsX; x++) {
        for (let y = 0; y < grid.numVoxelsY; y++) {
          for (let z = 0; z < grid.numVoxelsZ; z++) {
            const voxelKey = `${x},${y},${z}`;
            const voxelInfo = voxelData.get(voxelKey) || { x, y, z, count: 0 };
            
            displayVoxels.push({
              key: voxelKey,
              info: voxelInfo
            });
            
            if (displayVoxels.length >= maxVoxels) {
              Logger.debug(`Reached maximum voxel limit of ${maxVoxels}`);
              break;
            }
          }
          if (displayVoxels.length >= maxVoxels) break;
        }
        if (displayVoxels.length >= maxVoxels) break;
      }
    } else {
      // データがあるボクセルのみ表示
      displayVoxels = Array.from(voxelData.entries()).map(([key, info]) => {
        return { key, info };
      });
      
      // v0.1.9: 適応的レンダリング制限の適用
      if (this.options.maxRenderVoxels && displayVoxels.length > this.options.maxRenderVoxels) {
        const selectionResult = this._selectVoxelsForRendering(displayVoxels, this.options.maxRenderVoxels, bounds, grid);
        displayVoxels = selectionResult.selectedVoxels;
        
        // 統計情報の更新
        this._selectionStats = {
          strategy: selectionResult.strategy,
          clippedNonEmpty: selectionResult.clippedNonEmpty,
          coverageRatio: selectionResult.coverageRatio || 0
        };
        
        Logger.debug(`Applied ${selectionResult.strategy} strategy: ${displayVoxels.length} voxels selected, ${selectionResult.clippedNonEmpty} clipped`);
      }
    }

    // v0.1.5: TopN強調表示の前処理
    if (this.options.highlightTopN && this.options.highlightTopN > 0) {
      const sortedForTopN = [...displayVoxels].sort((a, b) => b.info.count - a.info.count);
      const topN = sortedForTopN.slice(0, this.options.highlightTopN);
      topN.forEach(voxel => topNVoxels.add(voxel.key));
      Logger.debug(`TopN highlight enabled: ${topNVoxels.size} voxels will be highlighted`);
    }
    
    Logger.debug(`Rendering ${displayVoxels.length} voxels`);
    
    // レンダリングカウント
    let renderedCount = 0;

    // 実際にボクセルを描画
    // パフォーマンス最適化: Resolver用の一時オブジェクトを再利用してGCを削減
    const reusableVoxelCtx = { x: 0, y: 0, z: 0, count: 0 };
    const reusableWidthResolverParams = { voxel: reusableVoxelCtx, isTopN: false, normalizedDensity: 0, statistics, adaptiveParams: null };
    const reusableOpacityResolverCtx = { voxel: reusableVoxelCtx, isTopN: false, normalizedDensity: 0, statistics, adaptiveParams: null };

    displayVoxels.forEach(({ key, info }) => {
      try {
        renderedCount += this._renderSingleVoxel(key, info, bounds, grid, statistics, topNVoxels, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx);
      } catch (error) {
        Logger.warn('Error rendering voxel:', error);
      }
    });

    Logger.info(`Successfully rendered ${renderedCount} voxels`);

    this._currentVoxelData = null;

    // 実際に描画されたボクセル数を返す
    return renderedCount;
  }

  // v0.1.11: _renderBoundingBox/_addEdgePolylines moved to GeometryRenderer (ADR-0009 Phase 4)

  /**
   * Render a single voxel with all visual configurations.
   * 単一ボクセルを全ての視覚設定で描画します。
   * v0.1.11: Phase5 オーケストレーション最適化 (ADR-0009 Phase 5)
   * @param {string} key - Voxel key / ボクセルキー
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {Object} bounds - Bounds / 境界
   * @param {Object} grid - Grid / グリッド
   * @param {Object} statistics - Statistics / 統計
   * @param {Set} topNVoxels - TopN voxel keys / TopNボクセルキー
   * @param {Object} reusableVoxelCtx - Reusable context for performance / 再利用コンテキスト
   * @param {Object} reusableWidthResolverParams - Reusable width resolver params / 再利用太さResolver
   * @param {Object} reusableOpacityResolverCtx - Reusable opacity resolver context / 再利用透明度Resolver
   * @returns {number} 1 if rendered successfully, 0 if skipped / 描画成功時1、スキップ時0
   * @private
   */
  _renderSingleVoxel(key, info, bounds, grid, statistics, topNVoxels, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx) {
    const isTopN = topNVoxels.has(key);
    
    // Calculate voxel rendering parameters
    const renderParams = this._calculateVoxelRenderingParams(info, bounds, grid, statistics, isTopN, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx);
    
    // 安全性チェック - レンダリングパラメータが無効な場合はスキップ
    if (!renderParams) {
      return 0; // Skipped rendering due to invalid parameters
    }
    
    // Delegate to GeometryRenderer for actual rendering
    this._delegateVoxelRendering(key, renderParams);
    
    return 1; // Successfully rendered
  }

  /**
   * Calculate all rendering parameters for a voxel.
   * ボクセルの全描画パラメータを計算します。
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {Object} bounds - Bounds / 境界
   * @param {Object} grid - Grid / グリッド
   * @param {Object} statistics - Statistics / 統計
   * @param {boolean} isTopN - Is TopN voxel / TopNボクセルか
   * @param {Object} reusableVoxelCtx - Reusable context / 再利用コンテキスト
   * @param {Object} reusableWidthResolverParams - Width resolver params / 太さResolver
   * @param {Object} reusableOpacityResolverCtx - Opacity resolver context / 透明度Resolver
   * @returns {Object} Complete rendering parameters / 完全な描画パラメータ
   * @private
   */
  _calculateVoxelRenderingParams(info, bounds, grid, statistics, isTopN, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx) {
    // 引数の安全性チェック
    if (!info || !bounds || !grid || !statistics) {
      return null;
    }
    
    // v0.1.17: Spatial ID mode - calculate position from 8-vertex bounds / 空間IDモード - 8頂点boundsから位置を計算
    let centerLon, centerLat, centerAlt, cellSizeX, cellSizeY, baseCellSizeZ;
    
    if (info.bounds && Array.isArray(info.bounds) && info.bounds.length === 8) {
      // Spatial ID mode: use 8 vertices to calculate center and dimensions
      // 空間IDモード: 8頂点を使って中心と寸法を計算
      const vertices = info.bounds;
      
      // Calculate center from vertices (average of all 8 points)
      // 頂点から中心を計算(8点の平均)
      centerLon = vertices.reduce((sum, v) => sum + v.lng, 0) / 8;
      centerLat = vertices.reduce((sum, v) => sum + v.lat, 0) / 8;
      centerAlt = vertices.reduce((sum, v) => sum + v.alt, 0) / 8;
      
      // Calculate dimensions from vertices (distance between corners)
      // 頂点から寸法を計算(角間の距離)
      // Assume vertices[0-3] are bottom face, vertices[4-7] are top face
      // vertices[0], [1], [2], [3] form bottom rectangle
      // SpatialIdAdapter/ZFXYConverter guarantee this ordering.
      const v0 = Cesium.Cartesian3.fromDegrees(vertices[0].lng, vertices[0].lat, vertices[0].alt);
      const v1 = Cesium.Cartesian3.fromDegrees(vertices[1].lng, vertices[1].lat, vertices[1].alt);
      const v3 = Cesium.Cartesian3.fromDegrees(vertices[3].lng, vertices[3].lat, vertices[3].alt);
      const v4 = Cesium.Cartesian3.fromDegrees(vertices[4].lng, vertices[4].lat, vertices[4].alt);
      
      cellSizeX = Cesium.Cartesian3.distance(v0, v1);
      cellSizeY = Cesium.Cartesian3.distance(v0, v3);
      baseCellSizeZ = Cesium.Cartesian3.distance(v0, v4);
    } else {
      // Uniform grid mode: use x/y/z indices (legacy)
      // 一様グリッドモード: x/y/zインデックスを使用(レガシー)
      const { x, y, z } = info;
      
      centerLon = bounds.minLon + (x + 0.5) * (bounds.maxLon - bounds.minLon) / grid.numVoxelsX;
      centerLat = bounds.minLat + (y + 0.5) * (bounds.maxLat - bounds.minLat) / grid.numVoxelsY;
      centerAlt = bounds.minAlt + (z + 0.5) * (bounds.maxAlt - bounds.minAlt) / grid.numVoxelsZ;
      
      // Calculate dimensions from grid (legacy)
      // グリッドから寸法を計算(レガシー)
      cellSizeX = grid.cellSizeX || (grid.lonRangeMeters ? (grid.lonRangeMeters / grid.numVoxelsX) : grid.voxelSizeMeters);
      cellSizeY = grid.cellSizeY || (grid.latRangeMeters ? (grid.latRangeMeters / grid.numVoxelsY) : grid.voxelSizeMeters);
      baseCellSizeZ = grid.cellSizeZ || (grid.altRangeMeters ? Math.max(grid.altRangeMeters / Math.max(grid.numVoxelsZ, 1), 1) : Math.max(grid.voxelSizeMeters, 1));
    }
    
    // Normalized density
    const normalizedDensity = statistics.maxCount > statistics.minCount ? 
      (info.count - statistics.minCount) / (statistics.maxCount - statistics.minCount) : 0;
    
    // Adaptive parameters
    const adaptiveParams = this._calculateAdaptiveParams(info, isTopN, this._currentVoxelData, statistics, grid);
    
    // Color and opacity
    const { color, opacity } = this._calculateColorAndOpacity(info, normalizedDensity, isTopN, adaptiveParams, statistics, reusableVoxelCtx, reusableOpacityResolverCtx);
    
    // Dimensions (v0.1.17: use pre-calculated dimensions for spatial ID mode)
    let finalCellSizeX, finalCellSizeY, boxHeight;
    if (info.bounds) {
      // Spatial ID mode: use pre-calculated dimensions, apply voxel gap
      // 空間IDモード: 事前計算された寸法を使用、voxelGapを適用
      finalCellSizeX = cellSizeX;
      finalCellSizeY = cellSizeY;
      let finalBaseCellSizeZ = baseCellSizeZ;
      
      if (this.options.voxelGap > 0) {
        finalCellSizeX = Math.max(cellSizeX - this.options.voxelGap, cellSizeX * 0.1);
        finalCellSizeY = Math.max(cellSizeY - this.options.voxelGap, cellSizeY * 0.1);
        finalBaseCellSizeZ = Math.max(baseCellSizeZ - this.options.voxelGap, baseCellSizeZ * 0.1);
      }
      
      boxHeight = finalBaseCellSizeZ;
      if (this.options.heightBased) {
        boxHeight = finalBaseCellSizeZ * (0.1 + normalizedDensity * 0.9);
      }
    } else {
      // Uniform grid mode: calculate dimensions from grid
      // 一様グリッドモード: グリッドから寸法を計算
      const dimensions = this._calculateDimensions(grid, normalizedDensity);
      finalCellSizeX = dimensions.cellSizeX;
      finalCellSizeY = dimensions.cellSizeY;
      boxHeight = dimensions.boxHeight;
    }
    
    // Outline properties
    const outlineProps = this._calculateOutlineProperties(info, isTopN, normalizedDensity, adaptiveParams, statistics, color, reusableVoxelCtx, reusableWidthResolverParams);
    
    return {
      centerLon, centerLat, centerAlt,
      cellSizeX: finalCellSizeX, cellSizeY: finalCellSizeY, boxHeight,
      color, opacity,
      ...outlineProps,
      voxelInfo: info,
      isTopN,
      adaptiveParams
    };
  }

  /**
   * Calculate color and opacity for a voxel.
   * ボクセルの色と透明度を計算します。
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {number} normalizedDensity - Normalized density / 正規化密度
   * @param {boolean} isTopN - Is TopN voxel / TopNボクセルか
   * @param {Object} adaptiveParams - Adaptive params / 適応パラメータ
   * @param {Object} statistics - Statistics / 統計
   * @param {Object} reusableVoxelCtx - Reusable context / 再利用コンテキスト
   * @param {Object} reusableOpacityResolverCtx - Opacity resolver context / 透明度Resolverコンテキスト
   * @returns {Object} Color and opacity / 色と透明度
   * @private
   */
  _calculateColorAndOpacity(info, normalizedDensity, isTopN, adaptiveParams, statistics, reusableVoxelCtx, reusableOpacityResolverCtx) {
    let color, opacity;
    
    if (info.count === 0) {
      color = Cesium.Color.LIGHTGRAY;
      opacity = this.options.emptyOpacity;
    } else {
      const classificationTargets = this.options.classification?.classificationTargets || {};
      const shouldApplyClassificationColor = this._classifier && classificationTargets.color !== false;
      if (shouldApplyClassificationColor) {
        try {
          const classIndex = this._classifier.classify(info.count ?? 0);
          color = this._classifier.getColorForClass(classIndex);
        } catch (error) {
          Logger.warn('Classification color calculation failed, falling back to legacy color:', error);
          color = ColorCalculator.calculateColor(normalizedDensity, info.count, this.options);
        }
      } else {
        color = ColorCalculator.calculateColor(normalizedDensity, info.count, this.options);
      }
      
      // Opacity calculation with resolver support
      if (this.options.boxOpacityResolver && typeof this.options.boxOpacityResolver === 'function') {
        reusableVoxelCtx.x = info.x; reusableVoxelCtx.y = info.y; reusableVoxelCtx.z = info.z; reusableVoxelCtx.count = info.count;
        reusableOpacityResolverCtx.isTopN = isTopN;
        reusableOpacityResolverCtx.normalizedDensity = normalizedDensity;
        reusableOpacityResolverCtx.adaptiveParams = adaptiveParams;
        try {
          const resolverOpacity = this.options.boxOpacityResolver(reusableOpacityResolverCtx);
          opacity = isNaN(resolverOpacity) ? this.options.opacity : Math.max(0, Math.min(1, resolverOpacity));
        } catch (e) {
          Logger.warn('boxOpacityResolver error, using fallback:', e);
          opacity = (adaptiveParams.boxOpacity ?? this.options.opacity);
        }
      } else {
        opacity = (adaptiveParams.boxOpacity ?? this.options.opacity);
      }
      
      // TopN highlight adjustment 
      if (this.options.highlightTopN && !isTopN) {
        opacity *= (1 - (this.options.highlightStyle?.boostOpacity || 0.2));
      }
    }
    
    return { color, opacity };
  }

  /**
   * Calculate voxel dimensions with gap support.
   * voxelGap対応のボクセル寸法を計算します。
   * @param {Object} grid - Grid info / グリッド情報
   * @param {number} normalizedDensity - Normalized density / 正規化密度
   * @returns {Object} Dimensions / 寸法
   * @private
   */
  _calculateDimensions(grid, normalizedDensity) {
    let cellSizeX = grid.cellSizeX || (grid.lonRangeMeters ? (grid.lonRangeMeters / grid.numVoxelsX) : grid.voxelSizeMeters);
    let cellSizeY = grid.cellSizeY || (grid.latRangeMeters ? (grid.latRangeMeters / grid.numVoxelsY) : grid.voxelSizeMeters);
    let baseCellSizeZ = grid.cellSizeZ || (grid.altRangeMeters ? Math.max(grid.altRangeMeters / Math.max(grid.numVoxelsZ, 1), 1) : Math.max(grid.voxelSizeMeters, 1));

    // Apply voxel gap
    if (this.options.voxelGap > 0) {
      cellSizeX = Math.max(cellSizeX - this.options.voxelGap, cellSizeX * 0.1);
      cellSizeY = Math.max(cellSizeY - this.options.voxelGap, cellSizeY * 0.1);
      baseCellSizeZ = Math.max(baseCellSizeZ - this.options.voxelGap, baseCellSizeZ * 0.1);
    }

    // Height-based scaling
    let boxHeight = baseCellSizeZ;
    if (this.options.heightBased) {
      boxHeight = baseCellSizeZ * (0.1 + normalizedDensity * 0.9);
    }
    
    return { cellSizeX, cellSizeY, boxHeight };
  }

  /**
   * Calculate outline properties with resolver support.
   * Resolver対応の枠線プロパティを計算します。
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {boolean} isTopN - Is TopN voxel / TopNボクセルか
   * @param {number} normalizedDensity - Normalized density / 正規化密度
   * @param {Object} adaptiveParams - Adaptive params / 適応パラメータ
   * @param {Object} statistics - Statistics / 統計
   * @param {Cesium.Color} color - Base color / ベース色
   * @param {Object} reusableVoxelCtx - Reusable context / 再利用コンテキスト
   * @param {Object} reusableWidthResolverParams - Width resolver params / 太さResolverパラメータ
   * @returns {Object} Outline properties / 枠線プロパティ
   * @private
   */
  _calculateOutlineProperties(info, isTopN, normalizedDensity, adaptiveParams, statistics, color, reusableVoxelCtx, reusableWidthResolverParams) {
    // Outline width calculation
    let finalOutlineWidth;
    if (this.options.outlineWidthResolver && typeof this.options.outlineWidthResolver === 'function') {
      reusableVoxelCtx.x = info.x; reusableVoxelCtx.y = info.y; reusableVoxelCtx.z = info.z; reusableVoxelCtx.count = info.count;
      reusableWidthResolverParams.isTopN = isTopN;
      reusableWidthResolverParams.normalizedDensity = normalizedDensity;
      reusableWidthResolverParams.adaptiveParams = adaptiveParams;
      try {
        finalOutlineWidth = this.options.outlineWidthResolver(reusableWidthResolverParams);
        if (isNaN(finalOutlineWidth)) {
          finalOutlineWidth = adaptiveParams.outlineWidth || this.options.outlineWidth;
        }
      } catch (e) {
        Logger.warn('outlineWidthResolver error, using fallback:', e);
        finalOutlineWidth = adaptiveParams.outlineWidth || this.options.outlineWidth;
      }
    } else {
      if (adaptiveParams.outlineWidth !== null && adaptiveParams.outlineWidth !== undefined) {
        finalOutlineWidth = adaptiveParams.outlineWidth;
      } else {
        finalOutlineWidth = isTopN && this.options.highlightTopN ? 
          (this.options.highlightStyle?.outlineWidth || this.options.outlineWidth) : 
          this.options.outlineWidth;
      }
    }

    // Outline opacity
    const finalOutlineOpacity = adaptiveParams.outlineOpacity ?? (this.options.outlineOpacity ?? 1.0);
    const outlineColorWithOpacity = color.withAlpha(finalOutlineOpacity);

    // Render mode configuration
    const renderModeConfig = this._determineRenderModeConfig();

    // Emulation logic
    let emulateThickForThis = renderModeConfig.shouldUseEmulationOnly;
    if (!renderModeConfig.shouldUseEmulationOnly) {
      // v0.1.12: Use new emulationScope instead of deprecated outlineEmulation
      const scope = this.options.emulationScope || 'off';
      if (scope === 'topn') {
        emulateThickForThis = isTopN && (finalOutlineWidth || 1) > 1;
      } else if (scope === 'non-topn') {
        emulateThickForThis = !isTopN && (finalOutlineWidth || 1) > 1;
      } else if (scope === 'all') {
        emulateThickForThis = (finalOutlineWidth || 1) > 1;
      } else if (this.options.adaptiveOutlines && adaptiveParams.shouldUseEmulation) {
        // Safety: when scope is explicitly 'off', do not enable emulation from adaptive control
        emulateThickForThis = scope !== 'off';
      }
    }

    return {
      shouldShowOutline: renderModeConfig.shouldShowStandardOutline,
      outlineColor: outlineColorWithOpacity,
      outlineWidth: finalOutlineWidth || 1,
      shouldShowInsetOutline: renderModeConfig.shouldShowInsetOutline,
      emulateThick: emulateThickForThis
    };
  }

  /**
   * Determine render mode configuration.
   * レンダーモード設定を決定します。
   * @returns {Object} Render mode config / レンダーモード設定
   * @private
   */
  _determineRenderModeConfig() {
    let shouldShowStandardOutline = true;
    let shouldShowInsetOutline = false;
    let shouldUseEmulationOnly = false;
    
    switch (this.options.outlineRenderMode) {
      case 'standard':
        shouldShowStandardOutline = this.options.showOutline;
        shouldShowInsetOutline = this.options.outlineInset > 0;
        break;
      case 'inset':
        shouldShowStandardOutline = false;
        shouldShowInsetOutline = true;
        break;
      case 'emulation-only':
        shouldShowStandardOutline = false;
        shouldShowInsetOutline = false;
        shouldUseEmulationOnly = true;
        break;
    }
    
    return { shouldShowStandardOutline, shouldShowInsetOutline, shouldUseEmulationOnly };
  }

  /**
   * Delegate voxel rendering to GeometryRenderer.
   * ボクセル描画をGeometryRendererに委譲します。
   * @param {string} key - Voxel key / ボクセルキー
   * @param {Object} params - Rendering parameters / 描画パラメータ
   * @private
   */
  _delegateVoxelRendering(key, params) {
    // Main voxel box
    this.geometryRenderer.createVoxelBox({
      centerLon: params.centerLon, centerLat: params.centerLat, centerAlt: params.centerAlt,
      cellSizeX: params.cellSizeX, cellSizeY: params.cellSizeY, boxHeight: params.boxHeight,
      color: params.color, opacity: params.opacity,
      shouldShowOutline: params.shouldShowOutline,
      outlineColor: params.outlineColor,
      outlineWidth: params.outlineWidth,
      voxelInfo: params.voxelInfo,
      voxelKey: key,
      emulateThick: params.emulateThick
    });

    // Inset outline
    if (params.shouldShowInsetOutline && this.geometryRenderer.shouldApplyInsetOutline(params.isTopN)) {
      try {
        const insetAmount = this.options.outlineInset > 0 ? this.options.outlineInset : 1;
        this.geometryRenderer.createInsetOutline({
          centerLon: params.centerLon, centerLat: params.centerLat, centerAlt: params.centerAlt,
          baseSizeX: params.cellSizeX, baseSizeY: params.cellSizeY, baseSizeZ: params.boxHeight,
          outlineColor: params.outlineColor,
          outlineWidth: Math.max(params.outlineWidth, 1),
          voxelKey: key,
          insetAmount
        });
      } catch (e) {
        Logger.warn('Failed to create inset outline:', e);
      }
    }
    
    // Edge polylines for thick emulation
    // 追加の安全ガード: emulation-only もしくは emulationScope!='off' の場合のみ許可
    const allowEmulationEdges = (this.options.outlineRenderMode === 'emulation-only') ||
      (this.options.emulationScope && this.options.emulationScope !== 'off');
    if (allowEmulationEdges && params.emulateThick) {
      try {
        const adaptiveOutlineOpacity = params.adaptiveParams?.outlineOpacity ?? params.outlineColor?.alpha ?? null;
        this.geometryRenderer.createEdgePolylines({
          centerLon: params.centerLon, centerLat: params.centerLat, centerAlt: params.centerAlt,
          cellSizeX: params.cellSizeX, cellSizeY: params.cellSizeY, boxHeight: params.boxHeight,
          outlineColor: params.outlineColor,
          outlineWidth: Math.max(params.outlineWidth, 1),
          outlineOpacity: adaptiveOutlineOpacity,
          voxelKey: key
        });
      } catch (e) {
        Logger.warn('Failed to add emulated thick outline polylines:', e);
      }
    }
  }

  /**
   * Interpolate color based on density (v0.1.5: color maps supported).
   * 密度に基づいて色を補間(v0.1.5: カラーマップ対応)。
   * v0.1.11: ColorCalculatorに委譲 (ADR-0009 Phase 1)
   * @param {number} normalizedDensity - Normalized density (0-1) / 正規化された密度 (0-1)
   * @param {number} [rawValue] - Raw value for diverging scheme / 生値(二極性配色用)
   * @returns {Cesium.Color} Calculated color / 計算された色
   */
  interpolateColor(normalizedDensity, rawValue = null) {
    // v0.1.11: 新しいColorCalculatorに委譲
    return ColorCalculator.calculateColor(normalizedDensity, rawValue, this.options);
  }

  _prepareClassifier(statistics) {
    const classificationOptions = this.options.classification;
    if (!classificationOptions || !classificationOptions.enabled) {
      this._classifier = null;
      return;
    }

    const allowedSchemes = ['linear', 'log', 'equal-interval', 'quantize', 'threshold', 'quantile', 'jenks'];
    const scheme = (classificationOptions.scheme || 'linear').toLowerCase();
    if (!allowedSchemes.includes(scheme)) {
      Logger.warn(`Classification scheme '${scheme}' is not supported in v1.0.0. Disabling classification.`);
      this._classifier = null;
      return;
    }

    const domain = Array.isArray(classificationOptions.domain) && classificationOptions.domain.length === 2
      ? classificationOptions.domain
      : [statistics?.minCount ?? 0, statistics?.maxCount ?? 0];

    let values = null;
    if (this._currentVoxelData && this._currentVoxelData.size > 0) {
      values = [];
      for (const voxelInfo of this._currentVoxelData.values()) {
        if (voxelInfo && Number.isFinite(voxelInfo.count)) {
          values.push(voxelInfo.count);
        }
      }
      if (values.length === 0) {
        values = null;
      }
    }

    try {
      this._classifier = createClassifier({
        scheme,
        classes: classificationOptions.classes,
        thresholds: classificationOptions.thresholds,
        colorMap: classificationOptions.colorMap,
        domain,
        values
      });
    } catch (error) {
      Logger.warn('Failed to initialize classifier:', error);
      this._classifier = null;
    }
  }
  
  // v0.1.11: _interpolateFromColorMap and _interpolateDivergingColor methods 
  // moved to ColorCalculator (ADR-0009 Phase 1)


  /**
   * Remove all rendered entities from the scene.
   * 描画されたエンティティを全てクリアします。
   * v0.1.11: GeometryRendererに委譲 (ADR-0009 Phase 4)
   */
  clear() {
    this.geometryRenderer.clear();
  }

  /**
   * デバッグ境界ボックス表示の判定(v0.1.5: debug.showBounds対応)
   * @returns {boolean} 境界ボックスを表示する場合はtrue
   * @private
   */
  _shouldShowBounds() {
    if (!this.options.debug) {
      return false;
    }
    
    if (typeof this.options.debug === 'boolean') {
      // 従来の動作:debugがtrueの場合はバウンディングボックス表示
      return this.options.debug;
    }
    
    if (typeof this.options.debug === 'object' && this.options.debug !== null) {
      // 新しい動作:debug.showBoundsで明示的に制御
      return this.options.debug.showBounds === true;
    }
    
    return false;
  }


  // v0.1.11: _createInsetOutline moved to GeometryRenderer (ADR-0009 Phase 4)

  // Thick outline frame creation is fully handled by GeometryRenderer.

  /**
   * Toggle visibility.
   * 表示/非表示を切り替えます。
   * v0.1.11: GeometryRendererに委譲 (ADR-0009 Phase 5)
   * @param {boolean} show - true to show / 表示する場合は true
   */
  setVisible(show) {
    Logger.debug('VoxelRenderer.setVisible:', show);
    this.voxelEntities.forEach(entity => {
      if (entity && (!entity.isDestroyed || !entity.isDestroyed())) {
        entity.show = show;
      }
    });
  }

  /**
   * Select voxels for rendering based on the specified strategy.
   * 指定された戦略に基づいてレンダリング用ボクセルを選択します。
   * @param {Array} allVoxels - All available voxels / 利用可能な全ボクセル
   * @param {number} maxCount - Maximum number of voxels to select / 選択する最大ボクセル数
   * @param {Object} bounds - Data bounds / データ境界
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectVoxelsForRendering(allVoxels, maxCount, bounds, grid) {
    // v0.1.11: 新しいVoxelSelectorに委譲しつつ、既存インターフェースを維持 (ADR-0009 Phase 2)
    const selectionResult = this.voxelSelector.selectVoxels(allVoxels, maxCount, { grid, bounds });
    
    // 統計情報の更新
    this._selectionStats = this.voxelSelector.getLastSelectionStats();
    
    return selectionResult;
  }




  /**
   * Get selection statistics.
   * 選択統計を取得します。
   * @returns {Object|null} Selection statistics / 選択統計
   */
  getSelectionStats() {
    return this._selectionStats || null;
  }
}

日本語

関連: VoxelRendererクラス

/**
 * Class responsible for rendering 3D voxels.
 * 3Dボクセルの描画を担当するクラス。
 * 
 * v0.1.11: ADR-0009準拠のアーキテクチャ - Single Responsibility Principle適用
 * 
 * **アーキテクチャ概要**:
 * - **オーケストレーション役**: 各専門クラスを統括し、描画プロセス全体を調整
 * - **ColorCalculator**: 色計算・カラーマップ処理の専門クラス (Phase 1)
 * - **VoxelSelector**: ボクセル選択戦略の専門クラス (Phase 2)  
 * - **AdaptiveController**: 適応制御ロジックの専門クラス (Phase 3)
 * - **GeometryRenderer**: ジオメトリ作成・エンティティ管理の専門クラス (Phase 4)
 * - **Phase 5**: 完全オーケストレーション化・性能最適化済み
 * 
 * **責任範囲**:
 * - 描画プロセスの統制・調整
 * - 各専門クラス間のデータ連携
 * - 高レベルAPIの提供・後方互換性維持
 * - エラーハンドリング・ログ管理
 */
import * as Cesium from 'cesium';
import { Logger } from '../utils/logger.js';
import { ColorCalculator } from './color/ColorCalculator.js';
import { VoxelSelector } from './selection/VoxelSelector.js';
import { AdaptiveController } from './adaptive/AdaptiveController.js';
import { GeometryRenderer } from './geometry/GeometryRenderer.js';
import { createClassifier } from '../utils/classification.js';

// v0.1.11: COLOR_MAPS moved to ColorCalculator (ADR-0009 Phase 1)
// v0.1.11: VoxelSelector added (ADR-0009 Phase 2)
// v0.1.11: AdaptiveController added (ADR-0009 Phase 3)
// v0.1.11: GeometryRenderer added (ADR-0009 Phase 4)

/**
 * VoxelRenderer - 3D voxel rendering orchestration class.
 * 3Dボクセル描画オーケストレーションクラス。
 * 
 * v0.1.11: Refactored for Single Responsibility Principle (ADR-0009).
 * Now serves as orchestrator delegating specialized tasks to:
 * ColorCalculator, VoxelSelector, AdaptiveController, and GeometryRenderer.
 * 
 * 各専門クラスに特化タスクを委譲するオーケストレーション役に特化。
 */
export class VoxelRenderer {
  /**
   * Constructor - Initialize VoxelRenderer orchestration system.
   * VoxelRendererオーケストレーションシステムを初期化します。
   * 
   * v0.1.11: Instantiates specialized classes for delegation:
   * - VoxelSelector: Voxel selection strategies (density, coverage, hybrid)
   * - AdaptiveController: Adaptive parameter calculation and preset logic  
   * - GeometryRenderer: Entity creation and management
   * - ColorCalculator: Used statically for color computation
   * 
   * 各専門クラスをインスタンス化し、委譲体制を構築:
   * - VoxelSelector: ボクセル選択戦略(密度・カバレッジ・ハイブリッド)
   * - AdaptiveController: 適応パラメータ計算・プリセットロジック
   * - GeometryRenderer: エンティティ作成・管理
   * - ColorCalculator: 色計算用の静的利用
   * 
   * @param {Cesium.Viewer} viewer - CesiumJS Viewer instance / CesiumJS Viewerインスタンス
   * @param {Object} options - Rendering options / 描画オプション
   * @param {Array} [options.minColor=[0,0,255]] - Minimum density color (RGB) / 最小密度色
   * @param {Array} [options.maxColor=[255,0,0]] - Maximum density color (RGB) / 最大密度色
   * @param {number} [options.opacity=0.8] - Base opacity / 基本透明度
   * @param {boolean} [options.showOutline=true] - Show voxel outlines / ボクセル枠線表示
   * @param {string} [options.voxelSelectionStrategy='density'] - Selection strategy / 選択戦略
   * @param {boolean} [options.adaptiveOutlines=false] - Enable adaptive outline control / 適応枠線制御
   */
  constructor(viewer, options = {}) {
    this.viewer = viewer;
    this.options = {
      minColor: [0, 0, 255],
      maxColor: [255, 0, 0],
      opacity: 0.8,
      emptyOpacity: 0.03,
      showOutline: true,
      showEmptyVoxels: false,
      wireframeOnly: false,    // 枠線のみ表示
      heightBased: false,      // 高さベース表現
      outlineWidth: 2,         // 枠線の太さ
      // v0.1.6.1: インセット枠線のデフォルト値
      outlineInset: 0,         // インセット枠線オフセット(メートル)
      outlineInsetMode: 'all', // インセット枠線適用範囲
      // v0.1.7: 新オプション
      outlineRenderMode: 'standard',
      emulationScope: 'off', // v0.1.12: emulation scope control
      adaptiveOutlines: false,
      outlineWidthPreset: 'medium', // v0.1.12: updated default
      ...options
    };
    if (!this.options.classification || typeof this.options.classification !== 'object') {
      this.options.classification = { enabled: false };
    }
    
    // v0.1.11-alpha: VoxelSelector instantiation (ADR-0009 Phase 2)
    this.voxelSelector = new VoxelSelector(this.options);
    this._selectionStats = null;
    
    // v0.1.11-alpha: AdaptiveController instantiation (ADR-0009 Phase 3)
    this.adaptiveController = new AdaptiveController(this.options);
    
    // v0.1.11-alpha: GeometryRenderer instantiation (ADR-0009 Phase 4)
    this.geometryRenderer = new GeometryRenderer(this.viewer, this.options);
    
    // Legacy compatibility: voxelEntities now delegates to GeometryRenderer
    Object.defineProperty(this, 'voxelEntities', {
      get: () => this.geometryRenderer.entities,
      enumerable: true,
      configurable: true
    });

    this._currentVoxelData = null;
    this._classifier = null;

    Logger.debug('VoxelRenderer initialized with options:', this.options);
  }

  /**
   * Compute adaptive outline parameters (v0.1.11).
   * 適応的枠線パラメータを計算 (v0.1.11-alpha)。
   * v0.1.11: AdaptiveControllerに委譲 (ADR-0009 Phase 3)
   * @param {Object} voxelInfo - Voxel info / ボクセル情報
   * @param {boolean} isTopN - Whether it is TopN / TopNボクセルかどうか
   * @param {Map} voxelData - All voxel data / 全ボクセルデータ
   * @param {Object} statistics - Statistics / 統計情報
  * @returns {Object} Adaptive params / 適応的パラメータ
  * @private
  */
  _calculateAdaptiveParams(voxelInfo, isTopN, voxelData, statistics, grid) {
    // v0.1.11: 新しいAdaptiveControllerに委譲しつつ、既存インターフェースを維持 (ADR-0009 Phase 3)
    return this.adaptiveController.calculateAdaptiveParams(
      voxelInfo,
      isTopN,
      voxelData,
      statistics,
      this.options,
      grid,
      this._classifier
    );
  }

  /**
   * Backward-compatible inset outline decision API.
   * 後方互換のためのインセット枠線適用判定メソッド。
   * v0.1.11: GeometryRenderer に委譲 (ADR-0009 Phase 4)
   * @param {boolean} isTopN
   * @returns {boolean}
   * @private
   */
  _shouldApplyInsetOutline(isTopN) {
    return this.geometryRenderer.shouldApplyInsetOutline(isTopN);
  }

  /**
   * Render voxel data - Orchestrated rendering process.
   * ボクセルデータ描画 - オーケストレーション化された描画プロセス。
   * 
   * v0.1.11: Fully orchestrated implementation (ADR-0009 Phase 5):
   * 
   * **Process Flow**:
   * 1. **GeometryRenderer.clear()** - Clear existing entities
   * 2. **VoxelSelector.selectVoxels()** - Apply selection strategy if needed
   * 3. **For each voxel**: Delegate to `_renderSingleVoxel()` for orchestration:
   *    - **AdaptiveController** - Calculate adaptive parameters
   *    - **ColorCalculator** - Compute colors based on density  
   *    - **GeometryRenderer** - Create voxel box, outlines, and polylines
   * 4. **Return count** - Number of successfully rendered voxels
   * 
   * **実行フロー**:
   * 1. **GeometryRenderer.clear()** - 既存エンティティのクリア
   * 2. **VoxelSelector.selectVoxels()** - 必要に応じて選択戦略適用
   * 3. **各ボクセル**: `_renderSingleVoxel()` へのオーケストレーション委譲:
   *    - **AdaptiveController** - 適応パラメータ計算
   *    - **ColorCalculator** - 密度ベース色計算
   *    - **GeometryRenderer** - ボクセルボックス・枠線・ポリライン作成
   * 4. **カウント返却** - 正常描画されたボクセル数
   * 
   * @param {Map} voxelData - Voxel data map / ボクセルデータマップ
   * @param {Object} bounds - Spatial bounds / 空間境界
   * @param {Object} grid - Grid configuration / グリッド設定
   * @param {Object} statistics - Density statistics / 密度統計
   * @returns {number} Number of rendered voxels / 実際に描画されたボクセル数
   */
  render(voxelData, bounds, grid, statistics) {
    // v0.1.11: GeometryRendererに委譲してエンティティクリア (ADR-0009 Phase 4)
    this.geometryRenderer.clear();
    Logger.debug('VoxelRenderer.render - Starting render with simplified approach', {
      voxelDataSize: voxelData.size,
      bounds,
      grid,
      statistics
    });

    this._currentVoxelData = voxelData;
    this._prepareClassifier(statistics);

    // バウンディングボックスのデバッグ表示制御(v0.1.5: debug.showBounds対応)
    const shouldShowBounds = this._shouldShowBounds();
    if (shouldShowBounds) {
      this.geometryRenderer.renderBoundingBox(bounds);
    }

    // 表示するボクセルのリスト
    let displayVoxels = [];
    const topNVoxels = new Set(); // v0.1.5: TopN強調表示用

    // 空ボクセルのフィルタリング
    if (this.options.showEmptyVoxels) {
      // 全ボクセルを生成(これは上限値が大きいとメモリ消費とパフォーマンスに影響する)
      const maxVoxels = Math.min(grid.totalVoxels, this.options.maxRenderVoxels || 10000);
      Logger.debug(`Generating grid for up to ${maxVoxels} voxels`);
      
      // 空のボクセルも含めて全ボクセルを追加
      for (let x = 0; x < grid.numVoxelsX; x++) {
        for (let y = 0; y < grid.numVoxelsY; y++) {
          for (let z = 0; z < grid.numVoxelsZ; z++) {
            const voxelKey = `${x},${y},${z}`;
            const voxelInfo = voxelData.get(voxelKey) || { x, y, z, count: 0 };
            
            displayVoxels.push({
              key: voxelKey,
              info: voxelInfo
            });
            
            if (displayVoxels.length >= maxVoxels) {
              Logger.debug(`Reached maximum voxel limit of ${maxVoxels}`);
              break;
            }
          }
          if (displayVoxels.length >= maxVoxels) break;
        }
        if (displayVoxels.length >= maxVoxels) break;
      }
    } else {
      // データがあるボクセルのみ表示
      displayVoxels = Array.from(voxelData.entries()).map(([key, info]) => {
        return { key, info };
      });
      
      // v0.1.9: 適応的レンダリング制限の適用
      if (this.options.maxRenderVoxels && displayVoxels.length > this.options.maxRenderVoxels) {
        const selectionResult = this._selectVoxelsForRendering(displayVoxels, this.options.maxRenderVoxels, bounds, grid);
        displayVoxels = selectionResult.selectedVoxels;
        
        // 統計情報の更新
        this._selectionStats = {
          strategy: selectionResult.strategy,
          clippedNonEmpty: selectionResult.clippedNonEmpty,
          coverageRatio: selectionResult.coverageRatio || 0
        };
        
        Logger.debug(`Applied ${selectionResult.strategy} strategy: ${displayVoxels.length} voxels selected, ${selectionResult.clippedNonEmpty} clipped`);
      }
    }

    // v0.1.5: TopN強調表示の前処理
    if (this.options.highlightTopN && this.options.highlightTopN > 0) {
      const sortedForTopN = [...displayVoxels].sort((a, b) => b.info.count - a.info.count);
      const topN = sortedForTopN.slice(0, this.options.highlightTopN);
      topN.forEach(voxel => topNVoxels.add(voxel.key));
      Logger.debug(`TopN highlight enabled: ${topNVoxels.size} voxels will be highlighted`);
    }
    
    Logger.debug(`Rendering ${displayVoxels.length} voxels`);
    
    // レンダリングカウント
    let renderedCount = 0;

    // 実際にボクセルを描画
    // パフォーマンス最適化: Resolver用の一時オブジェクトを再利用してGCを削減
    const reusableVoxelCtx = { x: 0, y: 0, z: 0, count: 0 };
    const reusableWidthResolverParams = { voxel: reusableVoxelCtx, isTopN: false, normalizedDensity: 0, statistics, adaptiveParams: null };
    const reusableOpacityResolverCtx = { voxel: reusableVoxelCtx, isTopN: false, normalizedDensity: 0, statistics, adaptiveParams: null };

    displayVoxels.forEach(({ key, info }) => {
      try {
        renderedCount += this._renderSingleVoxel(key, info, bounds, grid, statistics, topNVoxels, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx);
      } catch (error) {
        Logger.warn('Error rendering voxel:', error);
      }
    });

    Logger.info(`Successfully rendered ${renderedCount} voxels`);

    this._currentVoxelData = null;

    // 実際に描画されたボクセル数を返す
    return renderedCount;
  }

  // v0.1.11: _renderBoundingBox/_addEdgePolylines moved to GeometryRenderer (ADR-0009 Phase 4)

  /**
   * Render a single voxel with all visual configurations.
   * 単一ボクセルを全ての視覚設定で描画します。
   * v0.1.11: Phase5 オーケストレーション最適化 (ADR-0009 Phase 5)
   * @param {string} key - Voxel key / ボクセルキー
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {Object} bounds - Bounds / 境界
   * @param {Object} grid - Grid / グリッド
   * @param {Object} statistics - Statistics / 統計
   * @param {Set} topNVoxels - TopN voxel keys / TopNボクセルキー
   * @param {Object} reusableVoxelCtx - Reusable context for performance / 再利用コンテキスト
   * @param {Object} reusableWidthResolverParams - Reusable width resolver params / 再利用太さResolver
   * @param {Object} reusableOpacityResolverCtx - Reusable opacity resolver context / 再利用透明度Resolver
   * @returns {number} 1 if rendered successfully, 0 if skipped / 描画成功時1、スキップ時0
   * @private
   */
  _renderSingleVoxel(key, info, bounds, grid, statistics, topNVoxels, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx) {
    const isTopN = topNVoxels.has(key);
    
    // Calculate voxel rendering parameters
    const renderParams = this._calculateVoxelRenderingParams(info, bounds, grid, statistics, isTopN, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx);
    
    // 安全性チェック - レンダリングパラメータが無効な場合はスキップ
    if (!renderParams) {
      return 0; // Skipped rendering due to invalid parameters
    }
    
    // Delegate to GeometryRenderer for actual rendering
    this._delegateVoxelRendering(key, renderParams);
    
    return 1; // Successfully rendered
  }

  /**
   * Calculate all rendering parameters for a voxel.
   * ボクセルの全描画パラメータを計算します。
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {Object} bounds - Bounds / 境界
   * @param {Object} grid - Grid / グリッド
   * @param {Object} statistics - Statistics / 統計
   * @param {boolean} isTopN - Is TopN voxel / TopNボクセルか
   * @param {Object} reusableVoxelCtx - Reusable context / 再利用コンテキスト
   * @param {Object} reusableWidthResolverParams - Width resolver params / 太さResolver
   * @param {Object} reusableOpacityResolverCtx - Opacity resolver context / 透明度Resolver
   * @returns {Object} Complete rendering parameters / 完全な描画パラメータ
   * @private
   */
  _calculateVoxelRenderingParams(info, bounds, grid, statistics, isTopN, reusableVoxelCtx, reusableWidthResolverParams, reusableOpacityResolverCtx) {
    // 引数の安全性チェック
    if (!info || !bounds || !grid || !statistics) {
      return null;
    }
    
    // v0.1.17: Spatial ID mode - calculate position from 8-vertex bounds / 空間IDモード - 8頂点boundsから位置を計算
    let centerLon, centerLat, centerAlt, cellSizeX, cellSizeY, baseCellSizeZ;
    
    if (info.bounds && Array.isArray(info.bounds) && info.bounds.length === 8) {
      // Spatial ID mode: use 8 vertices to calculate center and dimensions
      // 空間IDモード: 8頂点を使って中心と寸法を計算
      const vertices = info.bounds;
      
      // Calculate center from vertices (average of all 8 points)
      // 頂点から中心を計算(8点の平均)
      centerLon = vertices.reduce((sum, v) => sum + v.lng, 0) / 8;
      centerLat = vertices.reduce((sum, v) => sum + v.lat, 0) / 8;
      centerAlt = vertices.reduce((sum, v) => sum + v.alt, 0) / 8;
      
      // Calculate dimensions from vertices (distance between corners)
      // 頂点から寸法を計算(角間の距離)
      // Assume vertices[0-3] are bottom face, vertices[4-7] are top face
      // vertices[0], [1], [2], [3] form bottom rectangle
      // SpatialIdAdapter/ZFXYConverter guarantee this ordering.
      const v0 = Cesium.Cartesian3.fromDegrees(vertices[0].lng, vertices[0].lat, vertices[0].alt);
      const v1 = Cesium.Cartesian3.fromDegrees(vertices[1].lng, vertices[1].lat, vertices[1].alt);
      const v3 = Cesium.Cartesian3.fromDegrees(vertices[3].lng, vertices[3].lat, vertices[3].alt);
      const v4 = Cesium.Cartesian3.fromDegrees(vertices[4].lng, vertices[4].lat, vertices[4].alt);
      
      cellSizeX = Cesium.Cartesian3.distance(v0, v1);
      cellSizeY = Cesium.Cartesian3.distance(v0, v3);
      baseCellSizeZ = Cesium.Cartesian3.distance(v0, v4);
    } else {
      // Uniform grid mode: use x/y/z indices (legacy)
      // 一様グリッドモード: x/y/zインデックスを使用(レガシー)
      const { x, y, z } = info;
      
      centerLon = bounds.minLon + (x + 0.5) * (bounds.maxLon - bounds.minLon) / grid.numVoxelsX;
      centerLat = bounds.minLat + (y + 0.5) * (bounds.maxLat - bounds.minLat) / grid.numVoxelsY;
      centerAlt = bounds.minAlt + (z + 0.5) * (bounds.maxAlt - bounds.minAlt) / grid.numVoxelsZ;
      
      // Calculate dimensions from grid (legacy)
      // グリッドから寸法を計算(レガシー)
      cellSizeX = grid.cellSizeX || (grid.lonRangeMeters ? (grid.lonRangeMeters / grid.numVoxelsX) : grid.voxelSizeMeters);
      cellSizeY = grid.cellSizeY || (grid.latRangeMeters ? (grid.latRangeMeters / grid.numVoxelsY) : grid.voxelSizeMeters);
      baseCellSizeZ = grid.cellSizeZ || (grid.altRangeMeters ? Math.max(grid.altRangeMeters / Math.max(grid.numVoxelsZ, 1), 1) : Math.max(grid.voxelSizeMeters, 1));
    }
    
    // Normalized density
    const normalizedDensity = statistics.maxCount > statistics.minCount ? 
      (info.count - statistics.minCount) / (statistics.maxCount - statistics.minCount) : 0;
    
    // Adaptive parameters
    const adaptiveParams = this._calculateAdaptiveParams(info, isTopN, this._currentVoxelData, statistics, grid);
    
    // Color and opacity
    const { color, opacity } = this._calculateColorAndOpacity(info, normalizedDensity, isTopN, adaptiveParams, statistics, reusableVoxelCtx, reusableOpacityResolverCtx);
    
    // Dimensions (v0.1.17: use pre-calculated dimensions for spatial ID mode)
    let finalCellSizeX, finalCellSizeY, boxHeight;
    if (info.bounds) {
      // Spatial ID mode: use pre-calculated dimensions, apply voxel gap
      // 空間IDモード: 事前計算された寸法を使用、voxelGapを適用
      finalCellSizeX = cellSizeX;
      finalCellSizeY = cellSizeY;
      let finalBaseCellSizeZ = baseCellSizeZ;
      
      if (this.options.voxelGap > 0) {
        finalCellSizeX = Math.max(cellSizeX - this.options.voxelGap, cellSizeX * 0.1);
        finalCellSizeY = Math.max(cellSizeY - this.options.voxelGap, cellSizeY * 0.1);
        finalBaseCellSizeZ = Math.max(baseCellSizeZ - this.options.voxelGap, baseCellSizeZ * 0.1);
      }
      
      boxHeight = finalBaseCellSizeZ;
      if (this.options.heightBased) {
        boxHeight = finalBaseCellSizeZ * (0.1 + normalizedDensity * 0.9);
      }
    } else {
      // Uniform grid mode: calculate dimensions from grid
      // 一様グリッドモード: グリッドから寸法を計算
      const dimensions = this._calculateDimensions(grid, normalizedDensity);
      finalCellSizeX = dimensions.cellSizeX;
      finalCellSizeY = dimensions.cellSizeY;
      boxHeight = dimensions.boxHeight;
    }
    
    // Outline properties
    const outlineProps = this._calculateOutlineProperties(info, isTopN, normalizedDensity, adaptiveParams, statistics, color, reusableVoxelCtx, reusableWidthResolverParams);
    
    return {
      centerLon, centerLat, centerAlt,
      cellSizeX: finalCellSizeX, cellSizeY: finalCellSizeY, boxHeight,
      color, opacity,
      ...outlineProps,
      voxelInfo: info,
      isTopN,
      adaptiveParams
    };
  }

  /**
   * Calculate color and opacity for a voxel.
   * ボクセルの色と透明度を計算します。
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {number} normalizedDensity - Normalized density / 正規化密度
   * @param {boolean} isTopN - Is TopN voxel / TopNボクセルか
   * @param {Object} adaptiveParams - Adaptive params / 適応パラメータ
   * @param {Object} statistics - Statistics / 統計
   * @param {Object} reusableVoxelCtx - Reusable context / 再利用コンテキスト
   * @param {Object} reusableOpacityResolverCtx - Opacity resolver context / 透明度Resolverコンテキスト
   * @returns {Object} Color and opacity / 色と透明度
   * @private
   */
  _calculateColorAndOpacity(info, normalizedDensity, isTopN, adaptiveParams, statistics, reusableVoxelCtx, reusableOpacityResolverCtx) {
    let color, opacity;
    
    if (info.count === 0) {
      color = Cesium.Color.LIGHTGRAY;
      opacity = this.options.emptyOpacity;
    } else {
      const classificationTargets = this.options.classification?.classificationTargets || {};
      const shouldApplyClassificationColor = this._classifier && classificationTargets.color !== false;
      if (shouldApplyClassificationColor) {
        try {
          const classIndex = this._classifier.classify(info.count ?? 0);
          color = this._classifier.getColorForClass(classIndex);
        } catch (error) {
          Logger.warn('Classification color calculation failed, falling back to legacy color:', error);
          color = ColorCalculator.calculateColor(normalizedDensity, info.count, this.options);
        }
      } else {
        color = ColorCalculator.calculateColor(normalizedDensity, info.count, this.options);
      }
      
      // Opacity calculation with resolver support
      if (this.options.boxOpacityResolver && typeof this.options.boxOpacityResolver === 'function') {
        reusableVoxelCtx.x = info.x; reusableVoxelCtx.y = info.y; reusableVoxelCtx.z = info.z; reusableVoxelCtx.count = info.count;
        reusableOpacityResolverCtx.isTopN = isTopN;
        reusableOpacityResolverCtx.normalizedDensity = normalizedDensity;
        reusableOpacityResolverCtx.adaptiveParams = adaptiveParams;
        try {
          const resolverOpacity = this.options.boxOpacityResolver(reusableOpacityResolverCtx);
          opacity = isNaN(resolverOpacity) ? this.options.opacity : Math.max(0, Math.min(1, resolverOpacity));
        } catch (e) {
          Logger.warn('boxOpacityResolver error, using fallback:', e);
          opacity = (adaptiveParams.boxOpacity ?? this.options.opacity);
        }
      } else {
        opacity = (adaptiveParams.boxOpacity ?? this.options.opacity);
      }
      
      // TopN highlight adjustment 
      if (this.options.highlightTopN && !isTopN) {
        opacity *= (1 - (this.options.highlightStyle?.boostOpacity || 0.2));
      }
    }
    
    return { color, opacity };
  }

  /**
   * Calculate voxel dimensions with gap support.
   * voxelGap対応のボクセル寸法を計算します。
   * @param {Object} grid - Grid info / グリッド情報
   * @param {number} normalizedDensity - Normalized density / 正規化密度
   * @returns {Object} Dimensions / 寸法
   * @private
   */
  _calculateDimensions(grid, normalizedDensity) {
    let cellSizeX = grid.cellSizeX || (grid.lonRangeMeters ? (grid.lonRangeMeters / grid.numVoxelsX) : grid.voxelSizeMeters);
    let cellSizeY = grid.cellSizeY || (grid.latRangeMeters ? (grid.latRangeMeters / grid.numVoxelsY) : grid.voxelSizeMeters);
    let baseCellSizeZ = grid.cellSizeZ || (grid.altRangeMeters ? Math.max(grid.altRangeMeters / Math.max(grid.numVoxelsZ, 1), 1) : Math.max(grid.voxelSizeMeters, 1));

    // Apply voxel gap
    if (this.options.voxelGap > 0) {
      cellSizeX = Math.max(cellSizeX - this.options.voxelGap, cellSizeX * 0.1);
      cellSizeY = Math.max(cellSizeY - this.options.voxelGap, cellSizeY * 0.1);
      baseCellSizeZ = Math.max(baseCellSizeZ - this.options.voxelGap, baseCellSizeZ * 0.1);
    }

    // Height-based scaling
    let boxHeight = baseCellSizeZ;
    if (this.options.heightBased) {
      boxHeight = baseCellSizeZ * (0.1 + normalizedDensity * 0.9);
    }
    
    return { cellSizeX, cellSizeY, boxHeight };
  }

  /**
   * Calculate outline properties with resolver support.
   * Resolver対応の枠線プロパティを計算します。
   * @param {Object} info - Voxel info / ボクセル情報
   * @param {boolean} isTopN - Is TopN voxel / TopNボクセルか
   * @param {number} normalizedDensity - Normalized density / 正規化密度
   * @param {Object} adaptiveParams - Adaptive params / 適応パラメータ
   * @param {Object} statistics - Statistics / 統計
   * @param {Cesium.Color} color - Base color / ベース色
   * @param {Object} reusableVoxelCtx - Reusable context / 再利用コンテキスト
   * @param {Object} reusableWidthResolverParams - Width resolver params / 太さResolverパラメータ
   * @returns {Object} Outline properties / 枠線プロパティ
   * @private
   */
  _calculateOutlineProperties(info, isTopN, normalizedDensity, adaptiveParams, statistics, color, reusableVoxelCtx, reusableWidthResolverParams) {
    // Outline width calculation
    let finalOutlineWidth;
    if (this.options.outlineWidthResolver && typeof this.options.outlineWidthResolver === 'function') {
      reusableVoxelCtx.x = info.x; reusableVoxelCtx.y = info.y; reusableVoxelCtx.z = info.z; reusableVoxelCtx.count = info.count;
      reusableWidthResolverParams.isTopN = isTopN;
      reusableWidthResolverParams.normalizedDensity = normalizedDensity;
      reusableWidthResolverParams.adaptiveParams = adaptiveParams;
      try {
        finalOutlineWidth = this.options.outlineWidthResolver(reusableWidthResolverParams);
        if (isNaN(finalOutlineWidth)) {
          finalOutlineWidth = adaptiveParams.outlineWidth || this.options.outlineWidth;
        }
      } catch (e) {
        Logger.warn('outlineWidthResolver error, using fallback:', e);
        finalOutlineWidth = adaptiveParams.outlineWidth || this.options.outlineWidth;
      }
    } else {
      if (adaptiveParams.outlineWidth !== null && adaptiveParams.outlineWidth !== undefined) {
        finalOutlineWidth = adaptiveParams.outlineWidth;
      } else {
        finalOutlineWidth = isTopN && this.options.highlightTopN ? 
          (this.options.highlightStyle?.outlineWidth || this.options.outlineWidth) : 
          this.options.outlineWidth;
      }
    }

    // Outline opacity
    const finalOutlineOpacity = adaptiveParams.outlineOpacity ?? (this.options.outlineOpacity ?? 1.0);
    const outlineColorWithOpacity = color.withAlpha(finalOutlineOpacity);

    // Render mode configuration
    const renderModeConfig = this._determineRenderModeConfig();

    // Emulation logic
    let emulateThickForThis = renderModeConfig.shouldUseEmulationOnly;
    if (!renderModeConfig.shouldUseEmulationOnly) {
      // v0.1.12: Use new emulationScope instead of deprecated outlineEmulation
      const scope = this.options.emulationScope || 'off';
      if (scope === 'topn') {
        emulateThickForThis = isTopN && (finalOutlineWidth || 1) > 1;
      } else if (scope === 'non-topn') {
        emulateThickForThis = !isTopN && (finalOutlineWidth || 1) > 1;
      } else if (scope === 'all') {
        emulateThickForThis = (finalOutlineWidth || 1) > 1;
      } else if (this.options.adaptiveOutlines && adaptiveParams.shouldUseEmulation) {
        // Safety: when scope is explicitly 'off', do not enable emulation from adaptive control
        emulateThickForThis = scope !== 'off';
      }
    }

    return {
      shouldShowOutline: renderModeConfig.shouldShowStandardOutline,
      outlineColor: outlineColorWithOpacity,
      outlineWidth: finalOutlineWidth || 1,
      shouldShowInsetOutline: renderModeConfig.shouldShowInsetOutline,
      emulateThick: emulateThickForThis
    };
  }

  /**
   * Determine render mode configuration.
   * レンダーモード設定を決定します。
   * @returns {Object} Render mode config / レンダーモード設定
   * @private
   */
  _determineRenderModeConfig() {
    let shouldShowStandardOutline = true;
    let shouldShowInsetOutline = false;
    let shouldUseEmulationOnly = false;
    
    switch (this.options.outlineRenderMode) {
      case 'standard':
        shouldShowStandardOutline = this.options.showOutline;
        shouldShowInsetOutline = this.options.outlineInset > 0;
        break;
      case 'inset':
        shouldShowStandardOutline = false;
        shouldShowInsetOutline = true;
        break;
      case 'emulation-only':
        shouldShowStandardOutline = false;
        shouldShowInsetOutline = false;
        shouldUseEmulationOnly = true;
        break;
    }
    
    return { shouldShowStandardOutline, shouldShowInsetOutline, shouldUseEmulationOnly };
  }

  /**
   * Delegate voxel rendering to GeometryRenderer.
   * ボクセル描画をGeometryRendererに委譲します。
   * @param {string} key - Voxel key / ボクセルキー
   * @param {Object} params - Rendering parameters / 描画パラメータ
   * @private
   */
  _delegateVoxelRendering(key, params) {
    // Main voxel box
    this.geometryRenderer.createVoxelBox({
      centerLon: params.centerLon, centerLat: params.centerLat, centerAlt: params.centerAlt,
      cellSizeX: params.cellSizeX, cellSizeY: params.cellSizeY, boxHeight: params.boxHeight,
      color: params.color, opacity: params.opacity,
      shouldShowOutline: params.shouldShowOutline,
      outlineColor: params.outlineColor,
      outlineWidth: params.outlineWidth,
      voxelInfo: params.voxelInfo,
      voxelKey: key,
      emulateThick: params.emulateThick
    });

    // Inset outline
    if (params.shouldShowInsetOutline && this.geometryRenderer.shouldApplyInsetOutline(params.isTopN)) {
      try {
        const insetAmount = this.options.outlineInset > 0 ? this.options.outlineInset : 1;
        this.geometryRenderer.createInsetOutline({
          centerLon: params.centerLon, centerLat: params.centerLat, centerAlt: params.centerAlt,
          baseSizeX: params.cellSizeX, baseSizeY: params.cellSizeY, baseSizeZ: params.boxHeight,
          outlineColor: params.outlineColor,
          outlineWidth: Math.max(params.outlineWidth, 1),
          voxelKey: key,
          insetAmount
        });
      } catch (e) {
        Logger.warn('Failed to create inset outline:', e);
      }
    }
    
    // Edge polylines for thick emulation
    // 追加の安全ガード: emulation-only もしくは emulationScope!='off' の場合のみ許可
    const allowEmulationEdges = (this.options.outlineRenderMode === 'emulation-only') ||
      (this.options.emulationScope && this.options.emulationScope !== 'off');
    if (allowEmulationEdges && params.emulateThick) {
      try {
        const adaptiveOutlineOpacity = params.adaptiveParams?.outlineOpacity ?? params.outlineColor?.alpha ?? null;
        this.geometryRenderer.createEdgePolylines({
          centerLon: params.centerLon, centerLat: params.centerLat, centerAlt: params.centerAlt,
          cellSizeX: params.cellSizeX, cellSizeY: params.cellSizeY, boxHeight: params.boxHeight,
          outlineColor: params.outlineColor,
          outlineWidth: Math.max(params.outlineWidth, 1),
          outlineOpacity: adaptiveOutlineOpacity,
          voxelKey: key
        });
      } catch (e) {
        Logger.warn('Failed to add emulated thick outline polylines:', e);
      }
    }
  }

  /**
   * Interpolate color based on density (v0.1.5: color maps supported).
   * 密度に基づいて色を補間(v0.1.5: カラーマップ対応)。
   * v0.1.11: ColorCalculatorに委譲 (ADR-0009 Phase 1)
   * @param {number} normalizedDensity - Normalized density (0-1) / 正規化された密度 (0-1)
   * @param {number} [rawValue] - Raw value for diverging scheme / 生値(二極性配色用)
   * @returns {Cesium.Color} Calculated color / 計算された色
   */
  interpolateColor(normalizedDensity, rawValue = null) {
    // v0.1.11: 新しいColorCalculatorに委譲
    return ColorCalculator.calculateColor(normalizedDensity, rawValue, this.options);
  }

  _prepareClassifier(statistics) {
    const classificationOptions = this.options.classification;
    if (!classificationOptions || !classificationOptions.enabled) {
      this._classifier = null;
      return;
    }

    const allowedSchemes = ['linear', 'log', 'equal-interval', 'quantize', 'threshold', 'quantile', 'jenks'];
    const scheme = (classificationOptions.scheme || 'linear').toLowerCase();
    if (!allowedSchemes.includes(scheme)) {
      Logger.warn(`Classification scheme '${scheme}' is not supported in v1.0.0. Disabling classification.`);
      this._classifier = null;
      return;
    }

    const domain = Array.isArray(classificationOptions.domain) && classificationOptions.domain.length === 2
      ? classificationOptions.domain
      : [statistics?.minCount ?? 0, statistics?.maxCount ?? 0];

    let values = null;
    if (this._currentVoxelData && this._currentVoxelData.size > 0) {
      values = [];
      for (const voxelInfo of this._currentVoxelData.values()) {
        if (voxelInfo && Number.isFinite(voxelInfo.count)) {
          values.push(voxelInfo.count);
        }
      }
      if (values.length === 0) {
        values = null;
      }
    }

    try {
      this._classifier = createClassifier({
        scheme,
        classes: classificationOptions.classes,
        thresholds: classificationOptions.thresholds,
        colorMap: classificationOptions.colorMap,
        domain,
        values
      });
    } catch (error) {
      Logger.warn('Failed to initialize classifier:', error);
      this._classifier = null;
    }
  }
  
  // v0.1.11: _interpolateFromColorMap and _interpolateDivergingColor methods 
  // moved to ColorCalculator (ADR-0009 Phase 1)


  /**
   * Remove all rendered entities from the scene.
   * 描画されたエンティティを全てクリアします。
   * v0.1.11: GeometryRendererに委譲 (ADR-0009 Phase 4)
   */
  clear() {
    this.geometryRenderer.clear();
  }

  /**
   * デバッグ境界ボックス表示の判定(v0.1.5: debug.showBounds対応)
   * @returns {boolean} 境界ボックスを表示する場合はtrue
   * @private
   */
  _shouldShowBounds() {
    if (!this.options.debug) {
      return false;
    }
    
    if (typeof this.options.debug === 'boolean') {
      // 従来の動作:debugがtrueの場合はバウンディングボックス表示
      return this.options.debug;
    }
    
    if (typeof this.options.debug === 'object' && this.options.debug !== null) {
      // 新しい動作:debug.showBoundsで明示的に制御
      return this.options.debug.showBounds === true;
    }
    
    return false;
  }


  // v0.1.11: _createInsetOutline moved to GeometryRenderer (ADR-0009 Phase 4)

  // Thick outline frame creation is fully handled by GeometryRenderer.

  /**
   * Toggle visibility.
   * 表示/非表示を切り替えます。
   * v0.1.11: GeometryRendererに委譲 (ADR-0009 Phase 5)
   * @param {boolean} show - true to show / 表示する場合は true
   */
  setVisible(show) {
    Logger.debug('VoxelRenderer.setVisible:', show);
    this.voxelEntities.forEach(entity => {
      if (entity && (!entity.isDestroyed || !entity.isDestroyed())) {
        entity.show = show;
      }
    });
  }

  /**
   * Select voxels for rendering based on the specified strategy.
   * 指定された戦略に基づいてレンダリング用ボクセルを選択します。
   * @param {Array} allVoxels - All available voxels / 利用可能な全ボクセル
   * @param {number} maxCount - Maximum number of voxels to select / 選択する最大ボクセル数
   * @param {Object} bounds - Data bounds / データ境界
   * @returns {Object} Selection result / 選択結果
   * @private
   */
  _selectVoxelsForRendering(allVoxels, maxCount, bounds, grid) {
    // v0.1.11: 新しいVoxelSelectorに委譲しつつ、既存インターフェースを維持 (ADR-0009 Phase 2)
    const selectionResult = this.voxelSelector.selectVoxels(allVoxels, maxCount, { grid, bounds });
    
    // 統計情報の更新
    this._selectionStats = this.voxelSelector.getLastSelectionStats();
    
    return selectionResult;
  }




  /**
   * Get selection statistics.
   * 選択統計を取得します。
   * @returns {Object|null} Selection statistics / 選択統計
   */
  getSelectionStats() {
    return this._selectionStats || null;
  }
}