Skip to content

XLineWidget

简介

通过在视图中选择两个点,从而定义该线段的中点。可交互移动两个端点或是中点。

XLineWidget

主要方法

  • 获取第一端点世界坐标(未放置时返回 null)。
  • 获取第二端点世界坐标(未放置时返回 null)。
  • 获取线段中点世界坐标(= (p1+p2)/2,未放置时返回 null)。
  • 编程方式设置第一端点(跳过交互流程)。
  • 编程方式设置第二端点(跳过交互流程)。
  • 一次性设置两端点。
  • 重置 widget。
  • 设置端点球体颜色 [R, G, B](0-255)。
  • 设置端点 hover 高亮颜色 [R, G, B](0-255)。
  • 设置端点球体大小(世界坐标缩放因子)。
  • 设置线段颜色 [R, G, B](0-255)。
  • 设置线段 hover 高亮颜色 [R, G, B](0-255)。
  • 放置/移动开始时触发。
  • 拖拽过程中持续触发。
  • 放置/移动结束时触发。

可以通过上面的方法主动设置属性,也可以在新建 widget 时,通过构造函数里的参数给定。

ts
// ── 初始化选项 ────────────────────────────────────────────────────────────────
export interface IXLineWidgetInitialValues {
  /** 端点 hover 高亮颜色 [R, G, B](0-255),默认 [255, 120, 0] */
  activeColor?: RGB255;
  /** 端点高亮放大系数,默认 1.35 */
  activeScaleFactor?: number;
  /** 线段默认颜色 [R, G, B](0-255),默认 [0, 230, 255] */
  lineColor?: RGB255;
  /** 线段 hover 高亮颜色 [R, G, B](0-255),默认 [255, 180, 0] */
  activeLineColor?: RGB255;
  /** 线段粗细(像素),默认 2 */
  lineThickness?: number;
  /** 端点 handle 大小(世界坐标缩放),默认 1,实际球体直径 = handleScale */
  handleScale?: number;
  /** 端点是否按像素保持恒定大小,默认 false */
  scaleInPixels?: boolean;
}

使用演示

vue
<template>
  <div
    style="
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 10px;
      height: calc(100vh - 120px);
    "
  >
    <!-- 工具栏 -->
    <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center">
      <button @click="startPlace">重新放置线段</button>
      <button @click="resetWidget">重置</button>
      <span
        v-if="info"
        style="
          padding: 4px 8px;
          background: #1a1a2e;
          color: #42b883;
          border-radius: 4px;
          font-size: 12px;
        "
      >
        {{ info }}
      </span>
    </div>

    <!-- 提示文字 -->
    <div style="font-size: 12px; color: #888; line-height: 1.6">
      <span v-if="phase === 'IDLE'">点击视图放置第一个端点</span>
      <span v-else-if="phase === 'PLACING'"
        >移动鼠标预览,再次点击放置第二个端点</span
      >
      <span v-else>hover 端点可拖动端点 · hover 线段可平移整条线段</span>
    </div>

    <!-- 3D 视图 -->
    <XView3D
      ref="view3D"
      viewID="xline-widget-demo"
      :background="[0.08, 0.08, 0.12, 1]"
      style="flex: 1; border: 2px solid #42b883; border-radius: 4px"
    />
  </div>
</template>

<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import XView3D from "@/components/XView3D.vue";
import vtkWidgetManager from "@kitware/vtk.js/Widgets/Core/WidgetManager";
import XLineWidget from "@/widgets/XLineWidget/index.js";

// ── 组件状态 ──────────────────────────────────────────────────────────────────
const view3D = ref<InstanceType<typeof XView3D> | null>(null);
const info = ref<string | null>(null);
const phase = ref<"IDLE" | "PLACING" | "PLACED">("IDLE");

let widgetManager: ReturnType<typeof vtkWidgetManager.newInstance> | null =
  null;
let widget: ReturnType<typeof XLineWidget.newInstance> | null = null;
let widgetView: any = null;

// ── 初始化 ────────────────────────────────────────────────────────────────────
onMounted(() => {
  if (!view3D.value) return;

  const renderer = view3D.value.getRenderer();
  const renderWindow = view3D.value.getRenderWindow();

  // ① 创建 WidgetManager
  widgetManager = vtkWidgetManager.newInstance();
  widgetManager!.setRenderer(renderer);

  // ② 创建 XLineWidget
  widget = XLineWidget.newInstance({
    lineColor: [0, 230, 255],
    activeLineColor: [255, 180, 0],
    activeColor: [255, 120, 0],
    activeScaleFactor: 1.4,
    lineThickness: 4,
    handleScale: 0.1,
  });

  // ③ 注册到场景
  widgetView = widgetManager!.addWidget(widget as any);

  // ④ 监听交互事件(放置完第二点 / 拖拽结束均会触发)
  widgetView?.onEndInteractionEvent(() => {
    const p1 = widget!.getPoint1();
    const p2 = widget!.getPoint2();
    const mid = widget!.getMidpoint();
    if (p1 && p2 && mid) {
      phase.value = "PLACED";
      info.value =
        `P1: (${p1.map((v: number) => v.toFixed(1)).join(", ")})  ` +
        `P2: (${p2.map((v: number) => v.toFixed(1)).join(", ")})  ` +
        `Mid: (${mid.map((v: number) => v.toFixed(1)).join(", ")})`;
    }
    // resetCameraClippingRange 会把 near/far 压缩到 z=0 面,导致世界坐标球体被裁剪。
    // 保持固定的充足裁剪范围即可。
    renderer.getActiveCamera().setClippingRange(1, 10000);
    renderWindow.render();
  });

  // ⑤ 监听开始交互:PLACING 阶段(放置第一点时触发)
  widgetView?.onStartInteractionEvent(() => {
    if (phase.value === "IDLE") {
      phase.value = "PLACING";
    }
  });

  // ⑥ 进入放置模式
  widgetManager!.grabFocus(widget as any);
  phase.value = "IDLE";

  // 场景为空时 resetCamera() 会把相机停在极近处(z≈1),导致球体充满视图。
  // 手动设置合理的初始相机:退后 500 个世界单位,clipping range 覆盖整个操作空间。
  const camera = renderer.getActiveCamera();
  camera.setPosition(0, 0, 500);
  camera.setFocalPoint(0, 0, 0);
  camera.setViewUp(0, 1, 0);
  camera.setClippingRange(1, 10000);
  renderWindow.render();
});

onBeforeUnmount(() => {
  widgetManager?.delete();
});

// ── 按钮操作 ──────────────────────────────────────────────────────────────────

/** 重新开始放置线段(保持 widget,仅 reset) */
function startPlace() {
  widget!.reset();
  info.value = null;
  phase.value = "IDLE";
  widgetManager!.grabFocus(widget as any);
  view3D.value?.getRenderWindow().render();
}

/** 完整重置 */
function resetWidget() {
  startPlace();
}
</script>