core_adaptive_AdaptiveController.js - hiro-nyon/cesium-heatbox GitHub Wiki
Source: core/adaptive/AdaptiveController.js
日本語 | English
English
See also: Class: AdaptiveController
import { Logger } from '../../utils/logger.js';
import { DEFAULT_OPTIONS } from '../../utils/constants.js';
const DEFAULT_ADAPTIVE_PARAMS = DEFAULT_OPTIONS.adaptiveParams || {};
/**
* AdaptiveController - Adaptive outline logic delegated from VoxelRenderer.
* 適応的制御ロジック - ボクセルレンダラーから委譲されるアウトライン制御を担当
*
* Responsibilities:
* - 近傍密度計算 (Neighborhood density calculation)
* - プリセット適用ロジック (Preset application logic)
* - 適応的パラメータ計算 (Adaptive parameter calculation)
* - Z軸スケール補正と重なり検出の推奨提示 (Z scale compensation & overlap recommendations)
*
* ADR-0009 Phase 3 + ADR-0011 Phase 4
* @version 0.1.15
*/
export class AdaptiveController {
/**
* AdaptiveController constructor
* @param {Object} options - Adaptive control options / 適応制御オプション
* @param {Object} options.adaptiveParams - Adaptive parameters / 適応パラメータ
* Properties: `neighborhoodRadius`, `densityThreshold`, `cameraDistanceFactor`,
* `overlapRiskFactor`, `outlineWidthRange`, `boxOpacityRange`,
* `outlineOpacityRange`, `zScaleCompensation`, `overlapDetection`
* / プロパティ: 近傍探索半径・密集判定閾値・カメラ距離係数・重なりリスク係数・
* 枠線太さ範囲・ボックス不透明度範囲・枠線不透明度範囲・Z軸補正フラグ・重なり検出フラグ
*/
constructor(options = {}) {
const mergedAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(options.adaptiveParams || {})
};
this.options = {
...options,
adaptiveParams: mergedAdaptiveParams
};
Logger.debug('AdaptiveController initialized with options:', this.options);
}
/**
* Calculate neighborhood density around a voxel
* ボクセル周辺の近傍密度を計算
*
* @param {Object} voxelInfo - Target voxel information (`x`, `y`, `z` number) / 対象ボクセル情報(`x`・`y`・`z` は数値)
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @param {number} [radius] - Search radius override / 探索半径オーバーライド
* @param {Object} [renderOptions] - Live render options snapshot / 現在の描画オプション
* @returns {Object} Neighborhood density result / 近傍密度結果
*/
calculateNeighborhoodDensity(voxelInfo, voxelData, radius = null, renderOptions = null) {
// voxelDataのnull/undefined安全性チェック
if (!voxelData || typeof voxelData.get !== 'function') {
return {
isDenseArea: false,
neighborhoodDensity: 0,
neighborCount: 0
};
}
const { x, y, z } = voxelInfo;
const controllerAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(this.options?.adaptiveParams || {}),
...(renderOptions?.adaptiveParams || {})
};
const effectiveRadius = controllerAdaptiveParams.neighborhoodRadius ?? DEFAULT_ADAPTIVE_PARAMS.neighborhoodRadius ?? 30;
const searchRadius = radius !== null ? radius :
Math.max(1, Math.floor(effectiveRadius / 20)); // 簡略化
let neighborhoodDensity = 0;
let neighborCount = 0;
for (let dx = -searchRadius; dx <= searchRadius; dx++) {
for (let dy = -searchRadius; dy <= searchRadius; dy++) {
for (let dz = -searchRadius; dz <= searchRadius; dz++) {
if (dx === 0 && dy === 0 && dz === 0) continue;
const neighborKey = `${x + dx},${y + dy},${z + dz}`;
const neighbor = voxelData.get(neighborKey);
if (neighbor) {
neighborhoodDensity += neighbor.count;
neighborCount++;
}
}
}
}
const avgNeighborhoodDensity = neighborCount > 0 ? neighborhoodDensity / neighborCount : 0;
const densityThreshold = controllerAdaptiveParams.densityThreshold ?? DEFAULT_ADAPTIVE_PARAMS.densityThreshold ?? 5;
const isDenseArea = avgNeighborhoodDensity > densityThreshold;
return {
totalDensity: neighborhoodDensity,
neighborCount,
avgDensity: avgNeighborhoodDensity,
isDenseArea,
searchRadius
};
}
/**
* Calculate Z-axis scale compensation factor
* Z軸スケール補正係数を計算(v0.1.15 Phase 1 - ADR-0011)
*
* @param {Object} voxelInfo - Target voxel information / 対象ボクセル情報
* @param {Object} grid - Grid information with cellSizeX/Y/Z / グリッド情報
* @returns {number} Scale compensation factor / スケール補正係数
*/
_calculateZScaleCompensation(voxelInfo, grid) {
const controllerAdaptiveParams = this.options.adaptiveParams || DEFAULT_ADAPTIVE_PARAMS;
if (!grid || !controllerAdaptiveParams.zScaleCompensation) {
return 1.0;
}
const { cellSizeX, cellSizeY, cellSizeZ } = grid;
if (!cellSizeX || !cellSizeY || !cellSizeZ) {
return 1.0;
}
const avgHorizontalSize = (cellSizeX + cellSizeY) / 2;
const aspectRatio = cellSizeZ / avgHorizontalSize;
// Z軸が極小の場合は補正を適用
if (aspectRatio < 0.1) {
return Math.max(0.7, Math.min(1.3, 1.0 + (0.1 - aspectRatio) * 2));
}
return 1.0;
}
/**
* Count adjacent voxels (6 directions: ±X, ±Y, ±Z)
* 隣接ボクセルをカウント(6方向:±X, ±Y, ±Z)(v0.1.15 Phase 2 - ADR-0011)
*
* @param {Object} voxelInfo - Target voxel information (`x`, `y`, `z` number) / 対象ボクセル情報(`x`・`y`・`z` は数値)
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @returns {number} Number of adjacent voxels / 隣接ボクセル数
*/
_countAdjacentVoxels(voxelInfo, voxelData) {
if (!voxelInfo || !voxelData || typeof voxelData.get !== 'function') {
return 0;
}
const { x, y, z } = voxelInfo;
const adjacentDirections = [
[1, 0, 0], // +X
[-1, 0, 0], // -X
[0, 1, 0], // +Y
[0, -1, 0], // -Y
[0, 0, 1], // +Z
[0, 0, -1] // -Z
];
let adjacentCount = 0;
for (const [dx, dy, dz] of adjacentDirections) {
const neighborKey = `${x + dx},${y + dy},${z + dz}`;
if (voxelData.get(neighborKey)) {
adjacentCount++;
}
}
return adjacentCount;
}
/**
* Detect overlap and recommend rendering mode
* 隣接重なりを検出してレンダリングモードを推奨(v0.1.15 Phase 2 - ADR-0011)
*
* @param {Object} voxelInfo - Target voxel information (`x`, `y`, `z` number) / 対象ボクセル情報(`x`・`y`・`z` は数値)
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @returns {Object} Recommended rendering settings / 推奨レンダリング設定
* @returns {string} returns.recommendedMode - Recommended outline render mode / 推奨アウトライン描画モード
* @returns {number} returns.recommendedInset - Recommended inset value / 推奨インセット値
* @returns {string} [returns.reason] - Reason for recommendation / 推奨理由
*/
_detectOverlapAndRecommendMode(voxelInfo, voxelData, renderOptions = null) {
const liveOptions = renderOptions || this.options || {};
const liveAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(this.options?.adaptiveParams || {}),
...(renderOptions?.adaptiveParams || {})
};
const currentMode = liveOptions.outlineRenderMode || 'standard';
const currentInset = liveOptions.outlineInset || 0;
// overlapDetection が無効な場合は現在の設定を返す
if (!liveAdaptiveParams.overlapDetection) {
return {
recommendedMode: currentMode,
recommendedInset: currentInset
};
}
const adjacentCount = this._countAdjacentVoxels(voxelInfo, voxelData);
const overlapRisk = adjacentCount / 6; // 最大6方向
// 重なりリスクが高い場合、insetモードを推奨
if (overlapRisk > 0.5 && currentMode !== 'emulation-only') {
return {
recommendedMode: 'inset',
recommendedInset: Math.max(0.3, 0.8 - overlapRisk * 0.4),
reason: `High overlap risk (${(overlapRisk * 100).toFixed(0)}%)`
};
}
return {
recommendedMode: currentMode,
recommendedInset: currentInset
};
}
/**
* Apply preset-specific adaptive logic
* プリセット固有の適応ロジックを適用
*
* @param {string} preset - Outline width preset / アウトライン幅プリセット
* @param {boolean} isTopN - Whether it is TopN voxel / TopNボクセルかどうか
* @param {number} normalizedDensity - Normalized density [0-1] / 正規化密度 [0-1]
* @param {boolean} isDenseArea - Whether it is dense area / 密集エリアかどうか
* @param {Object} baseOptions - Base options for calculation / 計算用基準オプション
* @returns {Object} Applied preset parameters / 適用済みプリセットパラメータ
*/
applyPresetLogic(preset, isTopN, normalizedDensity, isDenseArea, baseOptions) {
let adaptiveWidth, adaptiveBoxOpacity, adaptiveOutlineOpacity;
switch (preset) {
// New names (v0.1.12)
case 'thin':
// v0.1.12-alpha.10: 最小値を1.0に設定してRangeError防止
adaptiveWidth = Math.max(1.0, baseOptions.outlineWidth * 0.8);
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 0.8;
break;
case 'medium':
adaptiveWidth = baseOptions.outlineWidth;
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 1.0;
break;
case 'thick':
adaptiveWidth = Math.max(1, baseOptions.outlineWidth * 1.5);
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 1.0;
break;
case 'adaptive':
case 'adaptive-density': {
// v0.1.15 Phase 1: より柔軟で安定した調整(ADR-0011)
// 密度に応じたベース係数(中央値を基準に調整)
const baseFactor = isDenseArea ?
Math.max(0.6, 0.8 + (normalizedDensity - 0.5) * 0.3) : 1.0; // 0.6-0.95倍(密集時)
// Z軸スケール補正を適用(有効な場合)
// 注: voxelInfoとgridはcalculateAdaptiveParams内でのみ利用可能
// ここではbaseFactor * zScaleFactorの形で後段で適用される想定
adaptiveWidth = Math.max(1.0, Math.min(baseOptions.outlineWidth * 3.0,
baseOptions.outlineWidth * baseFactor));
adaptiveBoxOpacity = isDenseArea ? baseOptions.opacity * 0.8 : baseOptions.opacity;
adaptiveOutlineOpacity = isDenseArea ? 0.6 : 1.0;
break;
}
// Legacy name (map to thick/topn focus)
case 'topn-focus':
// v0.1.12-alpha.10: 安全な値範囲でRangeError防止
adaptiveWidth = isTopN ?
Math.max(1.0, Math.min(baseOptions.outlineWidth * 3.0,
baseOptions.outlineWidth * (1.5 + normalizedDensity * 0.5))) :
Math.max(1.0, baseOptions.outlineWidth * 0.8); // 0.5→0.8で最小値を安全に
adaptiveBoxOpacity = isTopN ? baseOptions.opacity : baseOptions.opacity * 0.6;
adaptiveOutlineOpacity = isTopN ? 1.0 : 0.4;
break;
// Legacy name (uniform)
case 'uniform':
default:
adaptiveWidth = baseOptions.outlineWidth;
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 1.0;
break;
}
return {
adaptiveWidth,
adaptiveBoxOpacity,
adaptiveOutlineOpacity
};
}
/**
* Calculate adaptive parameters for a voxel
* ボクセルの適応的パラメータを計算
*
* @param {Object} voxelInfo - Voxel information / ボクセル情報
* @param {boolean} isTopN - Whether it is TopN voxel / TopNボクセルかどうか
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @param {Object} statistics - Statistics information / 統計情報
* @param {Object} renderOptions - Rendering options / 描画オプション
* @param {Object} [grid] - Grid information (optional, for Z-scale compensation) / グリッド情報(オプション、Z軸補正用)
* @returns {Object} Adaptive parameters / 適応的パラメータ
*/
calculateAdaptiveParams(voxelInfo, isTopN, voxelData, statistics, renderOptions, grid = null, classifier = null) {
// 引数の安全性チェック
if (!voxelInfo || !statistics || !renderOptions) {
return {
outlineWidth: null,
boxOpacity: null,
outlineOpacity: null,
shouldUseEmulation: false
};
}
const classificationOptions = renderOptions.classification || {};
const classificationTargets = classificationOptions.classificationTargets || DEFAULT_OPTIONS.classification.classificationTargets || {};
const classificationEnabled = Boolean(classificationOptions.enabled && classifier);
const hasClassificationTarget =
(classificationTargets.opacity || classificationTargets.width) && classificationEnabled;
// v0.1.11-alpha: 適応制御が無効かつ分類ターゲットもない場合は早期リターン
if (!renderOptions.adaptiveOutlines && !hasClassificationTarget) {
return {
outlineWidth: null,
boxOpacity: null,
outlineOpacity: null,
shouldUseEmulation: false
};
}
const { count } = voxelInfo;
const normalizedDensity = statistics.maxCount > statistics.minCount ?
(count - statistics.minCount) / (statistics.maxCount - statistics.minCount) : 0;
let classificationNormalized = null;
let classificationIndex = null;
if (classificationEnabled) {
try {
classificationNormalized = Math.max(0, Math.min(1, classifier.normalize(count ?? 0)));
classificationIndex = classifier.classify(count ?? 0);
} catch (error) {
Logger.warn('AdaptiveController classification normalize failed, fallback to density:', error);
classificationNormalized = null;
}
}
const baseNormalized = classificationNormalized !== null ? classificationNormalized : normalizedDensity;
// 近傍密度を計算
const neighborhoodResult = this.calculateNeighborhoodDensity(voxelInfo, voxelData, null, renderOptions);
const { isDenseArea } = neighborhoodResult;
// v0.1.15 Phase 1: Z軸スケール補正を適用(ADR-0011)
const zScaleFactor = this._calculateZScaleCompensation(voxelInfo, grid);
// v0.1.15 Phase 2: 重なり検出と推奨モード判定(ADR-0011)
const overlapRecommendation = this._detectOverlapAndRecommendMode(voxelInfo, voxelData, renderOptions);
// カメラ距離は簡略化(実装では固定値を使用)
const cameraDistance = 1000; // 固定値、実際の実装ではカメラからの距離を取得
const controllerAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(this.options?.adaptiveParams || {}),
...(renderOptions?.adaptiveParams || {})
};
const cameraDistanceFactor = controllerAdaptiveParams.cameraDistanceFactor ?? 1.0;
const overlapRiskFactor = controllerAdaptiveParams.overlapRiskFactor ?? 0;
const cameraFactor = Math.min(1.0, 1000 / cameraDistance) * cameraDistanceFactor;
// 重なりリスクの算出
const overlapRisk = isDenseArea ? overlapRiskFactor : 0;
// プリセットによる調整
const presetResult = this.applyPresetLogic(
renderOptions.outlineWidthPreset,
isTopN,
normalizedDensity,
isDenseArea,
renderOptions
);
// Range & clamp adjustments (v0.1.15 Phase 0/1)
const rangeConfig = (renderOptions && renderOptions.adaptiveParams) || controllerAdaptiveParams || {};
const interpolateRange = (range, normalizedValue, fallback = null) => {
if (Array.isArray(range) && range.length === 2) {
const [minRange, maxRange] = range;
const span = (maxRange ?? 0) - (minRange ?? 0);
const minVal = minRange ?? 0;
return minVal + Math.max(0, Math.min(1, normalizedValue)) * span;
}
return fallback;
};
// v0.1.15 Phase 1: Z軸補正を含めた最終調整(ADR-0011)
let finalWidth = presetResult.adaptiveWidth * cameraFactor * zScaleFactor;
let finalOutlineOpacity = Math.max(0.2, presetResult.adaptiveOutlineOpacity * (1 - overlapRisk));
if (classificationTargets.width && classificationEnabled) {
const interpolatedWidth = interpolateRange(rangeConfig.outlineWidthRange, baseNormalized, finalWidth);
if (interpolatedWidth !== null && interpolatedWidth !== undefined) {
const topNBoost = isTopN && renderOptions.highlightTopN ? (renderOptions.highlightTopNWidthBoost || 0) : 0;
finalWidth = interpolatedWidth + topNBoost;
}
}
if (classificationTargets.opacity && classificationEnabled) {
const interpolatedBoxOpacity = interpolateRange(rangeConfig.boxOpacityRange, baseNormalized, presetResult.adaptiveBoxOpacity);
const interpolatedOutlineOpacity = interpolateRange(rangeConfig.outlineOpacityRange, baseNormalized, finalOutlineOpacity);
if (interpolatedBoxOpacity !== null && interpolatedBoxOpacity !== undefined) {
presetResult.adaptiveBoxOpacity = interpolatedBoxOpacity;
}
if (interpolatedOutlineOpacity !== null && interpolatedOutlineOpacity !== undefined) {
finalOutlineOpacity = interpolatedOutlineOpacity;
}
}
const clampWithRange = (value, range, hardMin, hardMax) => {
let clamped = value;
if (Array.isArray(range) && range.length === 2) {
const [minRange, maxRange] = range;
const minVal = (minRange !== undefined && minRange !== null) ? minRange : hardMin;
const maxVal = (maxRange !== undefined && maxRange !== null) ? maxRange : hardMax;
clamped = Math.min(maxVal ?? clamped, Math.max(minVal ?? clamped, clamped));
}
if (hardMin !== undefined && hardMin !== null) {
clamped = Math.max(hardMin, clamped);
}
if (hardMax !== undefined && hardMax !== null) {
clamped = Math.min(hardMax, clamped);
}
return clamped;
};
const clampedOutlineWidth = clampWithRange(
Math.max(1.0, finalWidth),
rangeConfig.outlineWidthRange,
controllerAdaptiveParams.minOutlineWidth ?? 1.0,
controllerAdaptiveParams.maxOutlineWidth ?? null
);
const clampedBoxOpacity = clampWithRange(
Math.max(0.0, Math.min(1.0, presetResult.adaptiveBoxOpacity)),
rangeConfig.boxOpacityRange,
0,
1
);
const clampedOutlineOpacity = clampWithRange(
Math.max(0.2, Math.min(1.0, finalOutlineOpacity)),
rangeConfig.outlineOpacityRange,
0,
1
);
return {
// v0.1.12-alpha.10: RangeError防止のため最小値を1.0に設定
outlineWidth: clampedOutlineWidth,
boxOpacity: clampedBoxOpacity,
outlineOpacity: clampedOutlineOpacity,
shouldUseEmulation: isDenseArea || (finalWidth > 2 && renderOptions.outlineRenderMode !== 'standard'),
// Debug info for testing / テスト用デバッグ情報
_debug: {
normalizedDensity,
classificationNormalized,
classificationIndex,
neighborhoodResult,
cameraFactor,
overlapRisk,
zScaleFactor, // v0.1.15 Phase 1: Z軸補正係数
overlapRecommendation, // v0.1.15 Phase 2: 重なり検出結果
preset: renderOptions.outlineWidthPreset
}
};
}
/**
* Update adaptive control options
* 適応制御オプションを更新
*
* @param {Object} newOptions - New options to merge / マージする新オプション
*/
updateOptions(newOptions) {
this.options = {
...this.options,
...newOptions,
adaptiveParams: {
...this.options.adaptiveParams,
...(newOptions.adaptiveParams || {})
}
};
Logger.debug('AdaptiveController options updated:', this.options);
}
/**
* Get current adaptive control configuration
* 現在の適応制御設定を取得
*
* @returns {Object} Current configuration / 現在の設定
*/
getConfiguration() {
return {
...this.options,
version: '0.1.11',
phase: 'ADR-0009 Phase 3'
};
}
}
日本語
import { Logger } from '../../utils/logger.js';
import { DEFAULT_OPTIONS } from '../../utils/constants.js';
const DEFAULT_ADAPTIVE_PARAMS = DEFAULT_OPTIONS.adaptiveParams || {};
/**
* AdaptiveController - Adaptive outline logic delegated from VoxelRenderer.
* 適応的制御ロジック - ボクセルレンダラーから委譲されるアウトライン制御を担当
*
* Responsibilities:
* - 近傍密度計算 (Neighborhood density calculation)
* - プリセット適用ロジック (Preset application logic)
* - 適応的パラメータ計算 (Adaptive parameter calculation)
* - Z軸スケール補正と重なり検出の推奨提示 (Z scale compensation & overlap recommendations)
*
* ADR-0009 Phase 3 + ADR-0011 Phase 4
* @version 0.1.15
*/
export class AdaptiveController {
/**
* AdaptiveController constructor
* @param {Object} options - Adaptive control options / 適応制御オプション
* @param {Object} options.adaptiveParams - Adaptive parameters / 適応パラメータ
* Properties: `neighborhoodRadius`, `densityThreshold`, `cameraDistanceFactor`,
* `overlapRiskFactor`, `outlineWidthRange`, `boxOpacityRange`,
* `outlineOpacityRange`, `zScaleCompensation`, `overlapDetection`
* / プロパティ: 近傍探索半径・密集判定閾値・カメラ距離係数・重なりリスク係数・
* 枠線太さ範囲・ボックス不透明度範囲・枠線不透明度範囲・Z軸補正フラグ・重なり検出フラグ
*/
constructor(options = {}) {
const mergedAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(options.adaptiveParams || {})
};
this.options = {
...options,
adaptiveParams: mergedAdaptiveParams
};
Logger.debug('AdaptiveController initialized with options:', this.options);
}
/**
* Calculate neighborhood density around a voxel
* ボクセル周辺の近傍密度を計算
*
* @param {Object} voxelInfo - Target voxel information (`x`, `y`, `z` number) / 対象ボクセル情報(`x`・`y`・`z` は数値)
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @param {number} [radius] - Search radius override / 探索半径オーバーライド
* @param {Object} [renderOptions] - Live render options snapshot / 現在の描画オプション
* @returns {Object} Neighborhood density result / 近傍密度結果
*/
calculateNeighborhoodDensity(voxelInfo, voxelData, radius = null, renderOptions = null) {
// voxelDataのnull/undefined安全性チェック
if (!voxelData || typeof voxelData.get !== 'function') {
return {
isDenseArea: false,
neighborhoodDensity: 0,
neighborCount: 0
};
}
const { x, y, z } = voxelInfo;
const controllerAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(this.options?.adaptiveParams || {}),
...(renderOptions?.adaptiveParams || {})
};
const effectiveRadius = controllerAdaptiveParams.neighborhoodRadius ?? DEFAULT_ADAPTIVE_PARAMS.neighborhoodRadius ?? 30;
const searchRadius = radius !== null ? radius :
Math.max(1, Math.floor(effectiveRadius / 20)); // 簡略化
let neighborhoodDensity = 0;
let neighborCount = 0;
for (let dx = -searchRadius; dx <= searchRadius; dx++) {
for (let dy = -searchRadius; dy <= searchRadius; dy++) {
for (let dz = -searchRadius; dz <= searchRadius; dz++) {
if (dx === 0 && dy === 0 && dz === 0) continue;
const neighborKey = `${x + dx},${y + dy},${z + dz}`;
const neighbor = voxelData.get(neighborKey);
if (neighbor) {
neighborhoodDensity += neighbor.count;
neighborCount++;
}
}
}
}
const avgNeighborhoodDensity = neighborCount > 0 ? neighborhoodDensity / neighborCount : 0;
const densityThreshold = controllerAdaptiveParams.densityThreshold ?? DEFAULT_ADAPTIVE_PARAMS.densityThreshold ?? 5;
const isDenseArea = avgNeighborhoodDensity > densityThreshold;
return {
totalDensity: neighborhoodDensity,
neighborCount,
avgDensity: avgNeighborhoodDensity,
isDenseArea,
searchRadius
};
}
/**
* Calculate Z-axis scale compensation factor
* Z軸スケール補正係数を計算(v0.1.15 Phase 1 - ADR-0011)
*
* @param {Object} voxelInfo - Target voxel information / 対象ボクセル情報
* @param {Object} grid - Grid information with cellSizeX/Y/Z / グリッド情報
* @returns {number} Scale compensation factor / スケール補正係数
*/
_calculateZScaleCompensation(voxelInfo, grid) {
const controllerAdaptiveParams = this.options.adaptiveParams || DEFAULT_ADAPTIVE_PARAMS;
if (!grid || !controllerAdaptiveParams.zScaleCompensation) {
return 1.0;
}
const { cellSizeX, cellSizeY, cellSizeZ } = grid;
if (!cellSizeX || !cellSizeY || !cellSizeZ) {
return 1.0;
}
const avgHorizontalSize = (cellSizeX + cellSizeY) / 2;
const aspectRatio = cellSizeZ / avgHorizontalSize;
// Z軸が極小の場合は補正を適用
if (aspectRatio < 0.1) {
return Math.max(0.7, Math.min(1.3, 1.0 + (0.1 - aspectRatio) * 2));
}
return 1.0;
}
/**
* Count adjacent voxels (6 directions: ±X, ±Y, ±Z)
* 隣接ボクセルをカウント(6方向:±X, ±Y, ±Z)(v0.1.15 Phase 2 - ADR-0011)
*
* @param {Object} voxelInfo - Target voxel information (`x`, `y`, `z` number) / 対象ボクセル情報(`x`・`y`・`z` は数値)
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @returns {number} Number of adjacent voxels / 隣接ボクセル数
*/
_countAdjacentVoxels(voxelInfo, voxelData) {
if (!voxelInfo || !voxelData || typeof voxelData.get !== 'function') {
return 0;
}
const { x, y, z } = voxelInfo;
const adjacentDirections = [
[1, 0, 0], // +X
[-1, 0, 0], // -X
[0, 1, 0], // +Y
[0, -1, 0], // -Y
[0, 0, 1], // +Z
[0, 0, -1] // -Z
];
let adjacentCount = 0;
for (const [dx, dy, dz] of adjacentDirections) {
const neighborKey = `${x + dx},${y + dy},${z + dz}`;
if (voxelData.get(neighborKey)) {
adjacentCount++;
}
}
return adjacentCount;
}
/**
* Detect overlap and recommend rendering mode
* 隣接重なりを検出してレンダリングモードを推奨(v0.1.15 Phase 2 - ADR-0011)
*
* @param {Object} voxelInfo - Target voxel information (`x`, `y`, `z` number) / 対象ボクセル情報(`x`・`y`・`z` は数値)
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @returns {Object} Recommended rendering settings / 推奨レンダリング設定
* @returns {string} returns.recommendedMode - Recommended outline render mode / 推奨アウトライン描画モード
* @returns {number} returns.recommendedInset - Recommended inset value / 推奨インセット値
* @returns {string} [returns.reason] - Reason for recommendation / 推奨理由
*/
_detectOverlapAndRecommendMode(voxelInfo, voxelData, renderOptions = null) {
const liveOptions = renderOptions || this.options || {};
const liveAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(this.options?.adaptiveParams || {}),
...(renderOptions?.adaptiveParams || {})
};
const currentMode = liveOptions.outlineRenderMode || 'standard';
const currentInset = liveOptions.outlineInset || 0;
// overlapDetection が無効な場合は現在の設定を返す
if (!liveAdaptiveParams.overlapDetection) {
return {
recommendedMode: currentMode,
recommendedInset: currentInset
};
}
const adjacentCount = this._countAdjacentVoxels(voxelInfo, voxelData);
const overlapRisk = adjacentCount / 6; // 最大6方向
// 重なりリスクが高い場合、insetモードを推奨
if (overlapRisk > 0.5 && currentMode !== 'emulation-only') {
return {
recommendedMode: 'inset',
recommendedInset: Math.max(0.3, 0.8 - overlapRisk * 0.4),
reason: `High overlap risk (${(overlapRisk * 100).toFixed(0)}%)`
};
}
return {
recommendedMode: currentMode,
recommendedInset: currentInset
};
}
/**
* Apply preset-specific adaptive logic
* プリセット固有の適応ロジックを適用
*
* @param {string} preset - Outline width preset / アウトライン幅プリセット
* @param {boolean} isTopN - Whether it is TopN voxel / TopNボクセルかどうか
* @param {number} normalizedDensity - Normalized density [0-1] / 正規化密度 [0-1]
* @param {boolean} isDenseArea - Whether it is dense area / 密集エリアかどうか
* @param {Object} baseOptions - Base options for calculation / 計算用基準オプション
* @returns {Object} Applied preset parameters / 適用済みプリセットパラメータ
*/
applyPresetLogic(preset, isTopN, normalizedDensity, isDenseArea, baseOptions) {
let adaptiveWidth, adaptiveBoxOpacity, adaptiveOutlineOpacity;
switch (preset) {
// New names (v0.1.12)
case 'thin':
// v0.1.12-alpha.10: 最小値を1.0に設定してRangeError防止
adaptiveWidth = Math.max(1.0, baseOptions.outlineWidth * 0.8);
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 0.8;
break;
case 'medium':
adaptiveWidth = baseOptions.outlineWidth;
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 1.0;
break;
case 'thick':
adaptiveWidth = Math.max(1, baseOptions.outlineWidth * 1.5);
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 1.0;
break;
case 'adaptive':
case 'adaptive-density': {
// v0.1.15 Phase 1: より柔軟で安定した調整(ADR-0011)
// 密度に応じたベース係数(中央値を基準に調整)
const baseFactor = isDenseArea ?
Math.max(0.6, 0.8 + (normalizedDensity - 0.5) * 0.3) : 1.0; // 0.6-0.95倍(密集時)
// Z軸スケール補正を適用(有効な場合)
// 注: voxelInfoとgridはcalculateAdaptiveParams内でのみ利用可能
// ここではbaseFactor * zScaleFactorの形で後段で適用される想定
adaptiveWidth = Math.max(1.0, Math.min(baseOptions.outlineWidth * 3.0,
baseOptions.outlineWidth * baseFactor));
adaptiveBoxOpacity = isDenseArea ? baseOptions.opacity * 0.8 : baseOptions.opacity;
adaptiveOutlineOpacity = isDenseArea ? 0.6 : 1.0;
break;
}
// Legacy name (map to thick/topn focus)
case 'topn-focus':
// v0.1.12-alpha.10: 安全な値範囲でRangeError防止
adaptiveWidth = isTopN ?
Math.max(1.0, Math.min(baseOptions.outlineWidth * 3.0,
baseOptions.outlineWidth * (1.5 + normalizedDensity * 0.5))) :
Math.max(1.0, baseOptions.outlineWidth * 0.8); // 0.5→0.8で最小値を安全に
adaptiveBoxOpacity = isTopN ? baseOptions.opacity : baseOptions.opacity * 0.6;
adaptiveOutlineOpacity = isTopN ? 1.0 : 0.4;
break;
// Legacy name (uniform)
case 'uniform':
default:
adaptiveWidth = baseOptions.outlineWidth;
adaptiveBoxOpacity = baseOptions.opacity;
adaptiveOutlineOpacity = baseOptions.outlineOpacity || 1.0;
break;
}
return {
adaptiveWidth,
adaptiveBoxOpacity,
adaptiveOutlineOpacity
};
}
/**
* Calculate adaptive parameters for a voxel
* ボクセルの適応的パラメータを計算
*
* @param {Object} voxelInfo - Voxel information / ボクセル情報
* @param {boolean} isTopN - Whether it is TopN voxel / TopNボクセルかどうか
* @param {Map} voxelData - All voxel data / 全ボクセルデータ
* @param {Object} statistics - Statistics information / 統計情報
* @param {Object} renderOptions - Rendering options / 描画オプション
* @param {Object} [grid] - Grid information (optional, for Z-scale compensation) / グリッド情報(オプション、Z軸補正用)
* @returns {Object} Adaptive parameters / 適応的パラメータ
*/
calculateAdaptiveParams(voxelInfo, isTopN, voxelData, statistics, renderOptions, grid = null, classifier = null) {
// 引数の安全性チェック
if (!voxelInfo || !statistics || !renderOptions) {
return {
outlineWidth: null,
boxOpacity: null,
outlineOpacity: null,
shouldUseEmulation: false
};
}
const classificationOptions = renderOptions.classification || {};
const classificationTargets = classificationOptions.classificationTargets || DEFAULT_OPTIONS.classification.classificationTargets || {};
const classificationEnabled = Boolean(classificationOptions.enabled && classifier);
const hasClassificationTarget =
(classificationTargets.opacity || classificationTargets.width) && classificationEnabled;
// v0.1.11-alpha: 適応制御が無効かつ分類ターゲットもない場合は早期リターン
if (!renderOptions.adaptiveOutlines && !hasClassificationTarget) {
return {
outlineWidth: null,
boxOpacity: null,
outlineOpacity: null,
shouldUseEmulation: false
};
}
const { count } = voxelInfo;
const normalizedDensity = statistics.maxCount > statistics.minCount ?
(count - statistics.minCount) / (statistics.maxCount - statistics.minCount) : 0;
let classificationNormalized = null;
let classificationIndex = null;
if (classificationEnabled) {
try {
classificationNormalized = Math.max(0, Math.min(1, classifier.normalize(count ?? 0)));
classificationIndex = classifier.classify(count ?? 0);
} catch (error) {
Logger.warn('AdaptiveController classification normalize failed, fallback to density:', error);
classificationNormalized = null;
}
}
const baseNormalized = classificationNormalized !== null ? classificationNormalized : normalizedDensity;
// 近傍密度を計算
const neighborhoodResult = this.calculateNeighborhoodDensity(voxelInfo, voxelData, null, renderOptions);
const { isDenseArea } = neighborhoodResult;
// v0.1.15 Phase 1: Z軸スケール補正を適用(ADR-0011)
const zScaleFactor = this._calculateZScaleCompensation(voxelInfo, grid);
// v0.1.15 Phase 2: 重なり検出と推奨モード判定(ADR-0011)
const overlapRecommendation = this._detectOverlapAndRecommendMode(voxelInfo, voxelData, renderOptions);
// カメラ距離は簡略化(実装では固定値を使用)
const cameraDistance = 1000; // 固定値、実際の実装ではカメラからの距離を取得
const controllerAdaptiveParams = {
...DEFAULT_ADAPTIVE_PARAMS,
...(this.options?.adaptiveParams || {}),
...(renderOptions?.adaptiveParams || {})
};
const cameraDistanceFactor = controllerAdaptiveParams.cameraDistanceFactor ?? 1.0;
const overlapRiskFactor = controllerAdaptiveParams.overlapRiskFactor ?? 0;
const cameraFactor = Math.min(1.0, 1000 / cameraDistance) * cameraDistanceFactor;
// 重なりリスクの算出
const overlapRisk = isDenseArea ? overlapRiskFactor : 0;
// プリセットによる調整
const presetResult = this.applyPresetLogic(
renderOptions.outlineWidthPreset,
isTopN,
normalizedDensity,
isDenseArea,
renderOptions
);
// Range & clamp adjustments (v0.1.15 Phase 0/1)
const rangeConfig = (renderOptions && renderOptions.adaptiveParams) || controllerAdaptiveParams || {};
const interpolateRange = (range, normalizedValue, fallback = null) => {
if (Array.isArray(range) && range.length === 2) {
const [minRange, maxRange] = range;
const span = (maxRange ?? 0) - (minRange ?? 0);
const minVal = minRange ?? 0;
return minVal + Math.max(0, Math.min(1, normalizedValue)) * span;
}
return fallback;
};
// v0.1.15 Phase 1: Z軸補正を含めた最終調整(ADR-0011)
let finalWidth = presetResult.adaptiveWidth * cameraFactor * zScaleFactor;
let finalOutlineOpacity = Math.max(0.2, presetResult.adaptiveOutlineOpacity * (1 - overlapRisk));
if (classificationTargets.width && classificationEnabled) {
const interpolatedWidth = interpolateRange(rangeConfig.outlineWidthRange, baseNormalized, finalWidth);
if (interpolatedWidth !== null && interpolatedWidth !== undefined) {
const topNBoost = isTopN && renderOptions.highlightTopN ? (renderOptions.highlightTopNWidthBoost || 0) : 0;
finalWidth = interpolatedWidth + topNBoost;
}
}
if (classificationTargets.opacity && classificationEnabled) {
const interpolatedBoxOpacity = interpolateRange(rangeConfig.boxOpacityRange, baseNormalized, presetResult.adaptiveBoxOpacity);
const interpolatedOutlineOpacity = interpolateRange(rangeConfig.outlineOpacityRange, baseNormalized, finalOutlineOpacity);
if (interpolatedBoxOpacity !== null && interpolatedBoxOpacity !== undefined) {
presetResult.adaptiveBoxOpacity = interpolatedBoxOpacity;
}
if (interpolatedOutlineOpacity !== null && interpolatedOutlineOpacity !== undefined) {
finalOutlineOpacity = interpolatedOutlineOpacity;
}
}
const clampWithRange = (value, range, hardMin, hardMax) => {
let clamped = value;
if (Array.isArray(range) && range.length === 2) {
const [minRange, maxRange] = range;
const minVal = (minRange !== undefined && minRange !== null) ? minRange : hardMin;
const maxVal = (maxRange !== undefined && maxRange !== null) ? maxRange : hardMax;
clamped = Math.min(maxVal ?? clamped, Math.max(minVal ?? clamped, clamped));
}
if (hardMin !== undefined && hardMin !== null) {
clamped = Math.max(hardMin, clamped);
}
if (hardMax !== undefined && hardMax !== null) {
clamped = Math.min(hardMax, clamped);
}
return clamped;
};
const clampedOutlineWidth = clampWithRange(
Math.max(1.0, finalWidth),
rangeConfig.outlineWidthRange,
controllerAdaptiveParams.minOutlineWidth ?? 1.0,
controllerAdaptiveParams.maxOutlineWidth ?? null
);
const clampedBoxOpacity = clampWithRange(
Math.max(0.0, Math.min(1.0, presetResult.adaptiveBoxOpacity)),
rangeConfig.boxOpacityRange,
0,
1
);
const clampedOutlineOpacity = clampWithRange(
Math.max(0.2, Math.min(1.0, finalOutlineOpacity)),
rangeConfig.outlineOpacityRange,
0,
1
);
return {
// v0.1.12-alpha.10: RangeError防止のため最小値を1.0に設定
outlineWidth: clampedOutlineWidth,
boxOpacity: clampedBoxOpacity,
outlineOpacity: clampedOutlineOpacity,
shouldUseEmulation: isDenseArea || (finalWidth > 2 && renderOptions.outlineRenderMode !== 'standard'),
// Debug info for testing / テスト用デバッグ情報
_debug: {
normalizedDensity,
classificationNormalized,
classificationIndex,
neighborhoodResult,
cameraFactor,
overlapRisk,
zScaleFactor, // v0.1.15 Phase 1: Z軸補正係数
overlapRecommendation, // v0.1.15 Phase 2: 重なり検出結果
preset: renderOptions.outlineWidthPreset
}
};
}
/**
* Update adaptive control options
* 適応制御オプションを更新
*
* @param {Object} newOptions - New options to merge / マージする新オプション
*/
updateOptions(newOptions) {
this.options = {
...this.options,
...newOptions,
adaptiveParams: {
...this.options.adaptiveParams,
...(newOptions.adaptiveParams || {})
}
};
Logger.debug('AdaptiveController options updated:', this.options);
}
/**
* Get current adaptive control configuration
* 現在の適応制御設定を取得
*
* @returns {Object} Current configuration / 現在の設定
*/
getConfiguration() {
return {
...this.options,
version: '0.1.11',
phase: 'ADR-0009 Phase 3'
};
}
}