初始化

This commit is contained in:
yuding
2025-12-24 19:02:34 +08:00
parent 4b5eb78bbb
commit 04a5e74284
51 changed files with 8576 additions and 5334 deletions

View File

@@ -0,0 +1,154 @@
.section-box-panel {
display: flex;
flex-direction: column;
padding: 12px;
box-sizing: border-box;
user-select: none;
}
.section-box-row-buttons {
display: flex;
gap: 6px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.section-box-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px;
background: var(--bim-section-box-btn-bg);
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
color: var(--bim-text-color);
min-height: 44px;
transition: all 0.2s;
}
.section-box-btn:hover {
background: var(--bim-section-box-btn-hover);
}
.section-box-btn.active {
background: var(--bim-section-box-btn-active);
border-color: var(--bim-text-active-color);
color: var(--bim-text-active-color);
}
.section-box-btn-icon {
width: 18px;
height: 18px;
color: var(--bim-icon-color);
}
.section-box-btn-icon svg {
width: 100%;
height: 100%;
}
.section-box-btn-label {
font-size: 11px;
white-space: nowrap;
}
/* 滑块区域 */
.section-box-sliders {
display: flex;
flex-direction: column;
gap: 16px;
/* 增加行间距防止重叠 */
padding-top: 16px;
}
.section-box-slider {
display: flex;
align-items: center;
gap: 12px;
position: relative;
z-index: 1;
}
/* 鼠标移入当前轴时提升层级 */
.section-box-slider:hover {
z-index: 10;
}
.section-box-slider-label {
font-size: 13px;
font-weight: bold;
color: var(--bim-text-color);
min-width: 14px;
}
.section-box-slider-track {
position: relative;
flex: 1;
height: 4px;
/* 轨道变细 */
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.section-box-slider-range {
position: absolute;
top: 0;
height: 100%;
background: var(--bim-primary-color);
border-radius: 2px;
pointer-events: none;
/* 防止遮挡手柄点击 */
}
.section-box-slider-handle {
position: absolute;
top: 50%;
width: 14px;
/* 手柄变小 */
height: 14px;
background: #fff;
border: 2px solid var(--bim-primary-color);
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: grab;
z-index: 5;
touch-action: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.section-box-slider-handle:hover {
transform: translate(-50%, -50%) scale(1.2);
box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.2);
}
.section-box-slider-handle.dragging {
cursor: grabbing;
transform: translate(-50%, -50%) scale(1.2);
background: var(--bim-primary-color);
}
/* 数值显示气泡 */
.section-box-slider-handle::after {
content: attr(data-value);
position: absolute;
top: -22px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 2px 5px;
border-radius: 3px;
font-size: 10px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.section-box-slider-handle:hover::after,
.section-box-slider-handle.dragging::after {
opacity: 1;
}

View File

@@ -0,0 +1,360 @@
import './index.css';
import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { SectionBoxPanelOptions, SectionBoxRange } from './types';
const DEFAULT_RANGE: SectionBoxRange = {
x: { min: 0, max: 100 },
y: { min: 0, max: 100 },
z: { min: 0, max: 100 }
};
export class SectionBoxPanel implements IBimComponent {
public element!: HTMLElement;
private options: SectionBoxPanelOptions;
private isHidden: boolean = false;
private isReversed: boolean = false;
private range: SectionBoxRange;
private hideBtn!: HTMLButtonElement;
private reverseBtn!: HTMLButtonElement;
private fitBtn!: HTMLButtonElement;
private resetBtn!: HTMLButtonElement;
private hideLabelEl!: HTMLElement;
private reverseLabelEl!: HTMLElement;
private fitLabelEl!: HTMLElement;
private resetLabelEl!: HTMLElement;
private xLabelEl!: HTMLElement;
private yLabelEl!: HTMLElement;
private zLabelEl!: HTMLElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
private xSlider!: HTMLElement;
private ySlider!: HTMLElement;
private zSlider!: HTMLElement;
private xMinHandle!: HTMLElement;
private xMaxHandle!: HTMLElement;
private yMinHandle!: HTMLElement;
private yMaxHandle!: HTMLElement;
private zMinHandle!: HTMLElement;
private zMaxHandle!: HTMLElement;
private dragState: {
isDragging: boolean;
axis: 'x' | 'y' | 'z' | null;
handleType: 'min' | 'max' | null;
pointerId: number | null;
} = {
isDragging: false,
axis: null,
handleType: null,
pointerId: null
};
constructor(options: SectionBoxPanelOptions = {}) {
this.options = options;
this.isHidden = options.defaultHidden ?? false;
this.isReversed = options.defaultReversed ?? false;
this.range = JSON.parse(JSON.stringify(options.defaultRange ?? DEFAULT_RANGE));
}
public init(): void {
this.element = this.createPanel();
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
this.updateButtonStates();
this.updateAllSlidersUI();
this.setupDragListeners();
}
// --- Public APIs ---
public setHiddenState(isHidden: boolean): void {
this.isHidden = isHidden;
this.updateButtonStates();
}
public getHiddenState(): boolean {
return this.isHidden;
}
public setReversedState(isReversed: boolean): void {
this.isReversed = isReversed;
this.updateButtonStates();
}
public getReversedState(): boolean {
return this.isReversed;
}
public setRange(range: Partial<SectionBoxRange>): void {
if (range.x) this.range.x = { ...this.range.x, ...range.x };
if (range.y) this.range.y = { ...this.range.y, ...range.y };
if (range.z) this.range.z = { ...this.range.z, ...range.z };
this.updateAllSlidersUI();
}
public getRange(): SectionBoxRange {
return JSON.parse(JSON.stringify(this.range));
}
public reset(): void {
this.isHidden = this.options.defaultHidden ?? false;
this.isReversed = this.options.defaultReversed ?? false;
this.range = JSON.parse(JSON.stringify(this.options.defaultRange ?? DEFAULT_RANGE));
this.updateButtonStates();
this.updateAllSlidersUI();
this.options.onReset?.();
this.options.onRangeChange?.(this.range);
}
// --- Private Setup ---
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'section-box-panel';
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'section-box-row-buttons';
this.hideBtn = this.createButton('hide', t('sectionBox.actions.hide'), () => {
this.isHidden = !this.isHidden;
this.updateButtonStates();
this.options.onHideToggle?.(this.isHidden);
}, 'hide');
this.reverseBtn = this.createButton('reverse', t('sectionBox.actions.reverse'), () => {
this.isReversed = !this.isReversed;
this.updateButtonStates();
this.options.onReverseToggle?.(this.isReversed);
}, 'reverse');
this.fitBtn = this.createButton('fit', t('sectionBox.actions.fitToModel'), () => {
this.options.onFitToModel?.();
}, 'fit');
this.resetBtn = this.createButton('reset', t('sectionBox.actions.reset'), () => this.reset(), 'reset');
[this.hideBtn, this.reverseBtn, this.fitBtn, this.resetBtn].forEach(btn => buttonsContainer.appendChild(btn));
const slidersContainer = document.createElement('div');
slidersContainer.className = 'section-box-sliders';
slidersContainer.addEventListener('pointerdown', (e) => e.stopPropagation());
this.xSlider = this.createSlider('x', t('sectionBox.axes.x'));
this.ySlider = this.createSlider('y', t('sectionBox.axes.y'));
this.zSlider = this.createSlider('z', t('sectionBox.axes.z'));
[this.xSlider, this.ySlider, this.zSlider].forEach(s => slidersContainer.appendChild(s));
panel.appendChild(buttonsContainer);
panel.appendChild(slidersContainer);
return panel;
}
private createButton(type: string, label: string, onClick: () => void, ref?: string): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = 'section-box-btn';
btn.title = label;
const icon = document.createElement('div');
icon.className = 'section-box-btn-icon';
icon.innerHTML = this.getIconSVG(type);
const labelEl = document.createElement('div');
labelEl.className = 'section-box-btn-label';
labelEl.textContent = label;
if (ref === 'hide') this.hideLabelEl = labelEl;
else if (ref === 'reverse') this.reverseLabelEl = labelEl;
else if (ref === 'fit') this.fitLabelEl = labelEl;
else if (ref === 'reset') this.resetLabelEl = labelEl;
btn.appendChild(icon);
btn.appendChild(labelEl);
btn.addEventListener('click', onClick);
return btn;
}
private createSlider(axis: 'x' | 'y' | 'z', label: string): HTMLElement {
const slider = document.createElement('div');
slider.className = 'section-box-slider';
const labelEl = document.createElement('div');
labelEl.className = 'section-box-slider-label';
labelEl.textContent = label;
if (axis === 'x') this.xLabelEl = labelEl;
else if (axis === 'y') this.yLabelEl = labelEl;
else this.zLabelEl = labelEl;
const track = document.createElement('div');
track.className = 'section-box-slider-track';
const range = document.createElement('div');
range.className = 'section-box-slider-range';
const minHandle = document.createElement('div');
minHandle.className = 'section-box-slider-handle';
minHandle.setAttribute('data-axis', axis);
minHandle.setAttribute('data-handle', 'min');
const maxHandle = document.createElement('div');
maxHandle.className = 'section-box-slider-handle';
maxHandle.setAttribute('data-axis', axis);
maxHandle.setAttribute('data-handle', 'max');
track.append(range, minHandle, maxHandle);
slider.append(labelEl, track);
if (axis === 'x') { this.xMinHandle = minHandle; this.xMaxHandle = maxHandle; }
else if (axis === 'y') { this.yMinHandle = minHandle; this.yMaxHandle = maxHandle; }
else { this.zMinHandle = minHandle; this.zMaxHandle = maxHandle; }
return slider;
}
private setupDragListeners(): void {
const handles = [this.xMinHandle, this.xMaxHandle, this.yMinHandle, this.yMaxHandle, this.zMinHandle, this.zMaxHandle];
handles.forEach(handle => {
handle.addEventListener('pointerdown', (e: PointerEvent) => {
e.preventDefault();
e.stopPropagation();
// 核心锁定:确保后续所有移动事件都只发给这个手柄
handle.setPointerCapture(e.pointerId);
this.dragState = {
isDragging: true,
axis: handle.getAttribute('data-axis') as 'x' | 'y' | 'z',
handleType: handle.getAttribute('data-handle') as 'min' | 'max',
pointerId: e.pointerId
};
handle.classList.add('dragging');
(handle.closest('.section-box-slider') as HTMLElement).style.zIndex = '100';
});
handle.addEventListener('pointermove', (e: PointerEvent) => {
if (this.dragState.isDragging && this.dragState.pointerId === e.pointerId) {
this.onDragging(e);
}
});
const stop = (e: PointerEvent) => {
if (this.dragState.isDragging && this.dragState.pointerId === e.pointerId) {
handle.releasePointerCapture(e.pointerId);
(handle.closest('.section-box-slider') as HTMLElement).style.zIndex = '';
handle.classList.remove('dragging');
this.dragState.isDragging = false;
this.dragState.pointerId = null;
}
};
handle.addEventListener('pointerup', stop);
handle.addEventListener('pointercancel', stop);
});
}
private onDragging(e: PointerEvent): void {
const { axis, handleType } = this.dragState;
if (!axis || !handleType) return;
const sliderEl = axis === 'x' ? this.xSlider : (axis === 'y' ? this.ySlider : this.zSlider);
const track = sliderEl.querySelector('.section-box-slider-track') as HTMLElement;
const rect = track.getBoundingClientRect();
let percentage = ((e.clientX - rect.left) / rect.width) * 100;
percentage = Math.max(0, Math.min(100, percentage));
const current = this.range[axis];
if (handleType === 'min') {
current.min = Math.min(percentage, current.max);
} else {
current.max = Math.max(percentage, current.min);
}
this.updateSliderUI(axis);
this.options.onRangeChange?.(this.range);
}
private updateSliderUI(axis: 'x' | 'y' | 'z'): void {
const range = this.range[axis];
const minH = axis === 'x' ? this.xMinHandle : (axis === 'y' ? this.yMinHandle : this.zMinHandle);
const maxH = axis === 'x' ? this.xMaxHandle : (axis === 'y' ? this.yMaxHandle : this.zMaxHandle);
const slider = axis === 'x' ? this.xSlider : (axis === 'y' ? this.ySlider : this.zSlider);
const rangeEl = slider.querySelector('.section-box-slider-range') as HTMLElement;
minH.style.left = `${range.min}%`;
maxH.style.left = `${range.max}%`;
rangeEl.style.left = `${range.min}%`;
rangeEl.style.width = `${range.max - range.min}%`;
minH.setAttribute('data-value', Math.round(range.min).toString());
maxH.setAttribute('data-value', Math.round(range.max).toString());
}
private updateAllSlidersUI(): void {
['x', 'y', 'z'].forEach((a: any) => this.updateSliderUI(a));
}
private updateButtonStates(): void {
if (this.hideBtn) this.hideBtn.classList.toggle('active', this.isHidden);
if (this.reverseBtn) this.reverseBtn.classList.toggle('active', this.isReversed);
}
private getIconSVG(type: string): string {
const icons: Record<string, string> = {
hide: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>',
reverse: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/></svg>',
fit: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M13 3h-2v10h2V3zm4 8h2v10h-2V11zm-8 4H7v6h2v-6zm-4 3H3v3h2v-3z"/></svg>',
reset: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>'
};
return icons[type] || '';
}
public setLocales(): void {
if (!this.hideLabelEl) return;
this.hideLabelEl.textContent = t('sectionBox.actions.hide');
this.reverseLabelEl.textContent = t('sectionBox.actions.reverse');
this.fitLabelEl.textContent = t('sectionBox.actions.fitToModel');
this.resetLabelEl.textContent = t('sectionBox.actions.reset');
this.xLabelEl.textContent = t('sectionBox.axes.x');
this.yLabelEl.textContent = t('sectionBox.axes.y');
this.zLabelEl.textContent = t('sectionBox.axes.z');
this.hideBtn.title = t('sectionBox.actions.hide');
this.reverseBtn.title = t('sectionBox.actions.reverse');
this.fitBtn.title = t('sectionBox.actions.fitToModel');
this.resetBtn.title = t('sectionBox.actions.reset');
}
public setTheme(theme: ThemeConfig): void {
if (!this.element) return;
const style = this.element.style;
style.setProperty('--bim-section-box-btn-bg', theme.componentBackground ?? 'rgba(255, 255, 255, 0.06)');
style.setProperty('--bim-section-box-btn-hover', theme.componentHover ?? 'rgba(255, 255, 255, 0.10)');
style.setProperty('--bim-section-box-btn-active', theme.componentActive ?? 'rgba(255, 255, 255, 0.14)');
style.setProperty('--bim-primary-color', theme.primary ?? '#1890ff');
style.setProperty('--bim-icon-color', theme.icon ?? '#ccc');
style.setProperty('--bim-text-color', theme.textSecondary ?? 'rgba(255, 255, 255, 0.90)');
style.setProperty('--bim-text-active-color', theme.textPrimary ?? '#fff');
}
public destroy(): void {
this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
}

View File

@@ -0,0 +1,69 @@
/**
* 剖切盒轴向范围
*/
export interface SectionBoxAxisRange {
/** 最小值0-100的百分比 */
min: number;
/** 最大值0-100的百分比 */
max: number;
}
/**
* 剖切盒范围数据
*/
export interface SectionBoxRange {
/** X轴范围 */
x: SectionBoxAxisRange;
/** Y轴范围 */
y: SectionBoxAxisRange;
/** Z轴范围 */
z: SectionBoxAxisRange;
}
/**
* 剖切盒面板配置选项
*/
export interface SectionBoxPanelOptions {
/**
* 隐藏按钮切换回调
* @param isHidden 是否隐藏剖切盒
*/
onHideToggle?: (isHidden: boolean) => void;
/**
* 反向按钮切换回调
* @param isReversed 是否反向
*/
onReverseToggle?: (isReversed: boolean) => void;
/**
* 适应到模型按钮回调
*/
onFitToModel?: () => void;
/**
* 重置按钮回调
*/
onReset?: () => void;
/**
* 范围变化回调
* @param range 当前范围值
*/
onRangeChange?: (range: SectionBoxRange) => void;
/**
* 默认隐藏状态(默认 false
*/
defaultHidden?: boolean;
/**
* 默认反向状态(默认 false
*/
defaultReversed?: boolean;
/**
* 默认范围值
*/
defaultRange?: SectionBoxRange;
}